diff --git a/packages/app/e2e/AGENTS.md b/packages/app/e2e/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..e0c6c006021d3ce5d75b3c14c5b95a63e8c80ae2 --- /dev/null +++ b/packages/app/e2e/AGENTS.md @@ -0,0 +1,16 @@ +## Required Reading + +- Before writing, changing, or reviewing E2E tests, ALWAYS read and follow Playwright's official [Best Practices](https://playwright.dev/docs/best-practices), [Auto-waiting](https://playwright.dev/docs/actionability), and [Assertions](https://playwright.dev/docs/test-assertions) guides. +- Use the official [Locators](https://playwright.dev/docs/locators), [Network](https://playwright.dev/docs/network), and [Test Isolation](https://playwright.dev/docs/browser-contexts) guides when those concerns apply. + +## Test Hygiene + +- Test user-visible behavior with isolated, deterministic data and scoped, unique locators. +- Prefer role, label, text, and explicit test-contract locators. Do not use `.first()` or `.last()` merely to silence strictness errors. +- Use locator actions, Playwright auto-waiting, and web-first assertions for observable readiness and outcomes. +- NEVER use `waitForTimeout`, `setTimeout`, sleeps, animation-frame counts, or other wall-clock delays to synchronize a test. Wait for the specific UI state, request, response, event, or application outcome instead. +- Do not treat navigation, a network response, DOM attachment, or visibility alone as proof that asynchronously rendered UI is ready. Assert the state the next action actually requires. +- Register event and network waits before the action that triggers them. +- Do not retry state-changing actions. Retry idempotent readiness checks, then perform the action once and assert its outcome. +- Keep action and assertion timeouts adaptive. Do not use short timeouts as readiness probes or rely on retries to hide flakes. +- Assert exact outcomes and identities so stale state, duplicate rendering, and interactions with the wrong element cannot pass. diff --git a/packages/app/e2e/performance/timeline-stability/README.md b/packages/app/e2e/performance/timeline-stability/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3e9def275730d000c2914765eb796f85ae54f5fb --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/README.md @@ -0,0 +1,61 @@ +# Timeline Layout Continuity + +Run from `packages/app`: + +```sh +bun run test:stability +``` + +The suite runs a production build in one Chromium worker. Selected scenarios use deterministic 4x CPU stress after application readiness. This is a stress profile, not emulation of a specific device. + +## What It Proves + +The continuity probe samples DOM-derived layout and visibility state across browser render opportunities. Tests declare explicit contracts such as: + +- Preserve a visible semantic anchor while the user is away from the bottom. +- Preserve end anchoring while active content grows or new content appears. +- Keep adjacent visible rows ordered without material overlap. +- Keep user-selected disclosure state through updates and virtualization. +- Avoid a sampled blank interval while one visible surface replaces another. +- Preserve logical row and control identity where local state or focus depends on it. +- Keep keyboard, wheel, and nested-scroll ownership consistent during remeasurement. + +The suite exercises real browser reducer, projection, component, virtualizer, layout, focus, and interaction code. The backend and event producer are controlled fixtures. + +## What It Does Not Prove + +The pass/fail oracle does not inspect every compositor-presented pixel. A sample taken after `requestAnimationFrame` is a DOM/layout observation, not proof that every sampled state was displayed or that every displayed frame was sampled. + +The suite does not provide complete coverage for: + +- Compositor-only or raster-only glitches. +- Color, contrast, canvas, WebGL, masks, irregular clips, or arbitrary occlusion. +- Physical display refresh rates, native OS scaling, or a named low-end device. +- TCP packetization, proxy buffering, or the complete real server/provider pipeline. + +Playwright video, trace, screenshots, and observation JSON are diagnostic evidence. They are not pixel baselines and do not participate in normal pass/fail decisions. + +For optional before/violation/after screenshots, set `OPENCODE_STABILITY_CAPTURE=1`. Capture is opt-in because compositor readback can perturb timing. + +## Test Layers + +- **Projection:** admitted rows, grouping, labels, and final visible states. +- **Local state:** disclosure state, identity, duplicate delivery, and virtualization restoration. +- **Interaction:** wheel, keyboard, nested scrolling, actionability, and focus behavior. +- **Layout continuity:** anchoring, adjacency, responsive reflow, and visible surface handoffs. +- **Reducer hardening:** validly shaped but intentionally reordered, duplicated, removed, or replaced events. +- **Oracle contract:** pure analyzer and browser sampler calibration tests. + +Production-lifecycle fixtures should model states emitted by the current producer. Impossible or reordered sequences belong in reducer-hardening tests and must not be described as normal provider behavior. + +## Diagnostics + +Failures retain: + +- `video.webm` +- `trace.zip` +- failure screenshot +- sampled DOM/layout trace JSON +- event markers and summarized violations + +The analyzer records both unclipped layout bounds and ancestor-clipped visible intersections. Scrollbar and raw `scrollTop` changes alone do not fail continuity checks; user-visible semantic anchor movement does. diff --git a/packages/app/e2e/performance/timeline-stability/adverse.spec.ts b/packages/app/e2e/performance/timeline-stability/adverse.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ce1be9064598a6ea9fd5b869b65e12a5d0df761 --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/adverse.spec.ts @@ -0,0 +1,250 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + partUpdated, + setupTimeline, + shell, + textPart, + toolPart, + userMessage, + waitForVisualSettle, + type TimelineMessage, +} from "./fixture" + +test.describe("timeline adverse visual stability", () => { + test("does not pull a scrolled-away user while an active shell grows", async ({ page }, testInfo) => { + const activeShellID = "prt_adverse_01_shell" + const messages = [ + ...history(24), + userMessage(), + assistantMessage([shell(activeShellID, "running")], { completed: false }), + ] + const timeline = await setupTimeline(page, { + messages, + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + eventRetry: 30, + }) + const scroller = page.locator(".scroll-view__viewport", { + has: page.locator('[data-timeline-row="AssistantPart"]'), + }) + await scroller.evaluate((element) => { + element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -450 })) + element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 450) + }) + await page.waitForTimeout(150) + await expect + .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeGreaterThan(100) + const anchor = await scroller.evaluate((element) => { + const view = element.getBoundingClientRect() + return [...element.querySelectorAll("[data-timeline-key]")].find((row) => { + const rect = row.getBoundingClientRect() + return rect.top >= view.top + 40 && rect.bottom <= view.bottom - 40 + })?.dataset.timelineKey + }) + expect(anchor).toBeTruthy() + await waitForVisualSettle(page, [`[data-timeline-key="${anchor}"]`]) + + const regions = defineVisualRegions({ + anchor: { selector: `[data-timeline-key="${anchor}"]` }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(shell(activeShellID, "running", lines(1))), 180) + await timeline.send(partUpdated(shell(activeShellID, "running", lines(10))), 90) + await timeline.send(partUpdated(shell(activeShellID, "running", lines(50))), 350) + await timeline.send(partUpdated(shell(activeShellID, "completed", lines(50))), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "scrolled-away-shell", + trace, + visualPlan(regions, [ + { type: "required", regions: ["anchor"] }, + { type: "unique", regions: ["anchor"] }, + { type: "stable", regions: ["anchor"] }, + { type: "fixed", regions: ["anchor"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + ]), + ) + }) + + test("preserves an explicit shell state across virtualization", async ({ page }) => { + const targetID = "prt_virtual_shell" + const messages = [ + userMessage(undefined, { id: "msg_0000_virtual_user", created: 1700000000000 }), + assistantMessage([shell(targetID, "completed", lines(20))], { + id: "msg_0001_virtual_assistant", + parentID: "msg_0000_virtual_user", + created: 1700000001000, + }), + ...history(35, 10), + ] + await setupTimeline(page, { messages, settings: { shellToolPartsExpanded: false } }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + await scroller.evaluate((element) => { + element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -1_000 })) + element.scrollTop = 0 + }) + await page.waitForTimeout(300) + const trigger = page.locator(`[data-timeline-part-id="${targetID}"] [data-slot="collapsible-trigger"]`) + await expect(trigger).toBeVisible() + await trigger.click() + await expect(trigger).toHaveAttribute("aria-expanded", "true") + + await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight)) + await expect(page.locator(`[data-timeline-part-id="${targetID}"]`)).toHaveCount(0) + await scroller.evaluate((element) => (element.scrollTop = 0)) + await expect(trigger).toBeVisible() + await expect(trigger).toHaveAttribute("aria-expanded", "true") + }) + + test("keeps narrow viewport rows ordered during long shell growth", async ({ page }, testInfo) => { + const shellID = "prt_narrow_01_shell" + const followingID = "prt_narrow_02_following" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage( + [shell(shellID, "running"), textPart(followingID, "A narrow following row that wraps across lines.")], + { + completed: false, + }, + ), + ], + settings: { shellToolPartsExpanded: true }, + viewport: { width: 430, height: 800 }, + cpuRate: 4, + }) + await waitForVisualSettle(page, [ + `[data-timeline-part-id="${shellID}"]`, + `[data-timeline-part-id="${followingID}"]`, + ]) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: `[data-timeline-part-id="${followingID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(shell(shellID, "running", wideLines(10))), 100) + await timeline.send(partUpdated(shell(shellID, "running", wideLines(50))), 300) + await timeline.send(partUpdated(shell(shellID, "completed", wideLines(50))), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "narrow-shell", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["shell", "following"] }, + ], + { perMarker: true }, + ), + ) + }) + + test("keeps visible rows ordered while resizing desktop to narrow and back", async ({ page }, testInfo) => { + const shellID = "prt_resize_01_shell" + const contextIDs = ["prt_resize_02_read", "prt_resize_03_glob"] + const followingID = "prt_resize_04_following" + await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([ + shell(shellID, "completed", wideLines(15)), + toolPart(contextIDs[0]!, "read", "completed", { filePath: "src/a.ts" }), + toolPart(contextIDs[1]!, "glob", "completed", { path: ".", pattern: "**/*.ts" }), + textPart(followingID, "Following responsive timeline content that wraps on narrow screens."), + ]), + ], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + seedHistory: true, + }) + const group = `[data-timeline-part-ids="${contextIDs.join(",")}"]` + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: `[data-timeline-part-id="${followingID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + await page.setViewportSize({ width: 430, height: 800 }) + await page.waitForTimeout(500) + await page.setViewportSize({ width: 900, height: 800 }) + await page.waitForTimeout(500) + await page.setViewportSize({ width: 1400, height: 900 }) + await page.waitForTimeout(500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "responsive-resize", + trace, + visualPlan(regions, [ + { type: "required", regions: ["shell", "context", "following"] }, + { type: "unique", regions: ["shell", "context", "following"] }, + { type: "stable", regions: ["shell", "context", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 4, maxReversals: 4 }, + { type: "label-stability", regions: "all" }, + { type: "flow", regions: ["shell", "context", "following"] }, + ]), + ) + }) +}) + +function history(count: number, offset = 0): TimelineMessage[] { + return Array.from({ length: count }, (_, index) => { + const value = index + offset + const prefix = `msg_0${String(value).padStart(3, "0")}_history` + const userID = `${prefix}_a_user` + return [ + userMessage(undefined, { id: userID, created: 1699990000000 + value * 10_000 }), + assistantMessage( + [ + textPart( + `prt_history_${String(value).padStart(3, "0")}`, + `Historical response ${value}. ${"Stable history content. ".repeat(8)}`, + ), + ], + { + id: `${prefix}_b_assistant`, + parentID: userID, + created: 1699990001000 + value * 10_000, + }, + ), + ] + }).flat() +} + +function lines(count: number) { + return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n") +} + +function wideLines(count: number) { + return Array.from({ length: count }, (_, index) => `line ${index + 1} ${"wide-output-".repeat(20)}`).join("\n") +} diff --git a/packages/app/e2e/performance/timeline-stability/context-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/context-matrix.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3438b77c06bcb1a817043b16fb6c145eeb4775e --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/context-matrix.spec.ts @@ -0,0 +1,192 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantID, + assistantMessage, + event, + partUpdated, + setupTimeline, + textPart, + toolPart, + userMessage, + waitForVisualSettle, +} from "./fixture" + +const inputs = { + read: { filePath: "src/a.ts", offset: 0, limit: 120 }, + glob: { path: ".", pattern: "**/*.ts" }, + grep: { path: ".", pattern: "stable", include: "*.ts" }, + list: { path: "src" }, +} + +test("appends context operations while the group is expanded", async ({ page }, testInfo) => { + const firstID = "prt_append_01_read" + const followingID = "prt_append_99_following" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([toolPart(firstID, "read", "running", inputs.read), textPart(followingID, "Following append")], { + completed: false, + }), + ], + cpuRate: 4, + }) + const initialGroup = `[data-timeline-part-ids="${firstID}"]` + await page.locator(`${initialGroup} [data-slot="collapsible-trigger"]`).click() + await waitForVisualSettle(page, [initialGroup, `[data-timeline-part-id="${followingID}"]`]) + const regions = defineVisualRegions({ + context: { + selector: '[data-timeline-part-ids^="prt_append_01_read"]', + closest: '[data-timeline-row="AssistantPart"]', + }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(toolPart("prt_append_02_glob", "glob", "running", inputs.glob)), 180) + await timeline.send(partUpdated(toolPart("prt_append_03_grep", "grep", "completed", inputs.grep)), 240) + await timeline.send(partUpdated(toolPart("prt_append_04_list", "list", "completed", inputs.list)), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "context-append", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["context", "following"] }, + { type: "unique", regions: ["context", "following"] }, + { type: "stable", regions: ["context", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: ["following"], maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["context", "following"] }, + ], + { perMarker: true }, + ), + ) + await expect( + page.locator( + '[data-timeline-part-ids="prt_append_01_read,prt_append_02_glob,prt_append_03_grep,prt_append_04_list"]', + ), + ).toBeVisible() + await expect( + page.locator('[data-timeline-part-ids^="prt_append_01_read"] [data-slot="collapsible-trigger"]'), + ).toHaveAttribute("aria-expanded", "true") +}) + +test("splits and merges context groups when a middle text part changes", async ({ page }, testInfo) => { + const textID = "prt_split_02_text" + const followingID = "prt_split_99_following" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([ + toolPart("prt_split_01_read", "read", "completed", inputs.read), + textPart(textID, "Boundary"), + toolPart("prt_split_03_glob", "glob", "completed", inputs.glob), + textPart(followingID, "Following split groups"), + ]), + ], + cpuRate: 4, + }) + const regions = defineVisualRegions({ + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send( + event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: textID }), + 500, + ) + await expect(page.locator('[data-timeline-part-ids="prt_split_01_read,prt_split_03_glob"]')).toBeVisible() + await timeline.send(partUpdated(textPart(textID, "Boundary restored")), 500) + await expect(page.locator('[data-timeline-part-ids="prt_split_01_read"]')).toBeVisible() + await expect(page.locator('[data-timeline-part-ids="prt_split_03_glob"]')).toBeVisible() + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "context-split-merge", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["following"] }, + { type: "unique", regions: ["following"] }, + { type: "stable", regions: ["following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 1 }, + { type: "label-stability", regions: "all" }, + ], + { perMarker: true }, + ), + ) +}) + +test("removing the first context member replaces the group once without overlapping following content", async ({ + page, +}, testInfo) => { + const ids = ["prt_key_01_read", "prt_key_02_glob", "prt_key_03_grep"] + const followingID = "prt_key_99_following" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([ + toolPart(ids[0]!, "read", "completed", inputs.read), + toolPart(ids[1]!, "glob", "completed", inputs.glob), + toolPart(ids[2]!, "grep", "completed", inputs.grep), + textPart(followingID, "Following replaced group"), + ]), + ], + cpuRate: 4, + }) + const original = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`) + const originalRowKey = await original.evaluate((element) => + element.closest("[data-timeline-key]")?.getAttribute("data-timeline-key"), + ) + await original.locator('[data-slot="collapsible-trigger"]').click() + await expect(original.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") + const regions = defineVisualRegions({ + context: { + selector: '[data-timeline-part-ids*="prt_key_02_glob"]', + closest: '[data-timeline-row="AssistantPart"]', + }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send( + event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: ids[0] }), + 500, + ) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "context-first-remove", + trace, + visualPlan(regions, [ + { type: "required", regions: ["context", "following"] }, + { type: "unique", regions: ["context", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "flow", regions: ["context", "following"] }, + ]), + ) + await expect(page.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"]`)).toBeVisible() + expect( + await page + .locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"]`) + .evaluate((element) => element.closest("[data-timeline-key]")?.getAttribute("data-timeline-key")), + ).toBe(originalRowKey) + await expect( + page.locator(`[data-timeline-part-ids="${ids.slice(1).join(",")}"] [data-slot="collapsible-trigger"]`), + ).toHaveAttribute("aria-expanded", "true") +}) diff --git a/packages/app/e2e/performance/timeline-stability/environment-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/environment-matrix.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3455438a73658f9469db7f2f54cdecea26fc94cc --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/environment-matrix.spec.ts @@ -0,0 +1,113 @@ +import { test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + partUpdated, + setupTimeline, + shell, + textPart, + userMessage, + waitForVisualSettle, +} from "./fixture" + +// Fractional scaling exercises different browser rounding than the baseline. +for (const deviceScaleFactor of [1, 1.25]) { + test(`keeps shell growth ordered at device scale ${deviceScaleFactor}`, async ({ page }, testInfo) => { + const shellID = `prt_dpr_${String(deviceScaleFactor).replace(".", "_")}_01_shell` + const followingID = `prt_dpr_${String(deviceScaleFactor).replace(".", "_")}_02_following` + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([shell(shellID, "running"), textPart(followingID, "Following scaled shell")], { + completed: false, + }), + ], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + deviceScaleFactor, + seedHistory: true, + }) + await waitForVisualSettle(page, [ + `[data-timeline-part-id="${shellID}"]`, + `[data-timeline-part-id="${followingID}"]`, + ]) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: `[data-timeline-part-id="${followingID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(shell(shellID, "running", lines(20))), 180) + await timeline.send(partUpdated(shell(shellID, "completed", lines(20))), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability(testInfo, `dpr-${deviceScaleFactor}`, trace, shellPlan(regions)) + }) +} + +for (const reducedMotion of [true]) { + test(`keeps shell and status transitions ordered with reduced motion ${reducedMotion}`, async ({ + page, + }, testInfo) => { + const shellID = `prt_motion_${reducedMotion}_01_shell` + const followingID = `prt_motion_${reducedMotion}_02_following` + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([shell(shellID, "running"), textPart(followingID, "Following motion profile")], { + completed: false, + }), + ], + settings: { shellToolPartsExpanded: true }, + reducedMotion, + cpuRate: 4, + seedHistory: true, + }) + await waitForVisualSettle(page, [ + `[data-timeline-part-id="${shellID}"]`, + `[data-timeline-part-id="${followingID}"]`, + ]) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: `[data-timeline-part-id="${followingID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(shell(shellID, "completed", lines(10))), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability(testInfo, `reduced-motion-${reducedMotion}`, trace, shellPlan(regions)) + }) +} + +function lines(count: number) { + return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n") +} + +function shellPlan>( + regions: Regions & Record<"shell" | "following", { selector: string }>, +) { + return visualPlan( + regions, + [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["shell", "following"] }, + ], + { perMarker: true }, + ) +} diff --git a/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0f0d72233bdc20b470e8d7075c08ae66152469f --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts @@ -0,0 +1,121 @@ +import { test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + partUpdated, + setupTimeline, + textPart, + toolPart, + userMessage, + waitForVisualSettle, +} from "./fixture" + +const profiles = [ + { name: "edit", tool: "edit", input: { filePath: "src/edit.ts" } }, + { + name: "multi patch", + tool: "apply_patch", + input: { files: ["src/a.ts", "src/b.ts", "src/old.ts", "src/moved.ts"] }, + }, +] as const + +for (const profile of profiles) { + test(`stabilizes ${profile.name} pending to completed`, async ({ page }, testInfo) => { + const partID = `prt_file_matrix_${profiles.indexOf(profile)}` + const followingID = `prt_file_matrix_following_${profiles.indexOf(profile)}` + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage( + [ + toolPart(partID, profile.tool, "pending", profile.input), + textPart(followingID, `Following ${profile.name}`), + ], + { completed: false }, + ), + ], + settings: { editToolPartsExpanded: true }, + cpuRate: 4, + }) + await waitForVisualSettle(page, [`[data-timeline-part-id="${partID}"]`, `[data-timeline-part-id="${followingID}"]`]) + const regions = defineVisualRegions({ + tool: { selector: `[data-timeline-part-id="${partID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: `[data-timeline-part-id="${followingID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(toolPart(partID, profile.tool, "running", profile.input)), 180) + await timeline.send(partUpdated(completedPart(partID, profile)), 900) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + `file-${profile.name}`, + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["tool", "following"] }, + { type: "unique", regions: ["tool", "following"] }, + { type: "stable", regions: ["tool", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 1 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["tool", "following"] }, + ], + { perMarker: true }, + ), + ) + }) +} + +function completedPart(partID: string, profile: (typeof profiles)[number]) { + if (profile.tool === "edit") { + return toolPart(partID, profile.tool, "completed", profile.input, { + metadata: { + filediff: { + file: "src/edit.ts", + additions: 50, + deletions: 50, + before: source(50, false), + after: source(50, true), + }, + }, + }) + } + const files = [ + patchFile("src/a.ts", "update"), + patchFile("src/b.ts", "add"), + patchFile("src/old.ts", "delete"), + { ...patchFile("src/moved.ts", "move"), move: "src/new-place.ts" }, + ] + return toolPart(partID, profile.tool, "completed", profile.input, { metadata: { files } }) +} + +function patchFile(filePath: string, type: "add" | "update" | "delete" | "move") { + return { + filePath, + relativePath: filePath, + type, + additions: type === "delete" ? 0 : 20, + deletions: type === "add" ? 0 : 20, + before: type === "add" ? undefined : source(20, false), + after: type === "delete" ? undefined : source(20, true), + } +} + +function source(count: number, changed: boolean) { + return Array.from( + { length: count }, + (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`, + ).join("") +} diff --git a/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts b/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..798bf0df3b28dcbe06d62a3effac26f2a60eb420 --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts @@ -0,0 +1,113 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + partUpdated, + setupTimeline, + textPart, + toolPart, + userMessage, + waitForVisualSettle, +} from "./fixture" + +test("adds patch files incrementally without resetting outer expansion", async ({ page }, testInfo) => { + const patchID = "prt_incremental_01_patch" + const followingID = "prt_incremental_02_following" + const first = patchFile("src/a.ts", "update") + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage( + [ + toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }), + textPart(followingID, "Following incremental patch"), + ], + { completed: false }, + ), + ], + settings: { editToolPartsExpanded: true }, + cpuRate: 4, + seedHistory: true, + }) + const trigger = page.locator(`[data-timeline-part-id="${patchID}"] [data-slot="collapsible-trigger"]`).first() + await expect(trigger).toHaveAttribute("aria-expanded", "true") + await waitForVisualSettle(page, [`[data-timeline-part-id="${patchID}"]`, `[data-timeline-part-id="${followingID}"]`]) + const regions = defineVisualRegions({ + patch: { selector: `[data-timeline-part-id="${patchID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + const second = patchFile("src/b.ts", "add") + const third = patchFile("src/old.ts", "delete") + await timeline.send( + partUpdated( + toolPart( + patchID, + "apply_patch", + "running", + { files: [first.filePath, second.filePath] }, + { metadata: { files: [first, second] } }, + ), + ), + 240, + ) + await timeline.send( + partUpdated( + toolPart( + patchID, + "apply_patch", + "completed", + { files: [first.filePath, second.filePath, third.filePath] }, + { metadata: { files: [first, second, third] } }, + ), + ), + 800, + ) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "incremental-patch", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["patch", "following"] }, + { type: "unique", regions: ["patch", "following"] }, + { type: "stable", regions: ["patch", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: ["following"], maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["patch", "following"] }, + ], + { perMarker: true }, + ), + ) + await expect(trigger).toHaveAttribute("aria-expanded", "true") + await expect(page.locator('[data-scope="apply-patch"] [data-type="delete"]')).toBeVisible() +}) + +function patchFile(filePath: string, type: "add" | "update" | "delete") { + return { + filePath, + relativePath: filePath, + type, + additions: type === "delete" ? 0 : 4, + deletions: type === "add" ? 0 : 3, + before: type === "add" ? undefined : source(false), + after: type === "delete" ? undefined : source(true), + } +} + +function source(changed: boolean) { + return Array.from({ length: 12 }, (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`).join( + "", + ) +} diff --git a/packages/app/e2e/performance/timeline-stability/fixture.test.ts b/packages/app/e2e/performance/timeline-stability/fixture.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b003645e640ca5f5baf6b62190b2cd00d698480a --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/fixture.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from "bun:test" +import { + assistantMessage, + event, + toolPart, + userMessage, + validateTimelineEvent, + validateTimelineMessages, + type PartSeed, +} from "./fixture" + +describe("timeline fixture validation", () => { + test("accepts a valid timeline", () => { + expect(validateTimelineMessages([userMessage(), assistantMessage()])).toHaveLength(2) + }) + + test("rejects malformed SDK values at runtime", () => { + expect(() => + assistantMessage([], { + error: { name: "APIError", data: { message: "failed" } } as never, + }), + ).toThrow() + expect(() => + validateTimelineEvent({ + directory: "C:/OpenCode/TimelineStability", + payload: { + id: "evt_invalid_status", + type: "session.status", + properties: { sessionID: "ses_timeline_stability", status: { type: "retry", attempt: 1 } }, + }, + }), + ).toThrow() + }) + + test("rejects duplicate IDs and orphan assistants", () => { + expect(() => validateTimelineMessages([userMessage(), userMessage()])).toThrow(/duplicate message ID/) + expect(() => + validateTimelineMessages([userMessage(), assistantMessage([], { parentID: "msg_missing_parent" })]), + ).toThrow(/parent user/) + }) + + test("assigns deterministic event IDs", () => { + const first = event("session.status", { sessionID: "ses_timeline_stability", status: { type: "busy" } }) + const second = event("session.status", { sessionID: "ses_timeline_stability", status: { type: "idle" } }) + expect(first.payload.id).toMatch(/^evt_timeline_\d{4}$/) + expect(Number(second.payload.id.slice(-4))).toBe(Number(first.payload.id.slice(-4)) + 1) + }) +}) + +if (false) { + const userSeed = { id: "prt_type_user", type: "text", text: "typed" } satisfies PartSeed<"user"> + userMessage([userSeed]) + + // @ts-expect-error Tool completion fields are not valid while pending. + toolPart("prt_invalid_pending", "bash", "pending", {}, { output: "impossible" }) + // @ts-expect-error Tool completion fields are not valid while running. + toolPart("prt_invalid_running", "bash", "running", {}, { output: "impossible" }) + // @ts-expect-error Tool error fields are not valid after completion. + toolPart("prt_invalid_completed", "bash", "completed", {}, { error: "impossible" }) + + assistantMessage([ + // @ts-expect-error Agent references belong to user messages, not assistant messages. + { id: "prt_invalid_owner", type: "agent", name: "explore", source: { value: "@explore", start: 0, end: 8 } }, + ]) + + // @ts-expect-error Retry status events require message and next. + event("session.status", { sessionID: "ses_timeline_stability", status: { type: "retry", attempt: 1 } }) +} diff --git a/packages/app/e2e/performance/timeline-stability/fixture.ts b/packages/app/e2e/performance/timeline-stability/fixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..66edece4092e8b23d3a0e9b1c8222343f995b490 --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/fixture.ts @@ -0,0 +1,568 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { Event } from "@opencode-ai/schema/event" +import { SessionStatusEvent } from "@opencode-ai/schema/session-status-event" +import { SessionV1 } from "@opencode-ai/schema/session-v1" +import type { + AssistantMessage, + GlobalEvent, + Message, + Part, + Session, + SessionStatus, + ToolPart, + ToolState, + UserMessage, +} from "@opencode-ai/sdk/v2/client" +import { expect, type Page } from "@playwright/test" +import { Schema } from "effect" +import { mockOpenCodeServer } from "../../utils/mock-server" +import { installSseTransport } from "../../utils/sse-transport" +import { expectSessionTitle } from "../../utils/waits" + +export const directory = "C:/OpenCode/TimelineStability" +export const projectID = "proj_timeline_stability" +export const sessionID = "ses_timeline_stability" +export const userID = "msg_1000_timeline_user" +export const assistantID = "msg_1001_timeline_assistant" +export const title = "Timeline visual stability" +export const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } + +type TimelinePayload = Extract< + GlobalEvent["payload"], + { + type: + | "message.updated" + | "message.removed" + | "message.part.updated" + | "message.part.removed" + | "message.part.delta" + | "session.status" + } +> + +type DeepReadonly = Value extends readonly unknown[] + ? { readonly [Key in keyof Value]: DeepReadonly } + : Value extends object + ? { readonly [Key in keyof Value]: DeepReadonly } + : Value + +export type TimelineEvent = DeepReadonly & { payload: TimelinePayload }> +export type EventPayload = TimelineEvent +export type ToolStatus = ToolState["status"] +export type TimelineMessage = { info: UserMessage; parts: Part[] } | { info: AssistantMessage; parts: Part[] } + +type UserPart = Extract +type AssistantPart = Exclude +type OwnedPart = Owner extends "user" ? UserPart : AssistantPart +export type PartSeed = + OwnedPart extends infer Candidate + ? Candidate extends Part + ? Omit + : never + : never + +type ToolOptions = State extends "pending" + ? { output?: never; title?: never; metadata?: never; error?: never } + : State extends "running" + ? { title?: string; metadata?: Record; output?: never; error?: never } + : State extends "error" + ? { error?: string; metadata?: Record; output?: never; title?: never } + : { output?: string; title?: string; metadata?: Record; error?: never } + +const decodeOptions = { errors: "all", onExcessProperty: "error" } as const +const decodeMessage = Schema.decodeUnknownSync(SessionV1.WithParts) +const decodePart = Schema.decodeUnknownSync(SessionV1.Part) +const decodeStatus = Schema.decodeUnknownSync(SessionStatusEvent.Info) +const timelineEventSchema = Schema.Union([ + eventSchema("message.updated", SessionV1.Event.MessageUpdated.data), + eventSchema("message.removed", SessionV1.Event.MessageRemoved.data), + eventSchema("message.part.updated", SessionV1.Event.PartUpdated.data), + eventSchema("message.part.removed", SessionV1.Event.PartRemoved.data), + eventSchema("message.part.delta", SessionV1.Event.PartDelta.data), + eventSchema("session.status", SessionStatusEvent.Status.data), +]) +const decodeEvent = Schema.decodeUnknownSync(timelineEventSchema) +let eventSequence = 0 + +export async function setupTimeline( + page: Page, + input: { + messages?: TimelineMessage[] + settings?: Record + sessions?: Session[] + cpuRate?: number + viewport?: { width: number; height: number } + eventRetry?: number + reducedMotion?: boolean + locale?: string + deviceScaleFactor?: number + seedHistory?: boolean + protocol?: "v1" | "v2" + } = {}, +) { + const sessions = input.sessions ?? [session()] + const messages = validateTimelineMessages([ + ...(input.seedHistory ? historyMessages(18) : []), + ...(input.messages ?? [userMessage(), assistantMessage()]), + ]) + const active = messages.findLast((message) => message.info.role === "assistant") + const initialStatus = decodeStatus( + active?.info.role === "assistant" && active.info.time.completed === undefined ? { type: "busy" } : { type: "idle" }, + decodeOptions, + ) + const transport = await installSseTransport(page, { + server: `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`, + retry: input.eventRetry ?? 20, + }) + await mockOpenCodeServer(page, { + protocol: input.protocol, + directory, + project: project(), + provider: provider(), + sessions, + sessionStatus: { [sessionID]: initialStatus }, + pageMessages: () => ({ + items: messages, + }), + }) + await page.addInitScript((settings) => { + localStorage.setItem( + "settings.v3", + JSON.stringify({ + general: { + editToolPartsExpanded: false, + shellToolPartsExpanded: false, + showReasoningSummaries: false, + showSessionProgressBar: true, + ...settings, + }, + }), + ) + if (settings.newLayoutDesigns === false) { + localStorage.setItem("app-version.v1", JSON.stringify({ version: "1.17.20" })) + } + }, input.settings ?? {}) + if (input.locale) { + await page.addInitScript((locale) => { + localStorage.setItem("opencode.global.dat:language", JSON.stringify({ locale })) + }, input.locale) + } + if (input.reducedMotion) await page.emulateMedia({ reducedMotion: "reduce" }) + await page.setViewportSize(input.viewport ?? { width: 1400, height: 900 }) + if (input.deviceScaleFactor) { + const devtools = await page.context().newCDPSession(page) + const viewport = input.viewport ?? { width: 1400, height: 900 } + await devtools.send("Emulation.setDeviceMetricsOverride", { + width: viewport.width, + height: viewport.height, + deviceScaleFactor: input.deviceScaleFactor, + mobile: false, + }) + } + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await transport.waitForConnection() + await expectSessionTitle(page, title) + if (input.cpuRate && input.cpuRate > 1) { + const devtools = await page.context().newCDPSession(page) + await devtools.send("Emulation.setCPUThrottlingRate", { rate: input.cpuRate }) + } + + return { + transport, + async send(event: TimelineEvent, delay = 0) { + const valid = validateTimelineEvent(event) + await transport.send(valid, { marker: describeEvent(valid) }) + if (delay) await page.waitForTimeout(delay) + }, + async sendAll(sequence: { event: TimelineEvent; delay: number }[]) { + for (const item of sequence) { + const valid = validateTimelineEvent(item.event) + await transport.send(valid, { marker: describeEvent(valid) }) + await page.waitForTimeout(item.delay) + } + }, + async settle(frames = 3) { + await page.evaluate( + (frames) => + new Promise((resolve) => { + let remaining = frames + const tick = () => { + remaining-- + if (remaining <= 0) return resolve() + requestAnimationFrame(tick) + } + requestAnimationFrame(tick) + }), + frames, + ) + }, + async waitForPart(partID: string) { + const part = page.locator(`[data-timeline-part-id="${partID}"]`) + await expect(part).toHaveCount(1) + await expect(part).toBeVisible() + }, + } +} + +function describeEvent(event: EventPayload) { + if (event.payload.type === "message.part.updated") { + const part = event.payload.properties.part + return [ + event.payload.type, + part.id, + part.type === "tool" ? part.tool : part.type, + part.type === "tool" ? part.state.status : undefined, + ] + .filter(Boolean) + .join(":") + } + if (event.payload.type === "session.status") { + const status = event.payload.properties.status + return [event.payload.type, status.type, status.type === "retry" ? status.attempt : undefined] + .filter((value) => value !== undefined) + .join(":") + } + return event.payload.type +} + +export function event( + type: Type, + properties: Extract["properties"], +): TimelineEvent +export function event(type: TimelinePayload["type"], properties: TimelinePayload["properties"]): TimelineEvent { + return validateTimelineEvent({ + directory, + payload: { id: `evt_timeline_${String(++eventSequence).padStart(4, "0")}`, type, properties }, + }) +} + +export function validateTimelineEvent(input: unknown): TimelineEvent { + return decodeEvent(input, decodeOptions) +} + +export function validateTimelineMessages(input: readonly TimelineMessage[]): TimelineMessage[] { + input.forEach((message) => decodeMessage(message, decodeOptions)) + const messages = [...input] + const messageIDs = new Set() + const partIDs = new Set() + const users = new Set(messages.filter((message) => message.info.role === "user").map((message) => message.info.id)) + + messages.forEach((message) => { + if (messageIDs.has(message.info.id)) + throw new Error(`Timeline fixture has duplicate message ID: ${message.info.id}`) + messageIDs.add(message.info.id) + if (message.info.role === "assistant" && !users.has(message.info.parentID)) + throw new Error(`Timeline assistant ${message.info.id} must reference a parent user in the fixture`) + message.parts.forEach((part) => { + if (part.sessionID !== message.info.sessionID || part.messageID !== message.info.id) + throw new Error(`Timeline part ${part.id} ownership does not match message ${message.info.id}`) + if (message.info.role === "user" && !["text", "file", "agent", "subtask"].includes(part.type)) + throw new Error(`Timeline user message ${message.info.id} cannot own ${part.type} part ${part.id}`) + if (message.info.role === "assistant" && ["agent", "subtask"].includes(part.type)) + throw new Error(`Timeline assistant message ${message.info.id} cannot own ${part.type} part ${part.id}`) + if (partIDs.has(part.id)) throw new Error(`Timeline fixture has duplicate part ID: ${part.id}`) + partIDs.add(part.id) + }) + }) + return messages +} + +export async function waitForVisualSettle(page: Page, selectors: string[], stableFrames = 3) { + await page.waitForFunction( + ({ selectors, stableFrames }) => { + const elements = selectors.map((selector) => document.querySelector(selector)) + if (elements.some((element) => !element)) return false + return new Promise((resolve) => { + let stable = 0 + let previous = "" + const sample = () => { + const signature = JSON.stringify( + elements.map((element) => { + const rect = element!.getBoundingClientRect() + return [Math.round(rect.top * 10), Math.round(rect.bottom * 10), Math.round(rect.height * 10)] + }), + ) + stable = signature === previous ? stable + 1 : 0 + previous = signature + const ordered = elements + .slice(1) + .every( + (element, index) => + elements[index]!.getBoundingClientRect().bottom <= element!.getBoundingClientRect().top + 0.5, + ) + if (stable >= stableFrames && ordered) return resolve(true) + requestAnimationFrame(sample) + } + requestAnimationFrame(sample) + }) + }, + { selectors, stableFrames }, + ) +} + +export function historyMessages(count: number): TimelineMessage[] { + return Array.from({ length: count }, (_, index) => { + const value = String(index).padStart(4, "0") + const historyUserID = `msg_0${value}_history_a_user` + return [ + userMessage(undefined, { id: historyUserID, created: 1690000000000 + index * 10_000 }), + assistantMessage( + [ + { + id: `prt_0${value}_history_text`, + type: "text", + text: `Historical response ${index}. ${"Existing session content keeps the virtual timeline realistic. ".repeat(5)}`, + }, + ], + { + id: `msg_0${value}_history_b_assistant`, + parentID: historyUserID, + created: 1690000001000 + index * 10_000, + }, + ), + ] + }).flat() +} + +export function partUpdated(part: Part | PartSeed<"assistant">) { + const owned = "messageID" in part ? part : { ...part, sessionID, messageID: assistantID } + decodePart(owned, decodeOptions) + return event("message.part.updated", { + sessionID, + part: owned, + time: 1700000002000, + }) +} + +export function partDelta(partID: string, delta: string, messageID = assistantID) { + return event("message.part.delta", { sessionID, messageID, partID, field: "text", delta }) +} + +export function messageUpdated(info: Message) { + return event("message.updated", { sessionID, info }) +} + +export function status(type: SessionStatus["type"], attempt = 1) { + return event("session.status", { + sessionID, + status: type === "retry" ? { type, attempt, message: "Rate limited", next: 1700000010000 } : { type }, + }) +} + +export function userMessage( + parts?: PartSeed<"user">[], + input: { id?: string; summary?: UserMessage["summary"]; created?: number } = {}, +): Extract { + const id = input.id ?? userID + const seeds = parts ?? [userText("Build the timeline stability matrix.", { id: `prt_${id}_text` })] + const message = { + info: { + id, + sessionID, + role: "user", + time: { created: input.created ?? 1700000000000 }, + summary: input.summary ?? { diffs: [] }, + agent: "build", + model, + }, + parts: seeds.map((part) => ({ + ...part, + sessionID, + messageID: id, + })), + } satisfies Extract + decodeMessage(message, decodeOptions) + return message +} + +export function assistantMessage( + parts: PartSeed<"assistant">[] = [], + input: { + id?: string + parentID?: string + completed?: boolean + error?: AssistantMessage["error"] + created?: number + } = {}, +): Extract { + const id = input.id ?? assistantID + const message = { + info: { + id, + sessionID, + role: "assistant", + time: { + created: input.created ?? 1700000001000, + ...(input.completed === false ? {} : { completed: (input.created ?? 1700000001000) + 1_000 }), + }, + parentID: input.parentID ?? userID, + modelID: model.modelID, + providerID: model.providerID, + mode: "build", + agent: "build", + path: { cwd: directory, root: directory }, + cost: 0.01, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + variant: "max", + ...(input.error ? { error: input.error } : {}), + }, + parts: parts.map((part) => ({ ...part, sessionID, messageID: id })), + } satisfies Extract + decodeMessage(message, decodeOptions) + return message +} + +export function userText( + text: string, + input: Partial, { type: "text" }>, "type" | "text">> = {}, +): Extract, { type: "text" }> { + return { id: "prt_user_text", type: "text", text, ...input } +} + +export function textPart(id: string, text: string): Extract, { type: "text" }> { + return { id, type: "text", text } +} + +export function reasoningPart(id: string, text: string): Extract, { type: "reasoning" }> { + return { id, type: "reasoning", text, time: { start: 1700000001000 } } +} + +export function toolPart( + id: string, + tool: string, + state: "pending", + input: Record, + options?: ToolOptions<"pending">, +): Omit +export function toolPart( + id: string, + tool: string, + state: "running", + input: Record, + options?: ToolOptions<"running">, +): Omit +export function toolPart( + id: string, + tool: string, + state: "completed", + input: Record, + options?: ToolOptions<"completed">, +): Omit +export function toolPart( + id: string, + tool: string, + state: "error", + input: Record, + options?: ToolOptions<"error">, +): Omit +export function toolPart( + id: string, + tool: string, + state: ToolStatus, + input: Record, + options: ToolOptions = {}, +): Omit { + const base = { id, type: "tool" as const, callID: `call_${id}`, tool } + if (state === "pending") return { ...base, state: { status: state, input, raw: "" } } + if (state === "running") + return { + ...base, + state: { + status: state, + input, + title: options.title, + metadata: options.metadata ?? {}, + time: { start: 1700000001000 }, + }, + } + if (state === "error") + return { + ...base, + state: { + status: state, + input, + error: options.error ?? "Tool failed", + metadata: options.metadata ?? {}, + time: { start: 1700000001000, end: 1700000002000 }, + }, + } + return { + ...base, + state: { + status: state, + input, + output: options.output ?? "Completed", + title: options.title ?? tool, + metadata: options.metadata ?? {}, + time: { start: 1700000001000, end: 1700000002000 }, + }, + } +} + +export function shell( + id: string, + state: ToolStatus, + output = "", + command = `echo ${id}`, +): Omit { + if (state === "pending") return toolPart(id, "bash", state, { command }) + if (state === "running") + return toolPart(id, "bash", state, { command }, { title: command, metadata: { command, output } }) + if (state === "error") + return toolPart(id, "bash", state, { command }, { error: output || undefined, metadata: { command, output } }) + return toolPart(id, "bash", state, { command }, { title: command, output, metadata: { command, output } }) +} + +export function completedAssistantInfo(info: AssistantMessage): AssistantMessage { + return { ...info, time: { ...info.time, completed: 1700000003000 } } +} + +export function project() { + return { + id: projectID, + worktree: directory, + vcs: "git", + name: "timeline-stability", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + } +} + +export function session(input: Partial = {}): Session { + return { + id: sessionID, + slug: "timeline-stability", + projectID, + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + ...input, + } +} + +function eventSchema< + const Type extends TimelinePayload["type"], + const Properties extends Schema.Codec, +>(type: Type, properties: Properties) { + return Schema.Struct({ + directory: Schema.String, + project: Schema.optional(Schema.String), + workspace: Schema.optional(Schema.String), + payload: Schema.Struct({ id: Event.ID, type: Schema.Literal(type), properties }), + }) +} + +function provider() { + return { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "claude-opus-4-6" }, + } +} diff --git a/packages/app/e2e/performance/timeline-stability/interaction.spec.ts b/packages/app/e2e/performance/timeline-stability/interaction.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8cdf4fa8cfa803d40c8c89fd1ee09a7a9ac9a94e --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/interaction.spec.ts @@ -0,0 +1,242 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { assistantMessage, setupTimeline, shell, textPart, toolPart, userMessage, waitForVisualSettle } from "./fixture" + +test("expands and collapses a long completed shell without overlap", async ({ page }, testInfo) => { + const shellID = "prt_interaction_01_shell" + const followingID = "prt_interaction_02_following" + await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([shell(shellID, "completed", lines(50)), textPart(followingID, "Following shell expansion")]), + ], + settings: { shellToolPartsExpanded: false }, + cpuRate: 4, + seedHistory: true, + }) + const trigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`) + await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`]) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + const plan = visualPlan(regions, [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["shell", "following"] }, + ]) + await startVisualProbe(page, regions) + await trigger.click() + await expect(trigger).toHaveAttribute("aria-expanded", "true") + await page.waitForTimeout(500) + const expanded = await stopVisualProbe(page) + await reportVisualStability(testInfo, "shell-expand", expanded, plan) + + await startVisualProbe(page, regions) + await trigger.click() + await expect(trigger).toHaveAttribute("aria-expanded", "false") + await page.waitForTimeout(500) + const collapsed = await stopVisualProbe(page) + await reportVisualStability(testInfo, "shell-collapse", collapsed, plan) +}) + +test("expands and collapses a completed context group without overlap", async ({ page }, testInfo) => { + const ids = [ + "prt_interaction_01_read", + "prt_interaction_02_glob", + "prt_interaction_03_grep", + "prt_interaction_04_list", + ] + const group = `[data-timeline-part-ids="${ids.join(",")}"]` + const followingID = "prt_interaction_context_following" + await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([ + toolPart(ids[0]!, "read", "completed", { filePath: "src/a.ts" }), + toolPart(ids[1]!, "glob", "completed", { path: ".", pattern: "**/*.ts" }), + toolPart(ids[2]!, "grep", "completed", { path: ".", pattern: "stable" }), + toolPart(ids[3]!, "list", "completed", { path: "src" }), + textPart(followingID, "Following context expansion"), + ]), + ], + cpuRate: 4, + seedHistory: true, + }) + const trigger = page.locator(`${group} [data-slot="collapsible-trigger"]`) + await waitForVisualSettle(page, [group, `[data-timeline-part-id="${followingID}"]`]) + for (const [name, expanded] of [ + ["context-expand", true], + ["context-collapse", false], + ["context-reexpand", true], + ] as const) { + const regions = defineVisualRegions({ + context: { selector: group, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: `[data-timeline-part-id="${followingID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + await trigger.click() + await expect(trigger).toHaveAttribute("aria-expanded", String(expanded)) + await page.waitForTimeout(500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + name, + trace, + visualPlan(regions, [ + { type: "required", regions: ["context", "following"] }, + { type: "unique", regions: ["context", "following"] }, + { type: "stable", regions: ["context", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["context", "following"] }, + ]), + ) + } +}) + +test("expands and collapses an edit diff without moving twice", async ({ page }, testInfo) => { + const editID = "prt_interaction_edit" + const followingID = "prt_interaction_edit_following" + await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([ + toolPart( + editID, + "edit", + "completed", + { filePath: "src/edit.ts" }, + { + metadata: { + filediff: { + file: "src/edit.ts", + additions: 40, + deletions: 40, + before: source(40, false), + after: source(40, true), + }, + }, + }, + ), + textPart(followingID, "Following edit expansion"), + ]), + ], + settings: { editToolPartsExpanded: false }, + cpuRate: 4, + seedHistory: true, + }) + const trigger = page.locator(`[data-timeline-part-id="${editID}"] [data-slot="collapsible-trigger"]`).first() + await waitForVisualSettle(page, [`[data-timeline-part-id="${editID}"]`, `[data-timeline-part-id="${followingID}"]`]) + const regions = defineVisualRegions({ + edit: { selector: `[data-timeline-part-id="${editID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await trigger.click() + await expect(trigger).toHaveAttribute("aria-expanded", "true") + await page.waitForTimeout(900) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "edit-expand", + trace, + visualPlan(regions, [ + { type: "required", regions: ["edit", "following"] }, + { type: "unique", regions: ["edit", "following"] }, + { type: "stable", regions: ["edit", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 1 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["edit", "following"] }, + ]), + ) +}) + +test("shows all and expands historical diff summary without overlap", async ({ page }, testInfo) => { + const firstUser = userMessage(undefined, { + summary: { + diffs: Array.from({ length: 12 }, (_, index) => ({ + file: `src/diff-${index}.ts`, + additions: 1, + deletions: 1, + patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`, + })), + }, + }) + const nextUserID = "msg_2000_diff_interaction_user" + await setupTimeline(page, { + messages: [ + firstUser, + assistantMessage(), + userMessage(undefined, { id: nextUserID, created: 1700000010000 }), + assistantMessage([], { + id: "msg_2001_diff_interaction_assistant", + parentID: nextUserID, + created: 1700000011000, + }), + ], + cpuRate: 4, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + await scroller.evaluate((element) => (element.scrollTop = 0)) + const diff = page.locator('[data-timeline-row="DiffSummary"]') + const following = page.locator(`[data-message-id="${nextUserID}"]`).first() + await expect(diff).toBeVisible() + const regions = defineVisualRegions({ + diff: { selector: '[data-timeline-row="DiffSummary"]' }, + following: { selector: `[data-message-id="${nextUserID}"]` }, + }) + await startVisualProbe(page, regions) + await page.getByText(/show all/i).click() + await page.waitForTimeout(500) + await diff.locator('[data-slot="session-turn-diff-trigger"]').first().click() + await page.waitForTimeout(900) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "diff-summary-expand", + trace, + visualPlan(regions, [ + { type: "required", regions: ["diff", "following"] }, + { type: "unique", regions: ["diff", "following"] }, + { type: "stable", regions: ["diff", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 1, maxReversals: 2 }, + { type: "label-stability", regions: "all" }, + { type: "flow", regions: ["diff", "following"] }, + ]), + ) +}) + +function lines(count: number) { + return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n") +} + +function source(count: number, changed: boolean) { + return Array.from( + { length: count }, + (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`, + ).join("") +} diff --git a/packages/app/e2e/performance/timeline-stability/lifecycle.spec.ts b/packages/app/e2e/performance/timeline-stability/lifecycle.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..40688429a33ec226f540ffbde40ea83a6461367e --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/lifecycle.spec.ts @@ -0,0 +1,157 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + mapVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + completedAssistantInfo, + messageUpdated, + partDelta, + partUpdated, + reasoningPart, + setupTimeline, + shell, + status, + textPart, + userMessage, + waitForVisualSettle, +} from "./fixture" + +test.describe("timeline visual lifecycle stability", () => { + test("streams empty, short, and long parallel shells to staggered completion", async ({ page }, testInfo) => { + test.setTimeout(180_000) + const ids = ["prt_parallel_01_empty", "prt_parallel_02_short", "prt_parallel_03_long"] as const + const initial = ids.map((id) => shell(id, "running")) + const followingID = "prt_parallel_04_following" + const assistant = assistantMessage([...initial, textPart(followingID, "Following all parallel shells.")], { + completed: false, + }) + const timeline = await setupTimeline(page, { + messages: [userMessage(), assistant], + settings: { shellToolPartsExpanded: true, showReasoningSummaries: true }, + cpuRate: 4, + eventRetry: 24, + seedHistory: true, + }) + await timeline.send(status("busy"), 150) + for (const id of ids) await timeline.waitForPart(id) + const scroller = page.locator(".scroll-view__viewport", { + has: page.locator('[data-timeline-row="AssistantPart"]'), + }) + await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight)) + const regions = defineVisualRegions({ + prt_shell_empty: shellRegion(ids[0]), + prt_shell_short: shellRegion(ids[1]), + prt_shell_long: shellRegion(ids[2]), + following: shellRegion(followingID), + }) + await waitForVisualSettle(page, [`[data-timeline-part-id="${followingID}"]`]) + await startVisualProbe(page, regions) + await timeline.sendAll([ + { event: partUpdated(shell(ids[0]!, "completed", "")), delay: 180 }, + { event: partUpdated(shell(ids[2]!, "running", lines(10))), delay: 70 }, + { event: partUpdated(shell(ids[1]!, "running", lines(2))), delay: 110 }, + { event: partUpdated(shell(ids[2]!, "running", lines(25))), delay: 80 }, + { event: partUpdated(shell(ids[1]!, "completed", lines(2))), delay: 260 }, + { event: partUpdated(shell(ids[2]!, "running", lines(50))), delay: 100 }, + { event: partUpdated(shell(ids[2]!, "completed", lines(50))), delay: 450 }, + { event: messageUpdated(completedAssistantInfo(assistant.info)), delay: 100 }, + { event: status("idle"), delay: 700 }, + ]) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "parallel-shells", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] }, + { type: "unique", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long"] }, + { type: "stable", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 4 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["prt_shell_empty", "prt_shell_short", "prt_shell_long", "following"] }, + ], + { perMarker: true }, + ), + ) + await expect(page.locator(`[data-timeline-part-id="${ids[2]}"] [data-slot="bash-pre"]`)).toContainText("line 50") + + const short = page.locator(`[data-timeline-part-id="${ids[1]}"]`) + await short.locator('[data-slot="collapsible-trigger"]').click() + await expect(short.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false") + await timeline.send(partUpdated(textPart("prt_late_sibling", "A later sibling rerender.")), 250) + await expect(short.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false") + }) + + test("replaces thinking with streamed reasoning and text without a blank visible turn", async ({ + page, + }, testInfo) => { + const reasoningID = "prt_reasoning_visible" + const textID = "prt_streamed_text" + const assistant = assistantMessage([], { completed: false }) + const timeline = await setupTimeline(page, { + messages: [userMessage(), assistant], + settings: { showReasoningSummaries: true }, + cpuRate: 4, + }) + await timeline.send(status("busy"), 120) + await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible() + + const regions = defineVisualRegions({ + thinking: { selector: '[data-timeline-row="Thinking"]' }, + reasoning: { + selector: `[data-timeline-part-id="${reasoningID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + text: { selector: `[data-timeline-part-id="${textID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(reasoningPart(reasoningID, "")), 100) + await expect(page.locator(`[data-timeline-part-id="${reasoningID}"]`)).toHaveCount(0) + await timeline.send(partUpdated(reasoningPart(reasoningID, "## Planning\n\nChecking the visible timeline.")), 160) + await timeline.waitForPart(reasoningID) + await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0) + await timeline.send(partUpdated(textPart(textID, "Starting")), 100) + await timeline.send(partDelta(textID, " **stable"), 90) + await timeline.send(partDelta(textID, " output** with `code` and [a link"), 130) + await timeline.send(partDelta(textID, "](https://example.com)."), 220) + await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 120) + await timeline.send(status("idle"), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "reasoning-text-handoff", + trace, + visualPlan(regions, [ + { type: "required", regions: ["reasoning", "text"] }, + { type: "continuous-any", regions: ["thinking", "reasoning", "text"] }, + { type: "unique", regions: ["reasoning", "text"] }, + { type: "stable", regions: ["reasoning", "text"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxReversals: 4 }, + { type: "label-stability", regions: "all" }, + { type: "flow", regions: ["reasoning", "text"] }, + ]), + ) + await expect(page.locator(`[data-timeline-part-id="${textID}"]`)).toContainText("stable output") + }) +}) + +function lines(count: number) { + return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n") +} + +function shellRegion(id: string) { + return { selector: `[data-timeline-part-id="${id}"]`, closest: '[data-timeline-row="AssistantPart"]' } +} diff --git a/packages/app/e2e/performance/timeline-stability/oracle-browser.spec.ts b/packages/app/e2e/performance/timeline-stability/oracle-browser.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7891eba5bd97a7c246a8e178ebc7677a91982360 --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/oracle-browser.spec.ts @@ -0,0 +1,75 @@ +import { expect, test } from "@playwright/test" +import { + analyzeVisualObservations, + defineVisualRegions, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { assistantMessage, setupTimeline, textPart, userMessage } from "./fixture" + +test("detects blanking caused by ancestor opacity", async ({ page }) => { + const partID = "prt_oracle_ancestor_opacity" + await setupTimeline(page, { messages: [userMessage(), assistantMessage([textPart(partID, "Visible content")])] }) + const row = page.locator(`[data-timeline-part-id="${partID}"]`).first() + const regions = defineVisualRegions({ + content: { selector: `[data-timeline-part-id="${partID}"]` }, + }) + await startVisualProbe(page, regions) + await row.evaluate((element) => { + element.parentElement!.style.opacity = "0" + }) + await page.waitForTimeout(50) + await row.evaluate((element) => { + element.parentElement!.style.opacity = "1" + }) + await page.waitForTimeout(50) + const trace = await stopVisualProbe(page) + const issues = analyzeVisualObservations( + trace.samples, + visualPlan(regions, [ + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all" }, + { type: "label-stability", regions: "all" }, + ]), + ) + + expect(issues.some((issue) => issue.includes("blanked between visible frames"))).toBe(true) +}) + +test("detects root opacity when probing descendant opacity", async ({ page }) => { + const partID = "prt_oracle_descendant_opacity" + await setupTimeline(page, { messages: [userMessage(), assistantMessage([textPart(partID, "Visible content")])] }) + const row = page.locator(`[data-timeline-part-id="${partID}"]`).first() + await row.evaluate((element) => { + element.innerHTML = 'Visible content' + }) + const regions = defineVisualRegions({ + content: { + selector: `[data-timeline-part-id="${partID}"]`, + opacitySelectors: ['[data-probe-opacity="true"]'], + }, + }) + await startVisualProbe(page, regions) + await row.evaluate((element) => { + ;(element as HTMLElement).style.opacity = "0" + }) + await page.waitForTimeout(50) + await row.evaluate((element) => { + ;(element as HTMLElement).style.opacity = "1" + }) + await page.waitForTimeout(50) + const trace = await stopVisualProbe(page) + const issues = analyzeVisualObservations( + trace.samples, + visualPlan(regions, [ + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all" }, + { type: "label-stability", regions: "all" }, + ]), + ) + + expect(issues.some((issue) => issue.includes("blanked between visible frames"))).toBe(true) +}) diff --git a/packages/app/e2e/performance/timeline-stability/playwright.config.ts b/packages/app/e2e/performance/timeline-stability/playwright.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..cfa4d926132dea3d468ee095248990335d4e7eea --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/playwright.config.ts @@ -0,0 +1,17 @@ +import config from "../playwright.config" + +export default { + ...config, + testDir: ".", + testMatch: "**/*.spec.ts", + outputDir: "../../test-results/timeline-stability", + reporter: [["html", { outputFolder: "../../playwright-report/timeline-stability", open: "never" }], ["line"]], + retries: 0, + workers: 1, + use: { + ...config.use, + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, +} diff --git a/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts b/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e67545104d48ddb9e31615db6c3133518d9a9083 --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts @@ -0,0 +1,353 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + partUpdated, + setupTimeline, + shell, + textPart, + userMessage, + type TimelineMessage, +} from "./fixture" + +test("does not reverse visible rows when the user wheels during shell remeasurement", async ({ page }, testInfo) => { + const shellID = "prt_wheel_01_shell" + const followingID = "prt_wheel_02_following" + const timeline = await setupTimeline(page, { + messages: [ + ...history(12), + userMessage(), + assistantMessage([shell(shellID, "running"), textPart(followingID, "Following wheel interaction")], { + completed: false, + }), + ], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + reducedMotion: true, + seedHistory: true, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(shell(shellID, "running", lines(30))), 80) + await scroller.evaluate((element) => + element.dispatchEvent(new WheelEvent("wheel", { bubbles: true, cancelable: true, deltaY: -180 })), + ) + await scroller.evaluate((element) => (element.scrollTop -= 180)) + await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 250) + const trace = await stopVisualProbe(page) + await reportVisualStability(testInfo, "wheel-during-resize", trace, rowPairPlan(regions, 1)) +}) + +test("keeps moving upward while drag-selecting above the timeline", async ({ page }) => { + await setupTimeline(page, { + messages: history(80), + viewport: { width: 1400, height: 700 }, + reducedMotion: true, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + const text = page.getByText("History 79.", { exact: false }) + await expect(text).toBeVisible() + await scroller.evaluate((element) => { + element.dataset.selectionLength = "0" + document.addEventListener("selectionchange", () => { + element.dataset.selectionLength = String( + Math.max(Number(element.dataset.selectionLength), window.getSelection()?.toString().length ?? 0), + ) + }) + }) + const textBox = await text.boundingBox() + const scrollBox = await scroller.boundingBox() + expect(textBox).not.toBeNull() + expect(scrollBox).not.toBeNull() + if (!textBox || !scrollBox) return + + await page.mouse.move(textBox.x + textBox.width - 10, textBox.y + textBox.height / 2) + await page.mouse.down() + await page.mouse.move(textBox.x + 20, scrollBox.y - 120, { steps: 30 }) + + await expect.poll(() => scroller.evaluate((element) => Number(element.dataset.selectionLength))).toBeGreaterThan(0) + await expect + .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeGreaterThan(500) + await page.mouse.up() +}) + +test("does not pull a keyboard-scrolled user during shell remeasurement", async ({ page }, testInfo) => { + const shellID = "prt_keyboard_01_shell" + const followingID = "prt_keyboard_02_following" + const timeline = await setupTimeline(page, { + messages: [ + ...history(12), + userMessage(), + assistantMessage([shell(shellID, "running"), textPart(followingID, "Following keyboard interaction")], { + completed: false, + }), + ], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + await scroller.focus() + for (let index = 0; index < 3; index++) { + await scroller.press("PageUp") + await page.waitForTimeout(250) + } + await expect + .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop), { + timeout: 20_000, + }) + .toBeGreaterThan(80) + await page.waitForFunction(() => { + const root = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector("[data-timeline-row]"), + ) + if (!root) return false + return new Promise((resolve) => { + const top = root.scrollTop + requestAnimationFrame(() => requestAnimationFrame(() => resolve(Math.abs(root.scrollTop - top) <= 0.5))) + }) + }) + const anchor = await scroller.evaluate((element) => { + const view = element.getBoundingClientRect() + return [...element.querySelectorAll("[data-timeline-key]")].find((row) => { + const rect = row.getBoundingClientRect() + return rect.top >= view.top + 40 && rect.bottom <= view.bottom - 40 + })?.dataset.timelineKey + }) + expect(anchor).toBeTruthy() + const regions = defineVisualRegions({ + anchor: { selector: `[data-timeline-key="${anchor}"]` }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 400) + const trace = await stopVisualProbe(page) + await reportVisualStability(testInfo, "keyboard-during-resize", trace, anchorPlan(regions)) +}) + +test("tracks keyboard scrolling from a focused timeline descendant", async ({ page }, testInfo) => { + const shellID = "prt_descendant_keyboard_01_shell" + const timeline = await setupTimeline(page, { + messages: [...history(12), userMessage(), assistantMessage([shell(shellID, "completed", lines(5))])], + settings: { shellToolPartsExpanded: false }, + cpuRate: 4, + reducedMotion: true, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + const row = page.locator(`[data-timeline-part-id="${shellID}"]`).first() + const trigger = page.locator(`[data-timeline-part-id="${shellID}"] [data-slot="collapsible-trigger"]`) + await row.evaluate((element) => element.setAttribute("tabindex", "0")) + await row.focus() + for (let index = 0; index < 3; index++) { + await row.press("PageUp") + await page.waitForTimeout(250) + } + await expect + .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeGreaterThan(5) + const anchor = await scroller.evaluate((element) => { + const view = element.getBoundingClientRect() + return [...element.querySelectorAll("[data-timeline-key]")].find((row) => { + const rect = row.getBoundingClientRect() + return rect.top >= view.top + 40 && rect.bottom <= view.bottom - 40 + })?.dataset.timelineKey + }) + expect(anchor).toBeTruthy() + const regions = defineVisualRegions({ + anchor: { selector: `[data-timeline-key="${anchor}"]` }, + }) + await startVisualProbe(page, regions) + await trigger.click() + await page.waitForTimeout(300) + const trace = await stopVisualProbe(page) + await reportVisualStability(testInfo, "descendant-keyboard-resize", trace, anchorPlan(regions)) +}) + +test("does not claim keyboard scrolling owned by a nested scrollable", async ({ page }) => { + const shellID = "prt_nested_keyboard_shell" + await setupTimeline(page, { + messages: [userMessage(), assistantMessage([shell(shellID, "completed", lines(50))])], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + reducedMotion: true, + seedHistory: true, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + const nested = page.locator(`[data-timeline-part-id="${shellID}"] [data-scrollable]`) + await nested.evaluate((element) => (element.scrollTop = element.scrollHeight)) + await nested.focus() + await page.waitForFunction(() => { + const root = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector("[data-timeline-row]"), + ) + if (!root) return false + return new Promise((resolve) => { + const top = root.scrollTop + requestAnimationFrame(() => requestAnimationFrame(() => resolve(Math.abs(root.scrollTop - top) <= 0.5))) + }) + }) + const before = await scroller.evaluate((element) => element.scrollTop) + const nestedBefore = await nested.evaluate((element) => element.scrollTop) + await nested.press("PageUp") + await page.waitForTimeout(300) + expect(await scroller.evaluate((element) => element.scrollTop)).toBe(before) + expect(await nested.evaluate((element) => element.scrollTop)).toBeLessThan(nestedBefore) + + await nested.evaluate((element) => (element.scrollTop = 0)) + await scroller.evaluate((element) => (element.scrollTop = Math.min(300, element.scrollHeight - element.clientHeight))) + const boundaryBefore = await scroller.evaluate((element) => element.scrollTop) + expect(boundaryBefore).toBeGreaterThan(0) + await nested.press("PageUp") + await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeLessThan(boundaryBefore) + + const nonOverflowing = page.locator(`[data-timeline-part-id="${shellID}"]`).first() + await nonOverflowing.evaluate((element) => { + element.setAttribute("data-scrollable", "") + element.setAttribute("tabindex", "0") + }) + await nonOverflowing.focus() + const nonOverflowBefore = await scroller.evaluate((element) => element.scrollTop) + await nonOverflowing.press("PageUp") + await expect.poll(() => scroller.evaluate((element) => element.scrollTop)).toBeLessThan(nonOverflowBefore) +}) + +test("jump to latest lands on stable final rows after offscreen growth", async ({ page }, testInfo) => { + const shellID = "prt_jump_01_shell" + const followingID = "prt_jump_02_following" + const timeline = await setupTimeline(page, { + messages: [ + ...history(20), + userMessage(), + assistantMessage([shell(shellID, "running"), textPart(followingID, "Latest visible row")], { completed: false }), + ], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + await scroller.evaluate( + (element) => (element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight - 600)), + ) + await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 300) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await page.getByRole("button", { name: /Jump to latest/i }).click() + await expect(page.locator(`[data-timeline-part-id="${followingID}"]`)).toBeVisible() + await page.waitForTimeout(600) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "jump-latest", + trace, + visualPlan(regions, [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 1 }, + { type: "label-stability", regions: "all" }, + { type: "acquire-bottom-anchor" }, + { type: "flow", regions: ["shell", "following"] }, + ]), + ) +}) + +test("handles a single row taller than the viewport", async ({ page }, testInfo) => { + const shellID = "prt_tall_01_shell" + const followingID = "prt_tall_02_following" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([shell(shellID, "running"), textPart(followingID, "After tall row")], { completed: false }), + ], + settings: { shellToolPartsExpanded: true }, + viewport: { width: 900, height: 360 }, + cpuRate: 4, + seedHistory: true, + }) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(shell(shellID, "completed", lines(100))), 700) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "taller-than-viewport", + trace, + visualPlan(regions, [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["shell", "following"] }, + ]), + ) +}) + +function history(count: number): TimelineMessage[] { + return Array.from({ length: count }, (_, index) => { + const prefix = `msg_${String(index).padStart(4, "0")}_scroll` + const userID = `${prefix}_a_user` + return [ + userMessage(undefined, { id: userID, created: 1690000000000 + index * 10_000 }), + assistantMessage( + [textPart(`prt_${String(index).padStart(4, "0")}_scroll`, `History ${index}. ${"content ".repeat(30)}`)], + { + id: `${prefix}_b_assistant`, + parentID: userID, + created: 1690000001000 + index * 10_000, + }, + ), + ] + }).flat() +} + +function lines(count: number) { + return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n") +} + +function rowPairPlan( + regions: Record<"shell" | "following", { selector: string; closest?: string }>, + maxPositionReversals: number, +) { + return visualPlan(regions, [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals }, + { type: "label-stability", regions: "all" }, + { type: "flow", regions: ["shell", "following"] }, + ]) +} + +function anchorPlan(regions: Record<"anchor", { selector: string; closest?: string }>) { + return visualPlan(regions, [ + { type: "required", regions: ["anchor"] }, + { type: "unique", regions: ["anchor"] }, + { type: "stable", regions: ["anchor"] }, + { type: "fixed", regions: ["anchor"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + ]) +} diff --git a/packages/app/e2e/performance/timeline-stability/shell-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/shell-matrix.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bab1ca23b4f998702c6b684f023d62e9998ccc26 --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/shell-matrix.spec.ts @@ -0,0 +1,255 @@ +import { test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + partUpdated, + setupTimeline, + shell, + textPart, + userMessage, + waitForVisualSettle, +} from "./fixture" + +const profiles = [ + { + name: "empty running to completed", + updates: [{ state: "completed" as const, output: "", delay: 350 }], + }, + { + name: "50 lines arriving incrementally", + updates: [ + { state: "running" as const, output: lines(1), delay: 100 }, + { state: "running" as const, output: lines(10), delay: 160 }, + { state: "running" as const, output: lines(25), delay: 90 }, + { state: "running" as const, output: lines(50), delay: 220 }, + { state: "completed" as const, output: lines(50), delay: 500 }, + ], + }, + { + name: "wide ANSI and CRLF output", + updates: [ + { + state: "running" as const, + output: Array.from({ length: 20 }, (_, index) => `\u001b[32mline ${index}\u001b[0m ${"wide-".repeat(30)}`).join( + "\r\n", + ), + delay: 240, + }, + { + state: "completed" as const, + output: Array.from({ length: 20 }, (_, index) => `line ${index} ${"wide-".repeat(30)}`).join("\n"), + delay: 500, + }, + ], + }, +] as const + +for (const profile of profiles) { + test(`keeps rows stable for shell ${profile.name}`, async ({ page }, testInfo) => { + const shellID = `prt_matrix_${profiles.indexOf(profile)}_01_shell` + const followingID = `prt_matrix_${profiles.indexOf(profile)}_02_following` + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([shell(shellID, "running"), textPart(followingID, "Following shell row")], { + completed: false, + }), + ], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + seedHistory: true, + }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + await scroller.evaluate((element) => (element.scrollTop = element.scrollHeight)) + await waitForVisualSettle(page, [ + `[data-timeline-part-id="${shellID}"]`, + `[data-timeline-part-id="${followingID}"]`, + ]) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: `[data-timeline-part-id="${followingID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + for (const update of profile.updates) { + await timeline.send(partUpdated(shell(shellID, update.state, update.output)), update.delay) + } + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + `shell-${profiles.indexOf(profile)}`, + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0, maxReversals: 1 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["shell", "following"] }, + ], + { perMarker: true }, + ), + ) + }) +} + +test("keeps following row stable when a collapsed shell receives 50 lines", async ({ page }, testInfo) => { + const shellID = "prt_matrix_collapsed_01_shell" + const followingID = "prt_matrix_collapsed_02_following" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([shell(shellID, "running"), textPart(followingID, "Following collapsed shell")], { + completed: false, + }), + ], + settings: { shellToolPartsExpanded: false }, + cpuRate: 4, + seedHistory: true, + }) + await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`]) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(shell(shellID, "running", lines(50))), 240) + await timeline.send(partUpdated(shell(shellID, "completed", lines(50))), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "collapsed-shell", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: ["following"], maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "flow", regions: ["shell", "following"] }, + ], + { perMarker: true }, + ), + ) +}) + +test("keeps rows stable when a running shell becomes an error", async ({ page }, testInfo) => { + const shellID = "prt_matrix_error_01_shell" + const followingID = "prt_matrix_error_02_following" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([shell(shellID, "running", lines(10)), textPart(followingID, "Following failed shell")], { + completed: false, + }), + ], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + seedHistory: true, + }) + await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`, `[data-timeline-part-id="${followingID}"]`]) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send( + partUpdated({ + ...shell(shellID, "error"), + state: { + status: "error", + input: { command: `echo ${shellID}` }, + error: "Command failed after output", + metadata: {}, + time: { start: 1700000001000, end: 1700000002000 }, + }, + }), + 500, + ) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "shell-error", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: ["following"], maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["shell", "following"] }, + ], + { perMarker: true }, + ), + ) +}) + +test("keeps rows stable when later text arrives before shell output", async ({ page }, testInfo) => { + const shellID = "prt_late_text_01_shell" + const followingID = "prt_late_text_02_following" + const timeline = await setupTimeline(page, { + messages: [userMessage(), assistantMessage([shell(shellID, "running")], { completed: false })], + settings: { shellToolPartsExpanded: true }, + cpuRate: 4, + seedHistory: true, + }) + await waitForVisualSettle(page, [`[data-timeline-part-id="${shellID}"]`]) + const regions = defineVisualRegions({ + shell: { selector: `[data-timeline-part-id="${shellID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: `[data-timeline-part-id="${followingID}"]`, + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(textPart(followingID, "Later assistant content arrived before shell output.")), 240) + await timeline.send(partUpdated(shell(shellID, "running", lines(20))), 300) + await timeline.send(partUpdated(shell(shellID, "completed", lines(20))), 600) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "late-text-before-shell-output", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["shell", "following"] }, + { type: "unique", regions: ["shell", "following"] }, + { type: "stable", regions: ["shell"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["shell", "following"] }, + ], + { perMarker: true }, + ), + ) +}) + +function lines(count: number) { + return Array.from({ length: count }, (_, index) => `line ${index + 1}`).join("\n") +} diff --git a/packages/app/e2e/performance/timeline-stability/tool-mutation.spec.ts b/packages/app/e2e/performance/timeline-stability/tool-mutation.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..03690ec9214798ca2b06a41800e85adc5a381f97 --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/tool-mutation.spec.ts @@ -0,0 +1,106 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + partUpdated, + session, + sessionID, + setupTimeline, + textPart, + toolPart, + userMessage, +} from "./fixture" + +test("adds a task child-session link without replacing the task row", async ({ page }, testInfo) => { + const taskID = "prt_task_link" + const childID = "ses_task_child" + const input = { description: "Inspect child", subagent_type: "explore" } + const timeline = await setupTimeline(page, { + messages: [userMessage(), assistantMessage([toolPart(taskID, "task", "running", input)], { completed: false })], + sessions: [session(), session({ id: childID, parentID: sessionID, title: "Inspect child" })], + cpuRate: 4, + }) + const regions = defineVisualRegions({ + task: { selector: `[data-timeline-part-id="${taskID}"] [data-slot="collapsible-trigger"]` }, + }) + await startVisualProbe(page, regions) + await timeline.send( + partUpdated(toolPart(taskID, "task", "completed", input, { metadata: { sessionId: childID } })), + 500, + ) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "task-link", + trace, + visualPlan(regions, [ + { type: "required", regions: ["task"] }, + { type: "unique", regions: ["task"] }, + { type: "stable", regions: ["task"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + ]), + ) + await expect( + page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }), + ).toBeVisible() +}) + +test("changes generic tool arguments without replacing the row", async ({ page }, testInfo) => { + const toolID = "prt_generic_mutation" + const followingID = "prt_generic_mutation_following" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage( + [ + toolPart(toolID, "mcp_probe", "running", { target: "one", count: 1 }), + textPart(followingID, "Following generic tool"), + ], + { completed: false }, + ), + ], + cpuRate: 4, + }) + const regions = defineVisualRegions({ + tool: { selector: `[data-timeline-part-id="${toolID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send( + partUpdated(toolPart(toolID, "mcp_probe", "running", { target: "two", count: 2, mode: "deep" })), + 200, + ) + await timeline.send( + partUpdated(toolPart(toolID, "mcp_probe", "completed", { target: "two", count: 2, mode: "deep" })), + 400, + ) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "generic-mutation", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["tool", "following"] }, + { type: "unique", regions: ["tool", "following"] }, + { type: "stable", regions: ["tool", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + { type: "flow", regions: ["tool", "following"] }, + ], + { perMarker: true }, + ), + ) +}) diff --git a/packages/app/e2e/performance/timeline-stability/tools.spec.ts b/packages/app/e2e/performance/timeline-stability/tools.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d28fdaa65f5838b673b928b1adfbddb0a271e721 --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/tools.spec.ts @@ -0,0 +1,198 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantMessage, + directory, + partUpdated, + session, + sessionID, + setupTimeline, + status, + textPart, + toolPart, + userMessage, +} from "./fixture" + +test.describe("timeline tool state stability", () => { + test("moves lightweight tools through pending, running, and completed without replacing rows", async ({ + page, + }, testInfo) => { + const ids = ["webfetch", "websearch", "task", "skill", "custom"] as const + const inputs = { + webfetch: { url: "https://example.com/docs" }, + websearch: { query: "timeline stability" }, + task: { description: "Inspect timeline", subagent_type: "explore" }, + skill: { name: "stability" }, + custom: { target: "timeline", depth: 2 }, + } + const names = { webfetch: "webfetch", websearch: "websearch", task: "task", skill: "skill", custom: "mcp_probe" } + const questionID = "prt_state_question" + const todoID = "prt_state_todo" + const initial = [ + ...ids.map((id) => toolPart(`prt_state_${id}`, names[id], "pending", inputs[id])), + toolPart(questionID, "question", "pending", questionInput()), + toolPart(todoID, "todowrite", "pending", { todos: [{ content: "Hidden", status: "pending" }] }), + textPart("prt_state_following", "Following lightweight tools"), + ] + const childID = "ses_timeline_child" + const timeline = await setupTimeline(page, { + messages: [userMessage(), assistantMessage(initial, { completed: false })], + sessions: [session(), session({ id: childID, parentID: sessionID, title: "Inspect timeline" })], + cpuRate: 4, + }) + await timeline.send(status("busy"), 120) + for (const id of ids) await timeline.waitForPart(`prt_state_${id}`) + await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) + await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0) + + const regionIDs = [ + "prt_state_webfetch", + "prt_state_websearch", + "prt_state_task", + "prt_state_skill", + "prt_state_custom", + ] as const + const regions = defineVisualRegions({ + prt_state_webfetch: toolRegion(regionIDs[0]), + prt_state_websearch: toolRegion(regionIDs[1]), + prt_state_task: toolRegion(regionIDs[2]), + prt_state_skill: toolRegion(regionIDs[3]), + prt_state_custom: toolRegion(regionIDs[4]), + }) + await startVisualProbe(page, regions) + for (const [index, id] of ids.entries()) { + await timeline.send( + partUpdated(toolPart(`prt_state_${id}`, names[id], "running", inputs[id])), + [80, 240, 100, 360, 140][index], + ) + } + for (const [index, id] of ["skill", "webfetch", "custom", "task", "websearch"].entries()) { + const key = id as (typeof ids)[number] + const metadata = key === "task" ? { sessionId: childID } : key === "websearch" ? { provider: "exa" } : {} + const output = key === "websearch" ? "Result https://example.com/result" : "Completed" + await timeline.send( + partUpdated(toolPart(`prt_state_${key}`, names[key], "completed", inputs[key], { metadata, output })), + [110, 70, 280, 130, 420][index], + ) + } + await timeline.send( + partUpdated( + toolPart(questionID, "question", "completed", questionInput(), { metadata: { answers: [["Keep it stable"]] } }), + ), + 350, + ) + await timeline.waitForPart(questionID) + await timeline.send(status("idle"), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "lightweight-tools", + trace, + visualPlan(regions, [ + { type: "required", regions: regionIDs }, + { type: "unique", regions: regionIDs }, + { type: "stable", regions: regionIDs }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxReversals: 4 }, + { type: "label-stability", regions: "all" }, + ]), + ) + await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toContainText("Keep it stable") + await expect(page.locator(`[data-timeline-part-id="${todoID}"]`)).toHaveCount(0) + await expect( + page.locator(`a[href$="/session/${childID}"]`, { has: page.locator('[data-component="task-tool-card"]') }), + ).toBeVisible() + await expect(page.getByRole("button", { name: /Exa Web Search/ })).toBeVisible() + }) + + test("keeps an expanded mixed context group stable through staggered completion and error", async ({ + page, + }, testInfo) => { + const ids = ["prt_ctx_01_read", "prt_ctx_02_glob", "prt_ctx_03_grep", "prt_ctx_04_list"] + const tools = ["read", "glob", "grep", "list"] + const inputs = [ + { filePath: "src/a.ts", offset: 0, limit: 120 }, + { path: directory, pattern: "**/*.ts" }, + { path: directory, pattern: "stability", include: "*.ts" }, + { path: "src" }, + ] + const context = ids.map((id, index) => toolPart(id, tools[index]!, "pending", inputs[index]!)) + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([...context, textPart("prt_ctx_following", "Following context")], { completed: false }), + ], + cpuRate: 4, + }) + await timeline.send(status("busy"), 100) + const groupSelector = `[data-timeline-part-ids="${ids.join(",")}"]` + const group = page.locator(groupSelector) + await expect(group).toBeVisible() + await group.locator('[data-slot="collapsible-trigger"]').click() + await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") + + const regions = defineVisualRegions({ + status: { + selector: `${groupSelector} [data-component="tool-status-title"]`, + opacitySelectors: ['[data-slot="tool-status-active"]', '[data-slot="tool-status-done"]'], + }, + context: { selector: groupSelector, closest: '[data-timeline-row="AssistantPart"]' }, + following: { + selector: '[data-timeline-part-id="prt_ctx_following"]', + closest: '[data-timeline-row="AssistantPart"]', + }, + }) + await startVisualProbe(page, regions) + for (const [index, delay] of [90, 260, 70, 380].entries()) { + await timeline.send(partUpdated(toolPart(ids[index]!, tools[index]!, "running", inputs[index]!)), delay) + } + await timeline.send(partUpdated(toolPart(ids[1]!, tools[1]!, "completed", inputs[1]!)), 130) + await timeline.send(partUpdated(toolPart(ids[3]!, tools[3]!, "completed", inputs[3]!)), 210) + await timeline.send( + partUpdated(toolPart(ids[0]!, tools[0]!, "error", inputs[0]!, { error: "Read interrupted" })), + 110, + ) + await timeline.send(partUpdated(toolPart(ids[2]!, tools[2]!, "completed", inputs[2]!)), 250) + await expect(group.locator('[data-component="tool-status-title"]')).toHaveAttribute("aria-label", "Explored") + await timeline.send(status("idle"), 700) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "mixed-context", + trace, + visualPlan(regions, [ + { type: "required", regions: ["context", "following"] }, + { type: "unique", regions: ["context"] }, + { type: "stable", regions: ["context"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxReversals: 4 }, + { type: "label-stability", regions: "all" }, + { type: "flow", regions: ["context", "following"] }, + ]), + ) + await expect(group.locator('[data-component="tool-status-title"]')).toHaveAttribute("aria-label", "Explored") + await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") + await group.locator('[data-slot="collapsible-trigger"]').click() + await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false") + await timeline.send(partUpdated(textPart("prt_ctx_late_sibling", "Later sibling content")), 200) + await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "false") + await group.locator('[data-slot="collapsible-trigger"]').click() + await expect(group.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") + }) +}) + +function questionInput() { + return { questions: [{ header: "Stability", question: "Keep it stable?", options: [] }] } +} + +function toolRegion(id: string) { + return { selector: `[data-timeline-part-id="${id}"]`, closest: '[data-timeline-row="AssistantPart"]' } +} diff --git a/packages/app/e2e/performance/timeline-stability/transition-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/transition-matrix.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e999e8c50a6f677bd21d4b8b1dd0d09881e8f11f --- /dev/null +++ b/packages/app/e2e/performance/timeline-stability/transition-matrix.spec.ts @@ -0,0 +1,272 @@ +import { expect, test } from "@playwright/test" +import { + defineVisualRegions, + reportVisualStability, + startVisualProbe, + stopVisualProbe, + visualPlan, +} from "../../utils/visual-stability" +import { + assistantID, + assistantMessage, + completedAssistantInfo, + event, + messageUpdated, + partDelta, + partUpdated, + setupTimeline, + shell, + status, + textPart, + toolPart, + userMessage, +} from "./fixture" + +test("keeps unchanged siblings stable while a middle part is inserted and removed", async ({ page }, testInfo) => { + const firstID = "prt_mutation_01_first" + const middleID = "prt_mutation_02_middle" + const lastID = "prt_mutation_03_last" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([textPart(firstID, "First stable row"), textPart(lastID, "Last stable row")], { + completed: false, + }), + ], + cpuRate: 4, + }) + const regions = defineVisualRegions({ + first: { selector: `[data-timeline-part-id="${firstID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + last: { selector: `[data-timeline-part-id="${lastID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send(partUpdated(textPart(middleID, "Inserted middle row. ".repeat(12))), 350) + await expect(page.locator(`[data-timeline-part-id="${middleID}"]`)).toBeVisible() + await timeline.send( + event("message.part.removed", { sessionID: "ses_timeline_stability", messageID: assistantID, partID: middleID }), + 500, + ) + await expect(page.locator(`[data-timeline-part-id="${middleID}"]`)).toHaveCount(0) + const trace = await stopVisualProbe(page) + await reportVisualStability(testInfo, "middle-insert-remove", trace, stablePairPlan(regions, 1)) +}) + +test("streams text through growth, canonical replacement, and completion", async ({ page }, testInfo) => { + const textID = "prt_text_reconcile" + const followingID = "prt_text_reconcile_following" + const assistant = assistantMessage([textPart(textID, "Starting"), textPart(followingID, "Following text row")], { + completed: false, + }) + const timeline = await setupTimeline(page, { messages: [userMessage(), assistant], cpuRate: 4 }) + const regions = defineVisualRegions({ + text: { selector: `[data-timeline-part-id="${textID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send(partDelta(textID, " streamed content"), 100) + await timeline.send(partDelta(textID, "\n\n- item one\n- item two\n- item three"), 180) + await timeline.send(partUpdated(textPart(textID, "Canonical replacement with a shorter final paragraph.")), 200) + await timeline.send(messageUpdated(completedAssistantInfo(assistant.info)), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "text-reconcile", + trace, + visualPlan(regions, [ + { type: "required", regions: ["text", "following"] }, + { type: "unique", regions: ["text", "following"] }, + { type: "stable", regions: ["text", "following"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 1, maxReversals: 2 }, + { type: "label-stability", regions: "all" }, + { type: "preserve-bottom-anchor" }, + { type: "flow", regions: ["text", "following"] }, + ]), + ) +}) + +test("inserts a completed question between stable rows", async ({ page }, testInfo) => { + const firstID = "prt_question_01_first" + const questionID = "prt_question_02_hidden" + const lastID = "prt_question_03_last" + const input = { questions: [{ header: "Choice", question: "Keep stable?", options: [] }] } + const timeline = await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage( + [ + textPart(firstID, "Before question"), + toolPart(questionID, "question", "running", input), + textPart(lastID, "After question"), + ], + { completed: false }, + ), + ], + cpuRate: 4, + }) + await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toHaveCount(0) + const regions = defineVisualRegions({ + first: { selector: `[data-timeline-part-id="${firstID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + last: { selector: `[data-timeline-part-id="${lastID}"]`, closest: '[data-timeline-row="AssistantPart"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send( + partUpdated(toolPart(questionID, "question", "completed", input, { metadata: { answers: [["Yes"]] } })), + 600, + ) + await expect(page.locator(`[data-timeline-part-id="${questionID}"]`)).toBeVisible() + const trace = await stopVisualProbe(page) + await reportVisualStability(testInfo, "question-insert", trace, stablePairPlan(regions, 0)) +}) + +test("replaces thinking with an assistant error without a blank turn", async ({ page }, testInfo) => { + const assistant = assistantMessage([], { completed: false }) + const timeline = await setupTimeline(page, { messages: [userMessage(), assistant], cpuRate: 4 }) + await timeline.send(status("busy"), 150) + await expect(page.locator('[data-timeline-row="Thinking"]')).toBeVisible() + const regions = defineVisualRegions({ + thinking: { selector: '[data-timeline-row="Thinking"]' }, + error: { selector: '[data-timeline-row="Error"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send( + messageUpdated({ + ...assistant.info, + error: { name: "APIError", data: { message: "Provider failed visibly", isRetryable: false } }, + }), + 500, + ) + await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0) + await expect(page.locator('[data-timeline-row="Error"]')).toContainText("Provider failed visibly") + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "thinking-error", + trace, + visualPlan(regions, [ + { type: "required", regions: ["thinking", "error"] }, + { type: "continuous-any", regions: ["thinking", "error"] }, + { type: "unique", regions: ["thinking", "error"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all" }, + { type: "label-stability", regions: "all" }, + ]), + ) +}) + +test("updates retry attempts and long provider messages without remounting the retry row", async ({ + page, +}, testInfo) => { + const timeline = await setupTimeline(page, { + messages: [userMessage(), assistantMessage([], { completed: false })], + cpuRate: 4, + }) + await timeline.send(status("retry", 1), 120) + await expect(page.locator('[data-timeline-row="Retry"]')).toBeVisible() + const regions = defineVisualRegions({ + retry: { selector: '[data-timeline-row="Retry"]' }, + }) + await startVisualProbe(page, regions) + await timeline.send( + event("session.status", { + sessionID: "ses_timeline_stability", + status: { + type: "retry", + attempt: 2, + message: "A very long provider retry message ".repeat(8), + next: Date.now() + 10_000, + }, + }), + 300, + ) + await timeline.send(status("retry", 3), 300) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "retry-evolution", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["retry"] }, + { type: "unique", regions: ["retry"] }, + { type: "stable", regions: ["retry"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + ], + { perMarker: true }, + ), + ) +}) + +test("reducer-hardening: removes a historical turn one message at a time without moving a visible lower anchor twice", async ({ + page, +}, testInfo) => { + const removeUserID = "msg_0500_remove_user" + const removeAssistantID = "msg_0501_remove_assistant" + const anchorUserID = "msg_2000_anchor_user" + const timeline = await setupTimeline(page, { + messages: [ + userMessage(undefined, { id: removeUserID, created: 1690000000000 }), + assistantMessage([textPart("prt_remove_text", "Removed historical content. ".repeat(15))], { + id: removeAssistantID, + parentID: removeUserID, + created: 1690000001000, + }), + userMessage(undefined, { id: anchorUserID, created: 1700000000000 }), + assistantMessage([textPart("prt_anchor_text", "Visible anchor response")], { + id: "msg_2001_anchor_assistant", + parentID: anchorUserID, + created: 1700000001000, + }), + ], + cpuRate: 4, + }) + const regions = defineVisualRegions({ + anchor: { selector: `[data-timeline-row="UserMessage"][data-message-id="${anchorUserID}"]` }, + }) + await startVisualProbe(page, regions) + await timeline.send( + event("message.removed", { sessionID: "ses_timeline_stability", messageID: removeAssistantID }), + 200, + ) + await timeline.send(event("message.removed", { sessionID: "ses_timeline_stability", messageID: removeUserID }), 500) + const trace = await stopVisualProbe(page) + await reportVisualStability( + testInfo, + "historical-turn-remove", + trace, + visualPlan( + regions, + [ + { type: "required", regions: ["anchor"] }, + { type: "unique", regions: ["anchor"] }, + { type: "stable", regions: ["anchor"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals: 0 }, + { type: "label-stability", regions: "all" }, + ], + { perMarker: true }, + ), + ) +}) + +function stablePairPlan( + regions: Record<"first" | "last", { selector: string; closest?: string }>, + maxPositionReversals: number, +) { + return visualPlan(regions, [ + { type: "required", regions: ["first", "last"] }, + { type: "unique", regions: ["first", "last"] }, + { type: "stable", regions: ["first", "last"] }, + { type: "opacity", regions: "all" }, + { type: "continuity", regions: "all" }, + { type: "motion", regions: "all", maxPositionReversals }, + { type: "label-stability", regions: "all" }, + ]) +} diff --git a/packages/app/e2e/performance/timeline/first-navigation-benchmark.spec.ts b/packages/app/e2e/performance/timeline/first-navigation-benchmark.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a07e8f3d89fb35ce1419e0b47d13510b44d9f482 --- /dev/null +++ b/packages/app/e2e/performance/timeline/first-navigation-benchmark.spec.ts @@ -0,0 +1,87 @@ +import { expectSessionTitle } from "../../utils/waits" +import { benchmark, expect } from "../benchmark" +import { measureFirstNavigation } from "./first-navigation-probe" +import { fixture } from "./session-timeline-stress.fixture" +import { + installStressSessionTabs, + installTimelineSettings, + mockStressTimeline, + stressDraftHref, + stressSessionHref, +} from "./timeline-test-helpers" +import { waitForStableTimeline } from "./session-tab-switch-probe" + +const contentSelector = '[data-message-id], [data-component="prompt-input"]' +const draftID = "draft_first_navigation" + +benchmark.describe("performance: first navigation paint", () => { + benchmark("opens an unvisited session tab without a blank frame", async ({ page, report }) => { + await setup(page) + const href = stressSessionHref(fixture.targetID) + const result = await measureFirstNavigation(page, { + href, + destinationPath: href, + sourceSelector: messageSelector(fixture.expected.sourceMessageIDs.at(-1)!), + destinationSelector: messageSelector(fixture.expected.targetMessageIDs.at(-1)!), + contentSelector, + navigate: async () => { + await page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first().click() + await expectSessionTitle(page, fixture.expected.targetTitle) + }, + }) + report(result) + expect(result.summary.blankSamples).toBe(0) + expect(result.summary.unknownSamples).toBe(0) + }) + + benchmark("opens the new session page before its lazy module is used", async ({ page, report }) => { + await setup(page, draftID) + const href = stressDraftHref(draftID) + const result = await measureFirstNavigation(page, { + href, + destinationPath: href, + sourceSelector: messageSelector(fixture.expected.sourceMessageIDs.at(-1)!), + destinationSelector: '[data-component="prompt-input"]', + contentSelector, + navigate: async () => { + await page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first().click() + await expect(page.locator('[data-component="prompt-input"]')).toBeVisible() + }, + }) + report(result) + expect(result.summary.blankSamples).toBe(0) + expect(result.summary.unknownSamples).toBe(0) + }) + + benchmark("opens a child session without a blank frame", async ({ page, report }) => { + await setup(page) + const href = stressSessionHref(fixture.childID) + const result = await measureFirstNavigation(page, { + href, + destinationPath: href, + sourceSelector: messageSelector(fixture.expected.sourceMessageIDs.at(-1)!), + destinationSelector: messageSelector(fixture.expected.childMessageIDs.at(-1)!), + contentSelector, + navigate: async () => { + await page.locator(`a[href="${href}"]`, { has: page.locator('[data-component="task-tool-card"]') }).click() + await expectSessionTitle(page, fixture.expected.childTitle) + }, + }) + report(result) + expect(result.summary.blankSamples).toBe(0) + expect(result.summary.unknownSamples).toBe(0) + }) +}) + +async function setup(page: Parameters[0], draft?: string) { + await mockStressTimeline(page) + await installTimelineSettings(page) + await installStressSessionTabs(page, draft ? { draftID: draft } : undefined) + await page.goto(stressSessionHref(fixture.sourceID)) + await expectSessionTitle(page, fixture.expected.sourceTitle) + await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) +} + +function messageSelector(id: string) { + return `[data-message-id="${id}"]` +} diff --git a/packages/app/e2e/performance/timeline/first-navigation-metrics.ts b/packages/app/e2e/performance/timeline/first-navigation-metrics.ts new file mode 100644 index 0000000000000000000000000000000000000000..db38678046918dde3a78c1dd89674c93e137b50e --- /dev/null +++ b/packages/app/e2e/performance/timeline/first-navigation-metrics.ts @@ -0,0 +1,32 @@ +export type FirstNavigationSample = { + observedAtMs: number + source: boolean + destination: boolean + content: boolean + pathname?: string + center?: string +} + +function category(sample: FirstNavigationSample) { + if (sample.destination && !sample.source) return "destination" + if (sample.source && !sample.destination) return "source" + if (!sample.content) return "blank" + return "unknown" +} + +export function summarizeFirstNavigation(samples: FirstNavigationSample[]) { + const categories = samples.map(category) + const stable = categories.findIndex( + (value, index) => + value === "destination" && categories[index + 1] === "destination" && categories[index + 2] === "destination", + ) + return { + samples: samples.length, + firstDestinationObservedMs: samples[categories.indexOf("destination")]?.observedAtMs ?? null, + stableDestinationObservedMs: stable === -1 ? null : samples[stable + 2]!.observedAtMs, + sourceSamples: categories.filter((value) => value === "source").length, + blankSamples: categories.filter((value) => value === "blank").length, + unknownSamples: categories.filter((value) => value === "unknown").length, + destinationSamples: categories.filter((value) => value === "destination").length, + } +} diff --git a/packages/app/e2e/performance/timeline/first-navigation-probe.ts b/packages/app/e2e/performance/timeline/first-navigation-probe.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e7ef45f2ffb707c6a6fa52f055c2c16247d0cd8 --- /dev/null +++ b/packages/app/e2e/performance/timeline/first-navigation-probe.ts @@ -0,0 +1,86 @@ +import type { Page } from "@playwright/test" +import { summarizeFirstNavigation, type FirstNavigationSample } from "./first-navigation-metrics" + +type FirstNavigationProbe = { + samples: FirstNavigationSample[] + stop: () => void +} + +export async function measureFirstNavigation( + page: Page, + input: { + href: string + destinationPath: string + sourceSelector: string + destinationSelector: string + contentSelector: string + navigate: () => Promise + }, +) { + await page.evaluate( + ({ href, destinationPath, sourceSelector, destinationSelector, contentSelector }) => { + const samples: FirstNavigationSample[] = [] + let started: number | undefined + let running = true + const visible = (selector: string) => + [...document.querySelectorAll(selector)].some((element) => { + const rect = element.getBoundingClientRect() + const style = getComputedStyle(element) + return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none" + }) + const sample = () => { + if (!running || started === undefined) return + requestAnimationFrame(() => { + setTimeout(() => { + if (!running || started === undefined) return + samples.push({ + observedAtMs: performance.now() - started, + source: visible(sourceSelector), + destination: `${location.pathname}${location.search}` === destinationPath && visible(destinationSelector), + content: visible(contentSelector), + pathname: `${location.pathname}${location.search}`, + center: document.elementFromPoint(innerWidth / 2, innerHeight / 2)?.textContent?.slice(0, 80), + }) + sample() + }, 0) + }) + } + document.addEventListener( + "click", + (event) => { + const link = event.target instanceof Element ? event.target.closest("a") : undefined + if (link?.getAttribute("href") !== href) return + started = performance.now() + sample() + }, + { capture: true, once: true }, + ) + ;(window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe = { + samples, + stop: () => { + running = false + }, + } + }, + { + href: input.href, + destinationPath: input.destinationPath, + sourceSelector: input.sourceSelector, + destinationSelector: input.destinationSelector, + contentSelector: input.contentSelector, + }, + ) + await input.navigate() + await page.waitForFunction(() => { + const samples = (window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe + ?.samples + if (!samples) return false + return samples.length >= 3 && samples.slice(-3).every((sample) => sample.destination && !sample.source) + }) + const samples = await page.evaluate(() => { + const probe = (window as Window & { __firstNavigationProbe?: FirstNavigationProbe }).__firstNavigationProbe! + probe.stop() + return probe.samples + }) + return { summary: summarizeFirstNavigation(samples), samples } +} diff --git a/packages/app/e2e/performance/timeline/home-tab-navigation-benchmark.spec.ts b/packages/app/e2e/performance/timeline/home-tab-navigation-benchmark.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b74c7d6bfd4884b94224fa2788850679089b199 --- /dev/null +++ b/packages/app/e2e/performance/timeline/home-tab-navigation-benchmark.spec.ts @@ -0,0 +1,114 @@ +import { benchmark, expect } from "../benchmark" +import { expectSessionTitle } from "../../utils/waits" +import { measureNavigationMilestones } from "./navigation-milestones" +import { fixture } from "./session-timeline-stress.fixture" +import { + installStressSessionTabs, + installTimelineSettings, + mockStressTimeline, + stressSessionHref, +} from "./timeline-test-helpers" +import { waitForStableTimeline } from "./session-tab-switch-probe" + +const homeRow = '[data-component="home-session-row"]' +const homeShell = '[data-component="home-session-search"]' + +benchmark.describe("performance: home and tab navigation", () => { + benchmark("opens a home session and paints its titlebar tab", async ({ page, report }) => { + await setup(page, []) + await page.goto("/") + const row = page.locator(homeRow).filter({ hasText: fixture.expected.targetTitle }).first() + await expect(row).toBeVisible() + const href = stressSessionHref(fixture.targetID) + const result = await measureNavigationMilestones(page, { + triggerSelector: homeRow, + milestones: { + content: { selector: messageSelector(fixture.expected.targetMessageIDs.at(-1)!) }, + tab: { selector: `[data-slot="titlebar-tabs"] a[href="${href}"]` }, + }, + navigate: async () => { + await row.click() + await expectSessionTitle(page, fixture.expected.targetTitle) + }, + }) + report(result) + await expect(page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`)).toContainText( + fixture.expected.targetTitle, + ) + }) + + benchmark("stages the review body after cold session content", async ({ page, report }) => { + await setup(page, []) + await page.goto("/") + const row = page.locator(homeRow).filter({ hasText: fixture.expected.targetTitle }).first() + await expect(row).toBeVisible() + const result = await page.evaluate( + ({ rowSelector, title, contentSelector }) => + new Promise<{ contentBeforeReview: boolean; samples: number }>((resolve) => { + let samples = 0 + const sample = () => { + samples++ + const content = !!document.querySelector(contentSelector) + const review = !!document.querySelector('[data-component="session-review"]') + if (content && !review) { + resolve({ contentBeforeReview: true, samples }) + return + } + if (content && review) { + resolve({ contentBeforeReview: false, samples }) + return + } + requestAnimationFrame(sample) + } + const target = [...document.querySelectorAll(rowSelector)].find((item) => + item.textContent?.includes(title), + ) + if (!target) throw new Error(`Home session row not found: ${title}`) + target.click() + requestAnimationFrame(sample) + }), + { + rowSelector: homeRow, + title: fixture.expected.targetTitle, + contentSelector: messageSelector(fixture.expected.targetMessageIDs.at(-1)!), + }, + ) + report(result) + expect(result.contentBeforeReview).toBe(true) + await expect(page.locator('[data-component="session-review"]')).toBeVisible() + }) + + benchmark("closes the only session tab and paints home", async ({ page, report }) => { + await setup(page, [fixture.sourceID]) + const href = stressSessionHref(fixture.sourceID) + await page.goto(href) + await expectSessionTitle(page, fixture.expected.sourceTitle) + await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) + const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first() + const close = tab.locator("..").locator('[data-component="icon-button-v2"]') + await expect(close).toBeVisible() + const result = await measureNavigationMilestones(page, { + triggerSelector: '[data-slot="titlebar-tabs"] [data-component="icon-button-v2"]', + milestones: { + home: { selector: homeShell }, + row: { selector: homeRow }, + tabRemoved: { selector: `[data-slot="titlebar-tabs"] a[href="${href}"]`, visible: false }, + }, + navigate: async () => { + await close.click() + await expect(page).toHaveURL("/") + }, + }) + report(result) + }) +}) + +async function setup(page: Parameters[0], sessionIDs: string[]) { + await mockStressTimeline(page) + await installTimelineSettings(page) + await installStressSessionTabs(page, { sessionIDs }) +} + +function messageSelector(id: string) { + return `[data-message-id="${id}"]` +} diff --git a/packages/app/e2e/performance/timeline/navigation-milestones.ts b/packages/app/e2e/performance/timeline/navigation-milestones.ts new file mode 100644 index 0000000000000000000000000000000000000000..b8ec858e84a3b265dc2e9dcddb7960251ed3592d --- /dev/null +++ b/packages/app/e2e/performance/timeline/navigation-milestones.ts @@ -0,0 +1,128 @@ +import type { Page } from "@playwright/test" + +export type NavigationMilestoneSample = { + observedAtMs: number + milestones: Record +} + +export function summarizeNavigationMilestones(samples: NavigationMilestoneSample[]) { + const names = Object.keys(samples[0]?.milestones ?? {}) + const summarize = (matches: (sample: NavigationMilestoneSample) => boolean) => { + const first = samples.find(matches) + const stable = samples.findIndex( + (sample, index) => + index + 2 < samples.length && matches(sample) && matches(samples[index + 1]!) && matches(samples[index + 2]!), + ) + return { + firstObservedMs: first?.observedAtMs ?? null, + stableObservedMs: stable === -1 ? null : samples[stable + 2]!.observedAtMs, + } + } + return { + samples: samples.length, + milestones: Object.fromEntries( + names.map((name) => [name, summarize((sample) => sample.milestones[name] === true)]), + ), + all: summarize((sample) => names.every((name) => sample.milestones[name] === true)), + } +} + +type NavigationMilestoneProbe = { + samples: NavigationMilestoneSample[] + stop: () => void +} + +export async function measureNavigationMilestones( + page: Page, + input: { + triggerSelector: string + milestones: Record + navigate: () => Promise + }, +) { + await page.evaluate( + ({ triggerSelector, milestones }) => { + const samples: NavigationMilestoneSample[] = [] + const streaks = new Map() + const marked = new Set() + let started: number | undefined + let running = true + const visible = (selector: string) => + [...document.querySelectorAll(selector)].some((element) => { + const rect = element.getBoundingClientRect() + const style = getComputedStyle(element) + return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none" + }) + const sample = () => { + if (!running || started === undefined) return + requestAnimationFrame(() => { + setTimeout(() => { + if (!running || started === undefined) return + const current = Object.fromEntries( + Object.entries(milestones).map(([name, milestone]) => [ + name, + milestone.visible === false ? !document.querySelector(milestone.selector) : visible(milestone.selector), + ]), + ) + samples.push({ + observedAtMs: performance.now() - started, + milestones: current, + }) + Object.entries(current).forEach(([name, value]) => { + if (!value) { + streaks.set(name, 0) + return + } + if (!marked.has(`${name}.first`)) { + performance.mark(`opencode.navigation.${name}.first`) + marked.add(`${name}.first`) + } + const streak = (streaks.get(name) ?? 0) + 1 + streaks.set(name, streak) + if (streak === 3) performance.mark(`opencode.navigation.${name}.stable`) + }) + const all = Object.values(current).every(Boolean) + const allStreak = all ? (streaks.get("all") ?? 0) + 1 : 0 + streaks.set("all", allStreak) + if (all && !marked.has("all.first")) { + performance.mark("opencode.navigation.all.first") + marked.add("all.first") + } + if (allStreak === 3) performance.mark("opencode.navigation.all.stable") + sample() + }, 0) + }) + } + document.addEventListener( + "click", + (event) => { + if (!(event.target instanceof Element) || !event.target.closest(triggerSelector)) return + started = performance.now() + performance.mark("opencode.navigation.click") + sample() + }, + { capture: true, once: true }, + ) + ;(window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones = { + samples, + stop: () => { + running = false + }, + } + }, + { triggerSelector: input.triggerSelector, milestones: input.milestones }, + ) + await input.navigate() + await page.waitForFunction(() => { + const samples = (window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones + ?.samples + if (!samples || samples.length < 3) return false + return samples.slice(-3).every((sample) => Object.values(sample.milestones).every(Boolean)) + }) + const samples = await page.evaluate(() => { + const probe = (window as Window & { __navigationMilestones?: NavigationMilestoneProbe }).__navigationMilestones! + probe.stop() + return probe.samples + }) + return { summary: summarizeNavigationMilestones(samples), samples } +} diff --git a/packages/app/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts b/packages/app/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..81de0a055f90053a4cfad4db551d204780d6e78c --- /dev/null +++ b/packages/app/e2e/performance/timeline/review-pane-scaling-benchmark.spec.ts @@ -0,0 +1,312 @@ +import type { Page } from "@playwright/test" +import { benchmark, expect } from "../benchmark" +import { setupTimelineBenchmark } from "./session-timeline-benchmark.fixture" + +const changedLinesPerFile = 100 +const linesPerSide = changedLinesPerFile / 2 +const fileCounts = [1, 10, 100, 1_000, 10_000] +const filesPerDirectory = 100 +const readyFrames = 3 +const completionTimeoutMs = Number(process.env.REVIEW_PANE_COMPLETION_TIMEOUT_MS ?? 900_000) + +type ReviewPaneScalingSample = { + observedAtMs: number + logicalRows: number + treeRows: number + fileRows: number + diffLines: number + header: string + ready: boolean +} + +type ReviewPaneScalingProbe = { + startedAt?: number + firstTreeRowMs?: number + logicalTreeReadyMs?: number + firstDiffRenderMs?: number + stableReadyMs?: number + samples: ReviewPaneScalingSample[] + frameTimesMs: number[] + longTasks: { startTime: number; duration: number }[] + stop: () => void +} + +benchmark.describe("performance: review pane scaling", () => { + for (const fileCount of fileCounts) { + const changedLines = fileCount * changedLinesPerFile + + benchmark( + `${changedLines} changed lines across ${fileCount} ${fileCount === 1 ? "file" : "files"}`, + async ({ page, report }) => { + benchmark.setTimeout(1_200_000) + await page.emulateMedia({ reducedMotion: "reduce" }) + + const patchByteLimit = Number(process.env.REVIEW_PANE_PATCH_BYTE_LIMIT ?? Number.POSITIVE_INFINITY) + if (Number.isNaN(patchByteLimit) || patchByteLimit < 0) + throw new Error(`Invalid REVIEW_PANE_PATCH_BYTE_LIMIT: ${process.env.REVIEW_PANE_PATCH_BYTE_LIMIT}`) + const responseBody = JSON.stringify(createScalingDiffs(fileCount, patchByteLimit)) + await setupTimelineBenchmark(page, { + historyTurns: 0, + eventBatch: 1, + newLayoutDesigns: true, + }) + await page.route("**/vcs/diff**", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + headers: { "access-control-allow-origin": "*" }, + body: responseBody, + }), + ) + + const expectedRows = fileCount + 2 + Math.ceil(fileCount / filesPerDirectory) + const metrics = await measureReviewPaneLoad(page, { + expectedFile: reviewFile(0), + expectedRows, + }) + const search = await measureBroadReviewSearch(page, fileCount) + + expect(metrics.logicalRows).toBe(expectedRows) + expect(metrics.fileRows).toBeGreaterThan(0) + expect(metrics.treeRows).toBeGreaterThan(0) + expect(metrics.diffLines).toBeGreaterThan(0) + expect(search.logicalRows).toBe(fileCount) + expect(search.renderedRows).toBeGreaterThan(0) + report( + { ...metrics, search }, + { + fileCount, + changedLinesPerFile, + changedLines, + additions: changedLines / 2, + deletions: changedLines / 2, + patchLines: changedLines, + patchByteLimit: Number.isFinite(patchByteLimit) ? patchByteLimit : null, + payloadBytes: new TextEncoder().encode(responseBody).byteLength, + expectedRows, + }, + ) + }, + ) + } +}) + +async function measureBroadReviewSearch(page: Page, expectedRows: number) { + const filter = page.getByRole("searchbox", { name: "Filter files" }) + await filter.evaluate((element) => { + element.addEventListener( + "input", + () => { + ;(window as Window & { __reviewSearchStartedAt?: number }).__reviewSearchStartedAt = performance.now() + }, + { once: true, capture: true }, + ) + }) + await filter.fill("file-") + + return page.evaluate((expectedRows) => { + const startedAt = (window as Window & { __reviewSearchStartedAt?: number }).__reviewSearchStartedAt! + return new Promise<{ stableMs: number; logicalRows: number; renderedRows: number }>((resolve) => { + let previous = -1 + let streak = 0 + const sample = () => { + const tree = document.querySelector('#review-panel [data-component="file-tree-v2"]') + const rows = [...document.querySelectorAll('#review-panel [data-slot="file-tree-v2-row"]')] + const logicalRows = Number(tree?.dataset.totalRows ?? rows.length) + const ready = + logicalRows === expectedRows && rows.length > 0 && rows.every((row) => row.textContent?.includes("file-")) + streak = ready && rows.length === previous ? streak + 1 : ready ? 1 : 0 + previous = rows.length + if (streak >= 3) { + resolve({ stableMs: performance.now() - startedAt, logicalRows, renderedRows: rows.length }) + return + } + requestAnimationFrame(sample) + } + requestAnimationFrame(sample) + }) + }, expectedRows) +} + +function createScalingDiffs(fileCount: number, patchByteLimit: number) { + const changes = Array.from({ length: linesPerSide }, (_, index) => { + const line = String(index).padStart(3, "0") + return `-export const value_${line} = "before"\n+export const value_${line} = "after"` + }).join("\n") + let patchBytes = 0 + let capped = false + + return Array.from({ length: fileCount }, (_, index) => { + const file = reviewFile(index) + const fullPatch = [ + `diff --git a/${file} b/${file}`, + `--- a/${file}`, + `+++ b/${file}`, + `@@ -1,${linesPerSide} +1,${linesPerSide} @@`, + changes, + ].join("\n") + if (index === 0 && fullPatch.length > patchByteLimit) + throw new Error(`REVIEW_PANE_PATCH_BYTE_LIMIT must include the active patch (${fullPatch.length} bytes)`) + const patch = !capped && patchBytes + fullPatch.length <= patchByteLimit ? fullPatch : emptyReviewPatch(file) + if (patch === fullPatch) patchBytes += fullPatch.length + else capped = true + return { + file, + patch, + additions: linesPerSide, + deletions: linesPerSide, + status: "modified" as const, + } + }) +} + +function emptyReviewPatch(file: string) { + return [`diff --git a/${file} b/${file}`, `--- a/${file}`, `+++ b/${file}`].join("\n") +} + +function reviewFile(index: number) { + return `src/review/d${String(Math.floor(index / filesPerDirectory)).padStart(5, "0")}/file-${String(index).padStart(5, "0")}.ts` +} + +async function measureReviewPaneLoad(page: Page, input: { expectedFile: string; expectedRows: number }) { + const toggle = page.getByRole("button", { name: "Toggle review" }) + await expect(toggle).toBeVisible() + await toggle.evaluate((element) => element.setAttribute("data-review-pane-scaling-toggle", "")) + await installReviewPaneScalingProbe(page, input) + await toggle.click() + await page.waitForFunction( + () => + (window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe + ?.stableReadyMs !== undefined, + undefined, + { timeout: completionTimeoutMs }, + ) + + return page.evaluate(() => { + const probe = (window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe! + probe.stop() + const startedAt = probe.startedAt! + const final = probe.samples.at(-1)! + const resources = performance + .getEntriesByType("resource") + .filter((entry) => entry.name.includes("/vcs/diff")) as PerformanceResourceTiming[] + const resource = resources.at(-1) + const longTasks = probe.longTasks.filter( + (entry) => entry.startTime >= startedAt && entry.startTime <= startedAt + probe.stableReadyMs!, + ) + const frameGaps = probe.frameTimesMs.map((time, index) => time - (probe.frameTimesMs[index - 1] ?? 0)) + + return { + firstTreeRowMs: probe.firstTreeRowMs ?? null, + logicalTreeReadyMs: probe.logicalTreeReadyMs ?? null, + firstDiffRenderMs: probe.firstDiffRenderMs ?? null, + stableReadyMs: probe.stableReadyMs ?? null, + responseStartMs: resource ? resource.responseStart - startedAt : null, + responseEndMs: resource ? resource.responseEnd - startedAt : null, + responseToStableMs: resource ? probe.stableReadyMs! - (resource.responseEnd - startedAt) : null, + treeRows: final.treeRows, + logicalRows: final.logicalRows, + fileRows: final.fileRows, + diffLines: final.diffLines, + samples: probe.samples.length, + maxFrameGapMs: Math.max(0, ...frameGaps), + longTaskCount: longTasks.length, + longTaskTotalMs: longTasks.reduce((sum, entry) => sum + entry.duration, 0), + maxLongTaskMs: Math.max(0, ...longTasks.map((entry) => entry.duration)), + } + }) +} + +async function installReviewPaneScalingProbe(page: Page, input: { expectedFile: string; expectedRows: number }) { + await page.evaluate( + ({ expectedFile, expectedRows, stableFrames }) => { + let running = true + let readyStreak = 0 + const basename = expectedFile.split("/").at(-1)! + const longTaskObserver = PerformanceObserver.supportedEntryTypes.includes("longtask") + ? new PerformanceObserver((list) => { + probe.longTasks.push( + ...list.getEntries().map((entry) => ({ startTime: entry.startTime, duration: entry.duration })), + ) + }) + : undefined + const probe: ReviewPaneScalingProbe = { + samples: [], + frameTimesMs: [], + longTasks: [], + stop: () => { + running = false + longTaskObserver?.disconnect() + }, + } + + const sample = (time: number) => { + if (!running || probe.startedAt === undefined) return + const panel = document.querySelector("#review-panel") + const tree = panel?.querySelector('[data-component="file-tree-v2"]') + const rows = panel?.querySelectorAll('[data-slot="file-tree-v2-row"]') ?? [] + const fileRows = panel?.querySelectorAll('button[data-slot="file-tree-v2-row"]') ?? [] + const header = + panel?.querySelector('[data-slot="session-review-v2-file-header"]')?.textContent?.trim() ?? "" + const viewers = panel + ? [...panel.querySelectorAll('[data-component="file"][data-mode="diff"]')] + : [] + const diffLines = viewers.reduce( + (sum, viewer) => + sum + (viewer.querySelector("diffs-container")?.shadowRoot?.querySelectorAll("[data-line]").length ?? 0), + 0, + ) + const observedAtMs = time - probe.startedAt + const logicalRows = Number(tree?.dataset.totalRows ?? rows.length) + const ready = + logicalRows === expectedRows && + fileRows.length > 0 && + header.includes(basename) && + viewers.length === 1 && + diffLines > 0 + const previous = probe.samples.at(-1) + const stable = + ready && + previous?.ready === true && + previous.logicalRows === logicalRows && + previous.treeRows === rows.length && + previous.fileRows === fileRows.length && + previous.diffLines === diffLines && + previous.header === header + + probe.frameTimesMs.push(observedAtMs) + probe.samples.push({ + observedAtMs, + logicalRows, + treeRows: rows.length, + fileRows: fileRows.length, + diffLines, + header, + ready, + }) + if (probe.firstTreeRowMs === undefined && rows.length > 0) probe.firstTreeRowMs = observedAtMs + if (probe.logicalTreeReadyMs === undefined && logicalRows === expectedRows) + probe.logicalTreeReadyMs = observedAtMs + if (probe.firstDiffRenderMs === undefined && diffLines > 0) probe.firstDiffRenderMs = observedAtMs + readyStreak = !ready ? 0 : stable ? readyStreak + 1 : 1 + if (readyStreak === stableFrames) probe.stableReadyMs = observedAtMs + if (probe.stableReadyMs === undefined) requestAnimationFrame(sample) + } + + longTaskObserver?.observe({ type: "longtask", buffered: true }) + document.addEventListener( + "click", + (event) => { + const toggle = event.target instanceof Element ? event.target.closest("button") : undefined + if (!toggle?.hasAttribute("data-review-pane-scaling-toggle")) return + probe.startedAt = performance.now() + performance.mark("opencode.review-pane-scaling.click") + requestAnimationFrame(sample) + }, + { capture: true, once: true }, + ) + ;(window as Window & { __reviewPaneScalingProbe?: ReviewPaneScalingProbe }).__reviewPaneScalingProbe = probe + }, + { ...input, stableFrames: readyFrames }, + ) +} diff --git a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..838af17c9399b035de0d5d07efd1a7c3ac2c4eb4 --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts @@ -0,0 +1,172 @@ +import type { Page } from "@playwright/test" +import { expectSessionTitle } from "../../utils/waits" +import { mockOpenCodeServer } from "../../utils/mock-server" +import { benchmark, expect, withBenchmarkPage } from "../benchmark" +import { fixture } from "./session-timeline-stress.fixture" +import { installStressSessionTabs, stressSessionHref } from "./timeline-test-helpers" +import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switch-probe" + +type ParentHydrationBenchmarkMode = "natural" | "candidate" + +const mode = process.env.SESSION_PARENT_HYDRATION_BENCHMARK_MODE ?? "natural" +if (mode !== "natural" && mode !== "candidate") throw new Error(`Unknown parent hydration benchmark mode: ${mode}`) +const userID = "msg_parent_hydration_user" +const user = { + ...fixture.messages[fixture.targetID][0]!, + info: { ...fixture.messages[fixture.targetID][0]!.info, id: userID, time: { created: 1700001000000 } }, + parts: fixture.messages[fixture.targetID][0]!.parts.map((part, index) => ({ + ...part, + id: `prt_parent_hydration_user_${index}`, + messageID: userID, + })), +} +const assistantSeed = fixture.messages[fixture.targetID][3]! +const assistants = Array.from({ length: 14 }, (_, index) => { + const messageID = `msg_parent_hydration_${String(index).padStart(2, "0")}` + return { + ...assistantSeed, + info: { + ...assistantSeed.info, + id: messageID, + parentID: userID, + time: { created: 1700001001000 + index * 1_000, completed: 1700001001500 + index * 1_000 }, + }, + parts: assistantSeed.parts.map((part, partIndex) => ({ + ...part, + id: `prt_parent_hydration_${String(index).padStart(2, "0")}_${partIndex}`, + messageID, + })), + } +}) +const messages = [user, ...assistants] +const target = fixture.sessions.find((session) => session.id === fixture.targetID)! +const lastID = userID +const lastAssistant = assistants.at(-1)! +const lastPart = lastAssistant.parts.at(-1)! +const lastPartID = + lastPart.type === "tool" + ? lastPart.id + : `${lastAssistant.info.id}:${lastPart.type}:${lastAssistant.parts.filter((part) => part.type === lastPart.type).length - 1}` + +benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => { + benchmark.setTimeout(180_000) + const results = [] as Awaited>[] + for (let run = 0; run < 5; run++) { + results.push( + await withBenchmarkPage( + browser, + `session-parent-hydration-${mode}-${run}`, + (page) => trial(page, mode), + testInfo, + ), + ) + } + const timing = results.map((result) => result.metrics.firstCorrectObservedMs!).sort((a, b) => a - b) + report( + { + results: results.map((result) => ({ ...result.metrics, historyGateCount: result.historyGateCount })), + summary: { + firstCorrectObservedMs: { min: timing[0], median: timing[2], max: timing.at(-1) }, + blankSamples: results.map((result) => result.metrics.blankSamples), + requestCounts: { + list: results.map((result) => result.requestCounts.list), + parent: results.map((result) => result.requestCounts.parent), + }, + historyGateCount: results.map((result) => result.historyGateCount), + }, + }, + { mode }, + ) +}) + +async function trial(page: Page, mode: ParentHydrationBenchmarkMode) { + const requests: { type: "list" | "parent"; before?: string }[] = [] + const history = mode === "candidate" ? Promise.withResolvers() : undefined + let historyGates = 0 + await mockOpenCodeServer(page, { + sessions: fixture.sessions.filter((session) => session.id === fixture.sourceID), + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + messageDelay: 50, + onMessages: (request) => { + if (request.sessionID === fixture.targetID && request.phase === "start") + requests.push({ type: "list", before: request.before }) + }, + beforeMessagesResponse: (request) => { + if (mode !== "candidate" || request.sessionID !== fixture.targetID || !request.before) return Promise.resolve() + historyGates++ + return history!.promise + }, + onMessage: (request) => { + if (request.sessionID === fixture.targetID && request.messageID === userID) requests.push({ type: "parent" }) + }, + message: (sessionID, messageID) => { + if (sessionID !== fixture.targetID || messageID !== userID) return + return user + }, + pageMessages: (sessionID, limit, before) => { + const items = sessionID === fixture.targetID ? messages : fixture.messages[fixture.sourceID] + const end = before ? items.findIndex((message) => message.info.id === before) : items.length + const start = Math.max(0, end - limit) + return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.info.id : undefined } + }, + }) + await page.route(`**/session/${fixture.targetID}`, (route) => { + const current = new URL(route.request().url()).pathname.startsWith("/api/") + return route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify( + current + ? { + data: { + ...target, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + location: { directory: target.directory }, + }, + } + : target, + ), + }) + }) + await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] }) + await page.goto(stressSessionHref(fixture.sourceID)) + await expectSessionTitle(page, fixture.expected.sourceTitle) + await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) + + const href = stressSessionHref(fixture.targetID) + await page.evaluate( + ({ href, title }) => { + const link = document.createElement("a") + link.id = "parent-hydration-target" + link.href = href + link.textContent = title + document.body.append(link) + }, + { href, title: target.title }, + ) + const metrics = await measureSessionSwitch(page, { + destinationIDs: messages.map((message) => message.info.id), + sourceIDs: fixture.messages[fixture.sourceID].map((message) => message.info.id), + lastID, + requiredPartID: lastPartID, + requireBottomAnchor: false, + href, + switch: async () => { + await page.locator("#parent-hydration-target").click() + await expectSessionTitle(page, target.title) + }, + }).finally(() => history?.resolve()) + expect(metrics.firstCorrectObservedMs).not.toBeNull() + const requestCounts = { + list: requests.filter((request) => request.type === "list").length, + parent: requests.filter((request) => request.type === "parent").length, + } + if (mode === "candidate") { + expect(requestCounts.parent).toBe(0) + expect(historyGates).toBe(0) + } + return { metrics, requestCounts, historyGateCount: historyGates } +} diff --git a/packages/app/e2e/performance/timeline/session-tab-flash.spec.ts b/packages/app/e2e/performance/timeline/session-tab-flash.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..051b29d0cd9f47e0fa3dd4e84d9219af15d6f24c --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-tab-flash.spec.ts @@ -0,0 +1,67 @@ +import { benchmark, expect } from "../benchmark" +import { expectSessionTitle } from "../../utils/waits" +import { fixture } from "./session-timeline-stress.fixture" +import { + collectCachedRepaintTrace, + compressCachedRepaintTrace, + installCachedRepaintProbe, + waitForCachedRepaintWindow, +} from "./session-tab-repaint-probe" +import { waitForStableTimeline } from "./session-tab-switch-probe" +import { + installStressSessionTabs, + installTimelineSettings, + mockStressTimeline, + stressSessionHref, +} from "./timeline-test-helpers" + +benchmark("samples cached session repaint after the click", async ({ page, report }) => { + benchmark.setTimeout(120_000) + await mockStressTimeline(page) + await installStressSessionTabs(page) + await installTimelineSettings(page) + await page.goto(stressSessionHref(fixture.targetID)) + await expectSessionTitle(page, fixture.expected.targetTitle) + await waitForStableTimeline(page, fixture.expected.targetMessageIDs.at(-1)!) + await page + .locator(`[data-slot="titlebar-tabs"] a[href="${stressSessionHref(fixture.sourceID)}"]`) + .first() + .click() + await expectSessionTitle(page, fixture.expected.sourceTitle) + await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) + + await installCachedRepaintProbe(page, { + targetHref: stressSessionHref(fixture.targetID), + destination: fixture.messages[fixture.targetID].map((message) => message.info.id), + source: fixture.messages[fixture.sourceID].map((message) => message.info.id), + last: fixture.expected.targetMessageIDs.at(-1)!, + windowMs: 1_000, + }) + + await page + .locator(`[data-slot="titlebar-tabs"] a[href="${stressSessionHref(fixture.targetID)}"]`) + .first() + .click() + await Promise.all([expectSessionTitle(page, fixture.expected.targetTitle), waitForCachedRepaintWindow(page, 1_000)]) + const result = await collectCachedRepaintTrace(page) + report(compressCachedRepaintTrace(result)) + expect(result.samples.length).toBeGreaterThan(0) +}) + +benchmark("prefetches every open session tab", async ({ page, report }) => { + const prefetched = new Set() + await mockStressTimeline(page, { + onMessages: (input) => { + if (!input.before && input.phase === "start") prefetched.add(input.sessionID) + }, + }) + await installStressSessionTabs(page, { + sessionIDs: [fixture.sourceID, fixture.targetID, fixture.childID], + }) + await installTimelineSettings(page) + await page.goto(stressSessionHref(fixture.sourceID)) + await expectSessionTitle(page, fixture.expected.sourceTitle) + + await expect.poll(() => prefetched.has(fixture.childID)).toBe(true) + report({ prefetched: [...prefetched] }) +}) diff --git a/packages/app/e2e/performance/timeline/session-tab-repaint-probe.ts b/packages/app/e2e/performance/timeline/session-tab-repaint-probe.ts new file mode 100644 index 0000000000000000000000000000000000000000..862e080f13dcb9ca53b9c44606e1144160003c6b --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-tab-repaint-probe.ts @@ -0,0 +1,251 @@ +import type { Page } from "@playwright/test" + +type CachedRepaintTrace = { + timeOriginEpochMs: number + startedAtPerformanceMs: number + samples: { + observedAtMs: number + root: number | undefined + scrollTop: number + scrollHeight: number + bottomErrorPx: number | undefined + last: boolean + rows: { key: string | undefined; node: number; top: number; bottom: number }[] + mounted: number + center: string | undefined + destination: string[] + source: string[] + }[] + mutations: { observedAtMs: number; changed: { type: string; node: number }[] }[] + shifts: { occurredAtMs: number; value: number }[] + windowMs: number + running: boolean + stop: () => void +} + +export async function installCachedRepaintProbe( + page: Page, + input: { targetHref: string; destination: string[]; source: string[]; last: string; windowMs: number }, +) { + await page.evaluate(({ targetHref, destination, source, last, windowMs }) => { + const destinationIDs = new Set(destination) + const sourceIDs = new Set(source) + const nodeIDs = new WeakMap() + let nextNodeID = 1 + const id = (node: Node) => { + const current = nodeIDs.get(node) + if (current) return current + nodeIDs.set(node, nextNodeID) + return nextNodeID++ + } + const state: CachedRepaintTrace = { + timeOriginEpochMs: performance.timeOrigin, + startedAtPerformanceMs: 0, + samples: [], + mutations: [], + shifts: [], + windowMs, + running: false, + stop: () => {}, + } + const recordShifts = (entries: PerformanceEntry[]) => { + if (!state.running) return + state.shifts.push( + ...entries + .map((entry) => { + if ( + entry.startTime < state.startedAtPerformanceMs || + entry.startTime > state.startedAtPerformanceMs + state.windowMs + ) + return + return { + occurredAtMs: entry.startTime - state.startedAtPerformanceMs, + value: (entry as PerformanceEntry & { value: number }).value, + } + }) + .filter((entry): entry is { occurredAtMs: number; value: number } => entry !== undefined), + ) + } + const shiftObserver = new PerformanceObserver((entries) => recordShifts(entries.getEntries())) + shiftObserver.observe({ type: "layout-shift" }) + const recordMutations = (entries: MutationRecord[]) => { + if (!state.running) return + const observedAtMs = performance.now() - state.startedAtPerformanceMs + if (observedAtMs > state.windowMs) return + const changed = entries.flatMap((entry) => [ + ...[...entry.addedNodes].map((node) => ({ type: "add", node: id(node) })), + ...[...entry.removedNodes].map((node) => ({ type: "remove", node: id(node) })), + ]) + if (changed.length) state.mutations.push({ observedAtMs, changed }) + } + const mutationObserver = new MutationObserver(recordMutations) + mutationObserver.observe(document.documentElement, { childList: true, subtree: true }) + state.stop = () => { + recordShifts(shiftObserver.takeRecords()) + recordMutations(mutationObserver.takeRecords()) + state.running = false + shiftObserver.disconnect() + mutationObserver.disconnect() + } + const sample = () => { + if (!state.running) return + setTimeout(() => { + if (!state.running) return + const observedAtMs = performance.now() - state.startedAtPerformanceMs + if (observedAtMs > state.windowMs) return + const root = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector("[data-timeline-row]"), + ) + if (root) { + const view = root.getBoundingClientRect() + const rows = [...root.querySelectorAll("[data-timeline-key]")] + .map((element) => ({ + key: element.dataset.timelineKey, + node: id(element), + rect: element.getBoundingClientRect(), + })) + .filter((item) => item.rect.bottom > view.top && item.rect.top < view.bottom) + .map((item) => ({ + key: item.key, + node: item.node, + top: item.rect.top - view.top, + bottom: item.rect.bottom - view.top, + })) + const messages = [...root.querySelectorAll("[data-message-id]")] + .filter((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + .map((element) => element.dataset.messageId!) + const spacer = root.querySelector('[data-timeline-row="bottom-spacer"]')?.getBoundingClientRect() + state.samples.push({ + observedAtMs, + root: id(root), + scrollTop: root.scrollTop, + scrollHeight: root.scrollHeight, + bottomErrorPx: spacer ? spacer.bottom - view.bottom : undefined, + last: messages.includes(last), + rows, + mounted: root.querySelectorAll("[data-timeline-key]").length, + center: document + .elementFromPoint(view.left + view.width / 2, view.top + view.height / 2) + ?.textContent?.slice(0, 80), + destination: messages.filter((messageID) => destinationIDs.has(messageID)), + source: messages.filter((messageID) => sourceIDs.has(messageID)), + }) + } else { + state.samples.push({ + observedAtMs, + root: undefined, + scrollTop: 0, + scrollHeight: 0, + bottomErrorPx: undefined, + last: false, + rows: [], + mounted: 0, + center: document.elementFromPoint(innerWidth / 2, innerHeight / 2)?.textContent?.slice(0, 80), + destination: [], + source: [], + }) + } + requestAnimationFrame(sample) + }, 0) + } + document.addEventListener( + "click", + (event) => { + const link = event.target instanceof Element ? event.target.closest("a") : undefined + if (link?.getAttribute("href") !== targetHref) return + state.startedAtPerformanceMs = performance.now() + state.running = true + requestAnimationFrame(sample) + }, + { capture: true, once: true }, + ) + ;(window as Window & { __cachedFlash?: CachedRepaintTrace }).__cachedFlash = state + }, input) +} + +export function layoutShiftSample(entry: Pick & { value: number }, started: number) { + if (entry.startTime < started) return + return { occurredAtMs: entry.startTime - started, value: entry.value } +} + +export async function waitForCachedRepaintWindow(page: Page, durationMs: number) { + await page.waitForFunction((durationMs) => { + const state = (window as Window & { __cachedFlash?: CachedRepaintTrace }).__cachedFlash + return !!state?.running && performance.now() - state.startedAtPerformanceMs >= durationMs + }, durationMs) +} + +export async function collectCachedRepaintTrace(page: Page) { + return page.evaluate(() => { + const state = (window as Window & { __cachedFlash?: CachedRepaintTrace }).__cachedFlash! + state.stop() + return state + }) +} + +export function summarizeCachedRepaintTrace(trace: CachedRepaintTrace) { + const roots = trace.samples.map((sample) => sample.root) + const bottomErrors = trace.samples.flatMap((sample) => + sample.bottomErrorPx === undefined ? [] : [Math.abs(sample.bottomErrorPx)], + ) + const category = (sample: CachedRepaintTrace["samples"][number]) => { + if (sample.source.length) return "source" + if (sample.root === undefined || sample.rows.length === 0) return "blank" + if (!sample.destination.length) return "unknown" + if (sample.last && Math.abs(sample.bottomErrorPx ?? Infinity) <= 1) return "correct" + return "wrongDestination" + } + return { + samples: trace.samples.length, + durationMs: trace.samples.at(-1)?.observedAtMs ?? 0, + firstSampleObservedMs: trace.samples[0]?.observedAtMs, + firstSampleCorrect: trace.samples[0] ? category(trace.samples[0]) === "correct" : false, + blankSamples: trace.samples.filter((sample) => category(sample) === "blank").length, + sourceSamples: trace.samples.filter((sample) => category(sample) === "source").length, + wrongDestinationSamples: trace.samples.filter((sample) => category(sample) === "wrongDestination").length, + unknownSamples: trace.samples.filter((sample) => category(sample) === "unknown").length, + rootChanges: roots.slice(1).filter((root, index) => root !== roots[index]).length, + mountedMin: trace.samples.length ? Math.min(...trace.samples.map((sample) => sample.mounted)) : 0, + mountedMax: Math.max(...trace.samples.map((sample) => sample.mounted)), + maxBottomErrorPx: Math.max(0, ...bottomErrors), + mutationBatches: trace.mutations.length, + addedNodes: trace.mutations.reduce( + (sum, batch) => sum + batch.changed.filter((change) => change.type === "add").length, + 0, + ), + removedNodes: trace.mutations.reduce( + (sum, batch) => sum + batch.changed.filter((change) => change.type === "remove").length, + 0, + ), + layoutShiftValueSum: trace.shifts.reduce((sum, shift) => sum + shift.value, 0), + maxLayoutShiftValue: Math.max(0, ...trace.shifts.map((shift) => shift.value)), + } +} + +export function compressCachedRepaintTrace(trace: CachedRepaintTrace) { + const samples: { + observedAtMs: number[] + state: Omit + }[] = [] + for (const sample of trace.samples) { + const { observedAtMs, ...state } = sample + const previous = samples.at(-1) + if (previous && JSON.stringify(previous.state) === JSON.stringify(state)) { + previous.observedAtMs.push(observedAtMs) + continue + } + samples.push({ observedAtMs: [observedAtMs], state }) + } + return { + timeOriginEpochMs: trace.timeOriginEpochMs, + startedAtPerformanceMs: trace.startedAtPerformanceMs, + windowMs: trace.windowMs, + summary: summarizeCachedRepaintTrace(trace), + samples, + mutations: trace.mutations, + shifts: trace.shifts, + } +} diff --git a/packages/app/e2e/performance/timeline/session-tab-switch-benchmark.spec.ts b/packages/app/e2e/performance/timeline/session-tab-switch-benchmark.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..48d4d3deb0d00911960235a489715d7aab8d00bf --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-tab-switch-benchmark.spec.ts @@ -0,0 +1,144 @@ +import type { Page } from "@playwright/test" +import { expectSessionTitle } from "../../utils/waits" +import { benchmark, expect, withBenchmarkPage } from "../benchmark" +import { fixture } from "./session-timeline-stress.fixture" +import { + createReviewDiffs, + installStressSessionTabs, + installTimelineSettings, + mockStressTimeline, + stressSessionHref, +} from "./timeline-test-helpers" +import { measureSessionSwitch, waitForStableTimeline } from "./session-tab-switch-probe" + +type Result = Awaited> + +benchmark("benchmarks cold and hot session tab switching", async ({ browser, report }, testInfo) => { + benchmark.setTimeout(180_000) + const results = { cold: [] as Result[], hot: [] as Result[] } + for (const mode of ["cold", "hot"] as const) { + for (let run = 0; run < 5; run++) { + results[mode].push( + await withBenchmarkPage(browser, `session-tab-switch-${mode}-${run}`, (page) => trial(page, mode), testInfo), + ) + } + } + report({ results, summary: summarize(results) }) +}) + +benchmark( + "benchmarks v2 session tab switching with and without the review pane", + async ({ browser, report }, testInfo) => { + benchmark.setTimeout(360_000) + const runs = Number(process.env.SESSION_TAB_SWITCH_RUNS ?? 5) + const results = { + closed: { cold: [] as Result[], hot: [] as Result[] }, + open: { cold: [] as Result[], hot: [] as Result[] }, + } + for (const reviewPane of ["closed", "open"] as const) { + for (const mode of ["cold", "hot"] as const) { + for (let run = 0; run < runs; run++) { + results[reviewPane][mode].push( + await withBenchmarkPage( + browser, + `session-tab-switch-v2-${reviewPane}-${mode}-${run}`, + (page) => trial(page, mode, { newLayoutDesigns: true, reviewPane }), + testInfo, + ), + ) + } + } + } + report({ results, summary: summarizeReviewPane(results) }, { runs, reviewDiffs: createReviewDiffs().length }) + }, +) + +async function trial( + page: Page, + mode: "cold" | "hot", + options?: { newLayoutDesigns?: boolean; reviewPane?: "closed" | "open" }, +) { + const reviewDiffs = options?.newLayoutDesigns ? createReviewDiffs() : undefined + await mockStressTimeline(page, { vcsDiff: reviewDiffs }) + if (options?.newLayoutDesigns) await installTimelineSettings(page) + await installStressSessionTabs(page) + if (mode === "hot") { + await page.goto(stressSessionHref(fixture.targetID)) + await expectSessionTitle(page, fixture.expected.targetTitle) + await waitForStableTimeline(page, fixture.expected.targetMessageIDs.at(-1)!) + await switchSession(page, fixture.sourceID, fixture.expected.sourceTitle) + } else { + await page.goto(stressSessionHref(fixture.sourceID)) + await expectSessionTitle(page, fixture.expected.sourceTitle) + } + await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) + if (options?.reviewPane === "open") { + await openReviewPane(page) + await waitForStableTimeline(page, fixture.expected.sourceMessageIDs.at(-1)!) + } + + const destinationIDs = fixture.messages[fixture.targetID].map((message) => message.info.id) + const sourceIDs = fixture.messages[fixture.sourceID].map((message) => message.info.id) + const lastID = fixture.expected.targetMessageIDs.at(-1)! + const href = stressSessionHref(fixture.targetID) + const result = await measureSessionSwitch(page, { + destinationIDs, + sourceIDs, + lastID, + href, + switch: () => switchSession(page, fixture.targetID, fixture.expected.targetTitle), + }) + return result +} + +function summarize(results: Record<"cold" | "hot", Result[]>) { + const stats = (values: (number | null)[]) => { + const sorted = values.filter((value): value is number => value !== null).sort((a, b) => a - b) + return { + min: sorted[0] ?? null, + median: sorted[Math.floor(sorted.length / 2)] ?? null, + max: sorted.at(-1) ?? null, + missing: values.length - sorted.length, + } + } + return Object.fromEntries( + Object.entries(results).map(([mode, values]) => [ + mode, + { + firstDestinationObservedMs: stats(values.map((value) => value.firstDestinationObservedMs)), + firstCorrectObservedMs: stats(values.map((value) => value.firstCorrectObservedMs)), + stableObservedMs: stats(values.map((value) => value.stableObservedMs)), + }, + ]), + ) +} + +function summarizeReviewPane(results: Record<"closed" | "open", Record<"cold" | "hot", Result[]>>) { + return Object.fromEntries( + Object.entries(results).map(([reviewPane, values]) => [ + reviewPane, + summarize(values as Record<"cold" | "hot", Result[]>), + ]), + ) +} + +async function switchSession(page: Page, sessionID: string, title: string) { + const href = stressSessionHref(sessionID) + const tab = page.locator(`[data-slot="titlebar-tabs"] a[href="${href}"]`).first() + await expect(tab).toBeVisible() + await tab.click() + await expectSessionTitle(page, title) +} + +async function openReviewPane(page: Page) { + await page.getByRole("button", { name: "Toggle review" }).click() + const panel = page.locator("#review-panel") + await expect(panel).toBeVisible() + // Text-based readiness works across review implementations; the legacy list mounts + // diff viewers lazily while V2 mounts the active preview eagerly. + await page.waitForFunction(() => { + const panel = document.querySelector("#review-panel") + const text = panel?.textContent ?? "" + return text.includes("generated-000.ts") && text.includes("+3") + }) +} diff --git a/packages/app/e2e/performance/timeline/session-tab-switch-metrics.ts b/packages/app/e2e/performance/timeline/session-tab-switch-metrics.ts new file mode 100644 index 0000000000000000000000000000000000000000..5298b9b0b7dfd1254f1d04d4d90c73edc5e5bb44 --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-tab-switch-metrics.ts @@ -0,0 +1,59 @@ +export type SessionSwitchSample = { + observedAtMs: number + destination: string[] + source: string[] + hasVisibleRows: boolean + last: boolean + requiredPartVisible?: boolean + bottomAnchorRequired?: boolean + bottomErrorPx?: number + review?: { + fileHost: boolean + fileHostReplaced: boolean + header: string + replacedLevels: string[] + } +} + +export function classifySessionSwitch(samples: SessionSwitchSample[]) { + const firstDestination = samples.findIndex((sample) => sample.destination.length > 0) + const firstCorrect = samples.findIndex(isCorrectDestination) + const stable = samples.findIndex((_, index) => isStableSessionSwitch(samples.slice(index, index + 3))) + return { + firstDestinationObservedMs: samples[firstDestination]?.observedAtMs ?? null, + firstCorrectObservedMs: samples[firstCorrect]?.observedAtMs ?? null, + stableObservedMs: samples[stable + 2]?.observedAtMs ?? null, + wrongDestinationSamples: samples + .slice(firstDestination) + .filter((sample) => sample.destination.length > 0 && !sample.last).length, + blankSamples: samples.filter((sample) => !sample.hasVisibleRows).length, + unknownSamples: samples.filter( + (sample) => sample.hasVisibleRows && sample.destination.length === 0 && sample.source.length === 0, + ).length, + sourceSamples: samples.filter((sample) => sample.source.length > 0).length, + reviewFileHostMissingSamples: samples.filter((sample) => sample.review && !sample.review.fileHost).length, + reviewFileHostReplacedSamples: samples.filter((sample) => sample.review?.fileHostReplaced).length, + reviewHeaders: [...new Set(samples.flatMap((sample) => (sample.review ? [sample.review.header] : [])))], + reviewReplacedLevels: [...new Set(samples.flatMap((sample) => sample.review?.replacedLevels ?? []))], + } +} + +export function isCorrectDestination(sample: SessionSwitchSample) { + return ( + sample.destination.length > 0 && + sample.source.length === 0 && + sample.last && + sample.requiredPartVisible !== false && + (sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1) + ) +} + +export function isStableSessionSwitch(samples: SessionSwitchSample[]) { + return samples.length === 3 && samples.every(isCorrectDestination) +} + +export function isStableDestination(samples: Pick[]) { + return ( + samples.length === 3 && samples.every((sample) => sample.last && Math.abs(sample.bottomErrorPx ?? Infinity) <= 1) + ) +} diff --git a/packages/app/e2e/performance/timeline/session-tab-switch-probe.ts b/packages/app/e2e/performance/timeline/session-tab-switch-probe.ts new file mode 100644 index 0000000000000000000000000000000000000000..955da80367d7bf3307864f758c8b1e0de94588f4 --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-tab-switch-probe.ts @@ -0,0 +1,226 @@ +import { expect, type Page } from "@playwright/test" +import { classifySessionSwitch, isStableDestination, type SessionSwitchSample } from "./session-tab-switch-metrics" + +type SessionSwitchProbe = { + samples: SessionSwitchSample[] + stop: () => void +} + +async function installSessionSwitchProbe( + page: Page, + input: { + destinationIDs: string[] + sourceIDs: string[] + lastID: string + requiredPartID?: string + requireBottomAnchor?: boolean + href: string + }, +) { + await page.evaluate(({ destinationIDs, sourceIDs, lastID, requiredPartID, requireBottomAnchor, href }) => { + const destination = new Set(destinationIDs) + const source = new Set(sourceIDs) + const samples: SessionSwitchSample[] = [] + let started: number | undefined + let running = true + const reviewLevels: Record = { + panel: "#review-panel", + tabs: '#review-panel [data-component="tabs"]', + body: '#review-panel [data-slot="session-review-v2-body"]', + review: '#review-panel [data-component="session-review-v2"]', + preview: '#review-panel [data-slot="session-review-v2-preview"]', + scroll: '#review-panel [data-slot="session-review-v2-diff-scroll"]', + file: '#review-panel [data-component="file"][data-mode="diff"]', + } + const initialReviewNodes: Record = {} + const sample = () => { + if (!running || started === undefined) return + setTimeout(() => { + if (!running || started === undefined) return + const observedAtMs = performance.now() - started + const reviewPanel = document.querySelector("#review-panel") + const reviewFile = reviewPanel?.querySelector('[data-component="file"][data-mode="diff"]') + const initialReviewFile = initialReviewNodes.file + const replacedLevels = Object.entries(reviewLevels).flatMap(([name, selector]) => { + const initial = initialReviewNodes[name] + if (!initial) return [] + const current = document.querySelector(selector) + return current && current !== initial ? [name] : [] + }) + const review = reviewPanel + ? { + fileHost: !!reviewFile, + fileHostReplaced: !!initialReviewFile && !!reviewFile && reviewFile !== initialReviewFile, + header: + reviewPanel + .querySelector('[data-slot="session-review-v2-file-header"]') + ?.textContent?.trim() ?? "", + replacedLevels, + } + : undefined + const root = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector("[data-timeline-row]"), + ) + if (root) { + const view = root.getBoundingClientRect() + const visible = [...root.querySelectorAll("[data-message-id]")] + .filter((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + .map((element) => element.dataset.messageId!) + const hasVisibleRows = [...root.querySelectorAll("[data-timeline-key]")].some((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + const requiredPartVisible = requiredPartID + ? [...root.querySelectorAll("[data-timeline-part-id]")].some((element) => { + if (element.dataset.timelinePartId !== requiredPartID) return false + const rect = element.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 && rect.bottom > view.top && rect.top < view.bottom + }) + : undefined + const spacer = root.querySelector('[data-timeline-row="bottom-spacer"]')?.getBoundingClientRect() + samples.push({ + observedAtMs, + destination: visible.filter((id) => destination.has(id)), + source: visible.filter((id) => source.has(id)), + hasVisibleRows, + last: visible.includes(lastID), + requiredPartVisible, + bottomAnchorRequired: requireBottomAnchor !== false, + bottomErrorPx: spacer ? spacer.bottom - view.bottom : undefined, + review, + }) + } else { + samples.push({ + observedAtMs, + destination: [], + source: [], + hasVisibleRows: false, + last: false, + requiredPartVisible: requiredPartID ? false : undefined, + bottomAnchorRequired: requireBottomAnchor !== false, + review, + }) + } + requestAnimationFrame(sample) + }, 0) + } + document.addEventListener( + "click", + (event) => { + const link = event.target instanceof Element ? event.target.closest("a") : undefined + if (link?.getAttribute("href") !== href) return + started = performance.now() + for (const [name, selector] of Object.entries(reviewLevels)) { + initialReviewNodes[name] = document.querySelector(selector) + } + requestAnimationFrame(sample) + }, + { capture: true, once: true }, + ) + ;(window as Window & { __sessionSwitchProbe?: SessionSwitchProbe }).__sessionSwitchProbe = { + samples, + stop: () => { + running = false + }, + } + }, input) +} + +async function waitForStableSessionSwitch(page: Page) { + await page.waitForFunction(() => { + const samples = (window as Window & { __sessionSwitchProbe?: SessionSwitchProbe }).__sessionSwitchProbe?.samples + if (!samples) return false + return samples.some((_, index) => { + const stable = samples.slice(index, index + 3) + return ( + stable.length === 3 && + stable.every( + (sample) => + sample.destination.length > 0 && + sample.source.length === 0 && + sample.last && + sample.requiredPartVisible !== false && + (sample.bottomAnchorRequired === false || Math.abs(sample.bottomErrorPx ?? Infinity) <= 1), + ) + ) + }) + }) +} + +async function collectSessionSwitchResult(page: Page) { + const samples = await page.evaluate(() => { + const probe = (window as Window & { __sessionSwitchProbe?: SessionSwitchProbe }).__sessionSwitchProbe! + probe.stop() + return probe.samples + }) + return classifySessionSwitch(samples) +} + +export async function measureSessionSwitch( + page: Page, + input: { + destinationIDs: string[] + sourceIDs: string[] + lastID: string + requiredPartID?: string + requireBottomAnchor?: boolean + href: string + switch: () => Promise + }, +) { + const { switch: run, ...probe } = input + await installSessionSwitchProbe(page, probe) + try { + await run() + await waitForStableSessionSwitch(page) + return await collectSessionSwitchResult(page) + } finally { + await page.evaluate(() => { + ;(window as Window & { __sessionSwitchProbe?: SessionSwitchProbe }).__sessionSwitchProbe?.stop() + }) + } +} + +export async function waitForStableTimeline(page: Page, lastID: string) { + const samples: Pick[] = [] + await expect + .poll( + async () => { + samples.push( + await page.evaluate( + (lastID) => + new Promise>((resolve) => { + requestAnimationFrame(() => + setTimeout(() => { + const root = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector("[data-timeline-row]"), + ) + if (!root) { + resolve({ last: false }) + return + } + const view = root.getBoundingClientRect() + const last = [...root.querySelectorAll("[data-message-id]")].some((element) => { + if (element.dataset.messageId !== lastID) return false + const rect = element.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + const spacer = root + .querySelector('[data-timeline-row="bottom-spacer"]') + ?.getBoundingClientRect() + resolve({ last, bottomErrorPx: spacer ? spacer.bottom - view.bottom : undefined }) + }, 0), + ) + }), + lastID, + ), + ) + return isStableDestination(samples.slice(-3)) + }, + { timeout: 30_000, intervals: [0] }, + ) + .toBe(true) +} diff --git a/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts b/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..a86a55cff25247abad6f1684aa32d9da31c4e2d6 --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-timeline-benchmark.fixture.ts @@ -0,0 +1,504 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import type { Page } from "@playwright/test" +import { mockOpenCodeServer } from "../../utils/mock-server" +import { expectAppVisible, expectSessionTitle } from "../../utils/waits" +import { expect } from "../benchmark" + +const directory = "C:/OpenCode/TimelineStateRegression" +const projectID = "proj_timeline_state_regression" +const sessionID = "ses_timeline_state_regression" +const userMessageID = "msg_user_regression" +const assistantMessageID = "msg_assistant_regression" +const editPartID = "prt_0001_edit" +export const textPartID = "prt_9999_text" +const title = "Timeline collapse state regression" +const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } + +type EventPayload = { + directory: string + payload: Record +} + +const userMessage = { + info: { + id: userMessageID, + sessionID, + role: "user", + time: { created: 1700000000000 }, + summary: { diffs: [] }, + agent: "build", + model, + }, + parts: [ + { + id: "prt_user_text", + sessionID, + messageID: userMessageID, + type: "text", + text: "Please edit the file.", + }, + ], +} + +const editPart = { + id: editPartID, + sessionID, + messageID: assistantMessageID, + type: "tool", + callID: "call_edit_regression", + tool: "edit", + state: { + status: "completed", + input: { filePath: "src/regression.ts" }, + output: "Edited src/regression.ts", + title: "src/regression.ts", + metadata: { + filediff: { + file: "src/regression.ts", + additions: 1, + deletions: 1, + before: "export const value = 'before'\n", + after: "export const value = 'after'\n", + }, + diff: "diff --git a/src/regression.ts b/src/regression.ts\n-export const value = 'before'\n+export const value = 'after'\n", + }, + time: { start: 1700000001000, end: 1700000002000 }, + }, +} + +const streamedTextPart = { + id: textPartID, + sessionID, + messageID: assistantMessageID, + type: "text", + text: "Streaming added a later assistant text part.", +} + +const assistantMessage = { + info: { + id: assistantMessageID, + sessionID, + role: "assistant", + time: { created: 1700000001000 }, + parentID: userMessageID, + modelID: model.modelID, + providerID: model.providerID, + mode: "build", + agent: "build", + path: { cwd: directory, root: directory }, + cost: 0.01, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + variant: "max", + }, + parts: [editPart], +} + +export async function setupTimelineBenchmark( + page: Page, + options: { + historyTurns: number + eventBatch: number + newLayoutDesigns?: boolean + vcsDiff?: unknown[] + turnDiffs?: unknown[] + }, +) { + const events: EventPayload[] = [] + let eventBatch = options.eventBatch + const currentUserMessage = options.turnDiffs + ? { ...userMessage, info: { ...userMessage.info, summary: { diffs: options.turnDiffs } } } + : userMessage + await mockOpenCodeServer(page, { + directory, + project: project(), + provider: provider(), + sessions: [session()], + vcsDiff: options.vcsDiff, + pageMessages: () => ({ + items: [ + ...Array.from({ length: options.historyTurns }, (_, index) => performanceTurn(index)).flat(), + currentUserMessage, + assistantMessage, + ], + }), + events: () => events.splice(0, eventBatch), + eventRetry: 16, + }) + await page.addInitScript( + (input) => { + localStorage.setItem( + "settings.v3", + JSON.stringify({ + general: { + newLayoutDesigns: input.newLayoutDesigns, + editToolPartsExpanded: true, + shellToolPartsExpanded: true, + showReasoningSummaries: true, + }, + }), + ) + }, + { newLayoutDesigns: options.newLayoutDesigns ?? false }, + ) + await page.setViewportSize({ width: 1366, height: 768 }) + const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) + const text = page.locator(`[data-timeline-part-id="${textPartID}"]`).first() + await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) + await expectSessionTitle(page, title) + await expectAppVisible(scroller) + return { + scroller, + text, + transport: { + enqueue(payload: EventPayload | EventPayload[]) { + events.push(...(Array.isArray(payload) ? payload : [payload])) + }, + pendingCount() { + return events.length + }, + releaseAll() { + eventBatch = events.length + }, + }, + async scrollToBottom() { + await scroller.evaluate((element) => { + element.scrollTop = element.scrollHeight + }) + }, + async waitForStableGeometry() { + await expect + .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight - element.scrollTop)) + .toBeLessThanOrEqual(1) + await page.waitForFunction((partID) => { + const root = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector(`[data-timeline-part-id="${partID}"]`), + ) + if (!root) return false + return new Promise((resolve) => { + const height = root.scrollHeight + requestAnimationFrame(() => + requestAnimationFrame(() => + resolve(root.scrollHeight === height && root.scrollHeight - root.clientHeight - root.scrollTop <= 1), + ), + ) + }) + }, textPartID) + }, + } +} + +export function buildInitialStreamEvent(deltaCount: number): EventPayload { + return { + directory, + payload: { + type: "message.part.updated", + properties: { + part: { + ...streamedTextPart, + text: `Streaming${streamChunk(0, deltaCount + 1)}\n\n\`\`\`ts\nconst initial = true\n\`\`\``, + }, + }, + }, + } +} + +export function buildStreamDeltaEvents(deltaCount: number): EventPayload[] { + return Array.from({ length: deltaCount }, (_, index) => ({ + directory, + payload: { + type: "message.part.delta", + properties: { + messageID: assistantMessageID, + partID: textPartID, + field: "text", + delta: streamChunk(index + 1, deltaCount + 1), + }, + }, + })) +} + +function performanceTurn(index: number) { + const suffix = String(index).padStart(4, "0") + const userID = `msg_0000_${suffix}_a_user` + const assistantID = `msg_0000_${suffix}_b_assistant` + const before = historicalSource(index, false) + const after = historicalSource(index, true) + const parts = [ + ...(index % 5 === 0 + ? [ + { + id: `prt_0000_${suffix}_reasoning`, + sessionID, + messageID: assistantID, + type: "reasoning", + text: `Reviewing the existing implementation. ${"constraint analysis ".repeat(20)}`, + time: { start: 1690000001000 + index * 2_000, end: 1690000001200 + index * 2_000 }, + }, + ] + : []), + { + id: `prt_0000_${suffix}_assistant`, + sessionID, + messageID: assistantID, + type: "text", + text: historicalMarkdown(index), + }, + ...(index % 8 === 0 + ? [ + { + id: `prt_0000_${suffix}_edit`, + sessionID, + messageID: assistantID, + type: "tool", + callID: `call_0000_${suffix}_edit`, + tool: "edit", + state: { + status: "completed", + input: { filePath: `src/history-${index}.ts` }, + output: `Edited src/history-${index}.ts`, + title: `src/history-${index}.ts`, + metadata: { + filediff: { file: `src/history-${index}.ts`, additions: 48, deletions: 48, before, after }, + }, + time: { start: 1690000001200 + index * 2_000, end: 1690000001400 + index * 2_000 }, + }, + }, + ] + : []), + ...(index % 12 === 0 + ? [ + { + id: `prt_0000_${suffix}_write`, + sessionID, + messageID: assistantID, + type: "tool", + callID: `call_0000_${suffix}_write`, + tool: "write", + state: { + status: "completed", + input: { filePath: `src/generated-${index}.tsx`, content: after }, + output: `Wrote src/generated-${index}.tsx`, + title: `src/generated-${index}.tsx`, + metadata: { + filediff: { file: `src/generated-${index}.tsx`, additions: 32, deletions: 0, before: "", after }, + }, + time: { start: 1690000001400 + index * 2_000, end: 1690000001500 + index * 2_000 }, + }, + }, + ] + : []), + ...(index % 16 === 0 + ? [ + { + id: `prt_0000_${suffix}_patch`, + sessionID, + messageID: assistantID, + type: "tool", + callID: `call_0000_${suffix}_patch`, + tool: "apply_patch", + state: { + status: "completed", + input: { patchText: realisticPatch(index) }, + output: "Success. Updated src/components/SessionCard.tsx", + title: "src/components/SessionCard.tsx", + metadata: { + files: [ + { + filePath: "src/components/SessionCard.tsx", + relativePath: "src/components/SessionCard.tsx", + type: "update", + additions: 8, + deletions: 3, + patch: realisticPatch(index), + before, + after, + }, + ], + }, + time: { start: 1690000001500 + index * 2_000, end: 1690000001700 + index * 2_000 }, + }, + }, + ] + : []), + ] + return [ + { + info: { + id: userID, + sessionID, + role: "user", + time: { created: 1690000000000 + index * 2_000 }, + summary: { diffs: [] }, + agent: "build", + model, + }, + parts: [ + { + id: `prt_0000_${suffix}_user`, + sessionID, + messageID: userID, + type: "text", + text: `Historical prompt ${index}`, + }, + ], + }, + { + info: { + id: assistantID, + sessionID, + role: "assistant", + time: { created: 1690000001000 + index * 2_000, completed: 1690000001500 + index * 2_000 }, + parentID: userID, + modelID: model.modelID, + providerID: model.providerID, + mode: "build", + agent: "build", + path: { cwd: directory, root: directory }, + cost: 0.01, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + variant: "max", + finish: "stop", + }, + parts, + }, + ] +} + +function historicalMarkdown(index: number) { + const code = `import { For, Show, createSignal } from "solid-js" + +type SessionRow = { id: string; title: string; active: boolean } + +export function SessionList(props: { rows: SessionRow[] }) { + const [selected, setSelected] = createSignal() + return ( +
+ {(row) => ( + + )} +
+ ) +}` + return `## Session renderer review ${index} + +The active session keeps **semantic row identity** while reconciling measured content. See [Solid documentation](https://docs.solidjs.com/) and the inline \`measureElement(node)\` call. + +| Concern | Current behavior | Verification | +| --- | --- | --- | +| streaming | appends Markdown blocks | painted frames | +| geometry | anchors visible rows | DOM coordinates | +| tools | preserves expanded state | keyed remount probe | + +> Long sessions combine Markdown, syntax highlighting, tool output, and asynchronously rendered diffs. + +${index % 4 === 0 ? `\`\`\`tsx\n${code}\n\`\`\`\n\n\`\`\`bash\nbun typecheck\nbun test --preload ./happydom.ts ./src/pages/session\ngit diff --check\n\`\`\`` : "- preserve the viewport anchor\n- avoid replacing stable Markdown nodes\n- process provider deltas without blocking input"}` +} + +function historicalSource(index: number, updated: boolean) { + const method = updated ? "toLocaleUpperCase(props.locale)" : "toUpperCase()" + const limit = updated ? 24 : 20 + return `import { createMemo, For } from "solid-js" + +type Message = { + id: string + role: "user" | "assistant" + text: string + tokens: { input: number; output: number } +} + +export function MessageSummary(props: { messages: Message[]; locale: string }) { + const visible = createMemo(() => props.messages.filter((message) => message.text.trim()).slice(-${limit})) + const total = createMemo(() => visible().reduce((sum, message) => sum + message.tokens.output, 0)) + return ( +
+
{total().toLocaleString(props.locale)} output tokens
+ {(message) =>

{message.text.${method}}

}
+
+ ) +} +` +} + +function realisticPatch(index: number) { + return `*** Begin Patch +*** Update File: src/components/SessionCard.tsx +@@ +-const title = props.session.title.toUpperCase() +-const messages = props.messages.slice(-20) ++const title = props.session.title.toLocaleUpperCase(props.locale) ++const messages = props.messages.filter((message) => message.text.trim()).slice(-24) ++const outputTokens = messages.reduce((sum, message) => sum + message.tokens.output, 0) +@@ +-

{title}

++

{title}

++ {outputTokens.toLocaleString(props.locale)} output tokens +*** End Patch` +} + +export function streamChunk(index: number, count: number) { + if (index === 0) return `\n\n## Implementation plan\n\nStreaming **bold analysis` + if (index === count - 1) + return `\n\`\`\`\n\n## Verification\n\n- **Typecheck:** passed\n- **Timeline geometry:** stable\n- **Streaming output:** benchmark-complete ` + + const section = Math.floor(index / 18) + 1 + const fragments = [ + ` continues across three`, + ` or four word`, + ` provider deltas and`, + ` closes in this fragment**. \n\n`, + `| Concern | State`, + ` | Verification |\n|`, + ` --- | ---`, + ` | --- |\n|`, + ` markdown | incremental |`, + ` painted frames | \n\n`, + `\`\`\`tsx\nconst row: SessionRow`, + ` = rows[index] ??`, + ` fallback\nconst title =`, + ` row.title.toLocaleUpperCase(locale)\n`, + `const selected = createMemo(()`, + ` => row.id ===`, + ` activeID()) // stream-${index}\n`, + `// stream-${index}\n\`\`\`\n\n### Iteration ${section}\n\nStreaming **bold analysis`, + ] + return fragments[(index - 1) % fragments.length]! +} + +function project() { + return { + id: projectID, + worktree: directory, + vcs: "git", + name: "timeline-state-regression", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + } +} + +function session() { + return { + id: sessionID, + slug: "timeline-state-regression", + projectID, + directory, + title, + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + } +} + +function provider() { + return { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "claude-opus-4-6" }, + } +} diff --git a/packages/app/e2e/performance/timeline/session-timeline-benchmark.spec.ts b/packages/app/e2e/performance/timeline/session-timeline-benchmark.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a52bc8aa3ca80eba9cb4d4a96657e772e298571 --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-timeline-benchmark.spec.ts @@ -0,0 +1,306 @@ +import type { Page } from "@playwright/test" +import { benchmark, benchmarkDiagnostics, expect } from "../benchmark" +import { + buildInitialStreamEvent, + buildStreamDeltaEvents, + setupTimelineBenchmark, + textPartID, +} from "./session-timeline-benchmark.fixture" +import { startTimelineProfile } from "./session-timeline-profile" +import { createReviewDiffs } from "./timeline-test-helpers" +import { + collectTimelineStreamMetrics, + installTimelineStreamProbe, + startTimelineStreamProbe, +} from "./session-timeline-stream-probe" + +type TimelineStreamOptions = { + newLayoutDesigns?: boolean + reviewDiffs?: boolean + reviewPane?: boolean +} + +type ReviewPaneSample = { + observedAtMs: number + panelVisible: boolean + header: string + diffViewers: number + diffLines: number + codeBlocks: number + ready: boolean +} + +type ReviewPaneProbe = { + samples: ReviewPaneSample[] + start: () => void + stop: () => void +} + +const reviewReadyStreak = 3 + +benchmark.describe("performance: session timeline streaming", () => { + benchmark("streams assistant text without remounting or oscillating", async ({ page, report }) => { + benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000) + const result = await runTimelineStreamBenchmark(page, {}) + report(result.metrics, result.context) + }) + + benchmark("streams assistant text in v2 with review pane closed", async ({ page, report }) => { + benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000) + const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true }) + report(result.metrics, result.context) + }) + + benchmark("streams assistant text in v2 with review diffs and pane closed", async ({ page, report }) => { + benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000) + const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true, reviewDiffs: true }) + report(result.metrics, result.context) + }) + + benchmark("streams assistant text in v2 with review pane open", async ({ page, report }) => { + benchmark.setTimeout(Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + 60_000) + const result = await runTimelineStreamBenchmark(page, { newLayoutDesigns: true, reviewPane: true }) + report(result.metrics, result.context) + }) +}) + +benchmark.describe("performance: review pane", () => { + benchmark("loads v2 review diffs and switches active files", async ({ page, report }) => { + benchmark.setTimeout(240_000) + const historyTurns = Number(process.env.REVIEW_PANE_HISTORY_TURNS ?? 72) + const diffs = createReviewDiffs() + const fixture = await setupTimelineBenchmark(page, { + historyTurns, + eventBatch: 1, + newLayoutDesigns: true, + vcsDiff: diffs, + }) + + fixture.transport.enqueue(buildInitialStreamEvent(1)) + await expect(fixture.text).toBeVisible() + await expect(fixture.text).toContainText("Implementation plan") + await fixture.scrollToBottom() + await fixture.waitForStableGeometry() + + const open = await measureReviewPaneLoad(page, diffs[0]!.file) + const switches = [] + for (const diff of diffs.slice(1, 4)) switches.push(await measureReviewNextFile(page, diff.file)) + + report( + { + open, + switches, + }, + { + historyTurns, + reviewDiffs: diffs.length, + }, + ) + }) +}) + +async function runTimelineStreamBenchmark(page: Page, options: TimelineStreamOptions) { + const completionTimeoutMs = Number(process.env.TIMELINE_COMPLETION_TIMEOUT_MS ?? 420_000) + const cpuThrottle = Number(process.env.TIMELINE_CPU_THROTTLE ?? 30) + const deltaCount = Number(process.env.TIMELINE_DELTA_COUNT ?? 160) + const historyTurns = Number(process.env.TIMELINE_HISTORY_TURNS ?? 320) + const eventBatch = Number(process.env.TIMELINE_EVENT_BATCH ?? 1) + const minimal = process.env.TIMELINE_MINIMAL === "1" + const profileCPU = process.env.TIMELINE_CPU_PROFILE === "1" + const profileVisual = !minimal && profileCPU && process.env.TIMELINE_VISUAL_PROFILE !== "0" + const diffs = options.reviewDiffs || options.reviewPane ? createReviewDiffs() : undefined + const fixture = await setupTimelineBenchmark(page, { + historyTurns, + eventBatch, + newLayoutDesigns: options.newLayoutDesigns, + // Turn diffs exercise timeline data cost; the pane-open scenario serves the same + // diffs through the default git mode so it works across review implementations. + turnDiffs: options.reviewDiffs ? diffs : undefined, + vcsDiff: options.reviewPane ? diffs : undefined, + }) + + fixture.transport.enqueue(buildInitialStreamEvent(deltaCount)) + const contentStart = performance.now() + await expect(fixture.text).toBeVisible() + await expect(fixture.text).toContainText("Implementation plan") + const initialContentObservedMs = performance.now() - contentStart + await fixture.scrollToBottom() + await fixture.waitForStableGeometry() + + const reviewPane = options.reviewPane && diffs ? await measureReviewPaneLoad(page, diffs[0]!.file) : undefined + if (reviewPane) await fixture.waitForStableGeometry() + + const profile = await startTimelineProfile(page, { cpuThrottle, profileCPU }) + await installTimelineStreamProbe(page, { textPartID, finalIndex: deltaCount, profileVisual, minimal }) + const deltas = buildStreamDeltaEvents(deltaCount) + await startTimelineStreamProbe(page) + fixture.transport.enqueue(deltas) + + await page.waitForFunction( + (finalIndex) => + ( + window as Window & { + __timelineStreamBenchmark?: { applied: { index: number }[] } + } + ).__timelineStreamBenchmark?.applied.some((value) => value.index === finalIndex), + deltaCount, + { timeout: completionTimeoutMs }, + ) + await expect(fixture.text).toContainText("benchmark-complete") + await expect(fixture.text).toContainText("Streaming") + await fixture.waitForStableGeometry() + const metrics = await collectTimelineStreamMetrics(page, { + textPartID, + finalIndex: deltaCount, + navigations: benchmarkDiagnostics(page).navigations, + }) + const delivered = deltas.length - fixture.transport.pendingCount() + await profile.stop() + + const result = { + metrics: { + endToEndInitialContentObservedMs: initialContentObservedMs, + ...metrics, + deliveredDeltas: delivered, + pendingDeltas: fixture.transport.pendingCount(), + reviewPane: reviewPane ?? null, + }, + context: { + cpuThrottle, + profileCPU, + profileVisual, + minimal, + queuedDeltas: deltas.length, + historyTurns, + eventBatch, + newLayoutDesigns: options.newLayoutDesigns === true, + reviewPane: options.reviewPane === true ? "open" : "closed", + reviewDiffs: diffs?.length ?? 0, + }, + } + + await profile.reset() + return result +} + +async function measureReviewPaneLoad(page: Page, file: string) { + // Default git mode reads the mocked /vcs/diff data, so opening the pane is enough + // and the flow works across review pane implementations. + await installReviewPaneProbe(page, { file }) + await startReviewPaneProbe(page) + await page.getByRole("button", { name: "Toggle review" }).click() + await expect(page.locator("#review-panel")).toBeVisible() + return collectReviewPaneProbe(page) +} + +async function measureReviewNextFile(page: Page, file: string) { + await installReviewPaneProbe(page, { file }) + await startReviewPaneProbe(page) + await page.getByRole("button", { name: "Next file" }).click() + return collectReviewPaneProbe(page) +} + +async function installReviewPaneProbe(page: Page, input: { file: string }) { + await page.evaluate((input) => { + const samples: ReviewPaneSample[] = [] + const basename = input.file.split(/[\\/]/).at(-1) ?? input.file + let started: number | undefined + let running = true + + const paneState = () => { + const panel = document.querySelector("#review-panel") + const review = panel?.querySelector('[data-component="session-review-v2"]') + const rect = (review ?? panel)?.getBoundingClientRect() + const text = panel?.textContent ?? "" + const previewHeader = panel?.querySelector( + '[data-slot="session-review-v2-file-header"]', + )?.textContent + const header = previewHeader ?? text + const viewers = panel ? [...panel.querySelectorAll('[data-component="file"][data-mode="diff"]')] : [] + const codeBlocks = panel?.querySelectorAll("code").length ?? 0 + const diffLines = viewers.reduce( + (sum, viewer) => + sum + + (viewer.shadowRoot?.querySelectorAll("[data-line]").length ?? viewer.querySelectorAll("[data-line]").length), + 0, + ) + const panelVisible = + !!panel && panel.getAttribute("aria-hidden") !== "true" && !!rect && rect.width > 0 && rect.height > 0 + return { + panelVisible, + header: header.slice(0, 500), + diffViewers: viewers.length, + diffLines, + codeBlocks, + ready: + panelVisible && + header.includes(basename) && + (viewers.length > 0 || text.includes("+3") || diffLines > 0 || codeBlocks > 0), + } + } + + const sample = () => { + if (!running || started === undefined) return + requestAnimationFrame(() => { + setTimeout(() => { + if (!running || started === undefined) return + samples.push({ observedAtMs: performance.now() - started, ...paneState() }) + if (performance.now() - started < 10_000) sample() + }, 0) + }) + } + + ;(window as Window & { __reviewPaneProbe?: ReviewPaneProbe }).__reviewPaneProbe = { + samples, + start: () => { + started = performance.now() + performance.mark("opencode.review-pane.click") + sample() + }, + stop: () => { + running = false + }, + } + }, input) +} + +async function startReviewPaneProbe(page: Page) { + await page.evaluate(() => { + ;(window as Window & { __reviewPaneProbe?: ReviewPaneProbe }).__reviewPaneProbe!.start() + }) +} + +async function collectReviewPaneProbe(page: Page) { + await page.waitForFunction((streak) => { + const samples = (window as Window & { __reviewPaneProbe?: ReviewPaneProbe }).__reviewPaneProbe?.samples + if (!samples) return false + return samples.some((_, index) => { + const stable = samples.slice(index, index + streak) + return stable.length === streak && stable.every((sample) => sample.ready) + }) + }, reviewReadyStreak) + + const samples = await page.evaluate(() => { + const probe = (window as Window & { __reviewPaneProbe?: ReviewPaneProbe }).__reviewPaneProbe! + probe.stop() + return probe.samples + }) + return { summary: summarizeReviewPaneSamples(samples), samples } +} + +function summarizeReviewPaneSamples(samples: ReviewPaneSample[]) { + const firstReady = samples.find((sample) => sample.ready) + const stableIndex = samples.findIndex((_, index) => { + const stable = samples.slice(index, index + reviewReadyStreak) + return stable.length === reviewReadyStreak && stable.every((sample) => sample.ready) + }) + return { + samples: samples.length, + firstReadyObservedMs: firstReady?.observedAtMs ?? null, + stableReadyObservedMs: stableIndex === -1 ? null : samples[stableIndex + reviewReadyStreak - 1]!.observedAtMs, + notReadySamples: samples.filter((sample) => !sample.ready).length, + maxDiffViewers: Math.max(0, ...samples.map((sample) => sample.diffViewers)), + maxDiffLines: Math.max(0, ...samples.map((sample) => sample.diffLines)), + maxCodeBlocks: Math.max(0, ...samples.map((sample) => sample.codeBlocks)), + } +} diff --git a/packages/app/e2e/performance/timeline/session-timeline-profile.ts b/packages/app/e2e/performance/timeline/session-timeline-profile.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1689498c1962dab0e31a3e18a7e9429979ef03c --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-timeline-profile.ts @@ -0,0 +1,40 @@ +import type { CDPSession, Page } from "@playwright/test" + +export async function startTimelineProfile(page: Page, options: { cpuThrottle: number; profileCPU: boolean }) { + const cdp = await page.context().newCDPSession(page) + if (options.cpuThrottle > 1) await cdp.send("Emulation.setCPUThrottlingRate", { rate: options.cpuThrottle }) + if (options.profileCPU) { + await cdp.send("Profiler.enable") + await cdp.send("Profiler.setSamplingInterval", { interval: 100 }) + await cdp.send("Profiler.start") + } + return { + async stop() { + if (!options.profileCPU) return + const result = await cdp.send("Profiler.stop") + const self = new Map() + result.profile.samples?.forEach((id, index) => { + const duration = (result.profile.timeDeltas?.[index] ?? 0) / 1_000 + self.set(id, (self.get(id) ?? 0) + duration) + }) + console.log( + "timeline cpu profile", + JSON.stringify( + result.profile.nodes + .map((node) => ({ + function: node.callFrame.functionName || "(anonymous)", + url: node.callFrame.url, + line: node.callFrame.lineNumber + 1, + selfMs: self.get(node.id) ?? 0, + })) + .filter((node) => node.selfMs > 1) + .sort((a, b) => b.selfMs - a.selfMs) + .slice(0, 40), + ), + ) + }, + async reset() { + if (options.cpuThrottle > 1) await cdp.send("Emulation.setCPUThrottlingRate", { rate: 1 }) + }, + } +} diff --git a/packages/app/e2e/performance/timeline/session-timeline-stream-probe.ts b/packages/app/e2e/performance/timeline/session-timeline-stream-probe.ts new file mode 100644 index 0000000000000000000000000000000000000000..a3cd698cde4fa75c7b5fca37eb1114612b07d0ff --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-timeline-stream-probe.ts @@ -0,0 +1,547 @@ +import type { Page } from "@playwright/test" + +const STREAM_MARKER_PATTERN = "stream-(\\d+)" +const STREAM_FRAGMENT_COUNT = 18 + +type TimelineProbeState = { + started: number + ended: number + profileVisual: boolean + minimal: boolean + frames: number[] + frameAt: number[] + applied: { at: number; index: number }[] + geometry: { + scrollTop: number + scrollHeight: number + clientHeight: number + distance: number + virtualHeight: number + headerHeight: number + }[] + blanks: number + longTasks: number[] + layoutShifts: number[] + visibleMounts: number + visibleUnmounts: number + visibleRows: Set + visibleSubtreeMounts: string[] + visibleSubtreeUnmounts: string[] + visibleSubtreeReplacements: number + visibleSubtreeDropouts: string[] + visibleSubtrees: Map + subtreeKeys: WeakMap + maxOverlap: number + maxGap: number + maxPartTopMovement: number + previousPartTop: number + slowFrames: { + duration: number + index: number + phase: "stream" | "boundary" | "complete" | "unknown" + tokenSpans: number + blocks: number + codeBlocks: number + height: number + distance: number + }[] + scroll: { + calls: number + callNoops: number + sameFrameCalls: number + assignments: number + assignmentNoops: number + lastCallFrame: number + frame: number + } + row: HTMLElement + markdown: HTMLElement + running: boolean + previous: number + cleanup: () => void + start: () => void +} + +export async function installTimelineStreamProbe( + page: Page, + options: { textPartID: string; finalIndex: number; profileVisual: boolean; minimal: boolean }, +) { + await page.evaluate( + ({ textPartID, finalIndex, profileVisual, minimal, markerPattern, fragmentCount }) => { + const part = document.querySelector(`[data-timeline-part-id="${textPartID}"]`) + const row = part?.closest("[data-timeline-row]") + const markdown = part?.querySelector('[data-component="markdown"]') + const root = part?.closest(".scroll-view__viewport") + if (!part || !row || !markdown || !root) throw new Error("missing streaming benchmark nodes") + const viewport = root.getBoundingClientRect() + const state: TimelineProbeState = { + started: 0, + ended: Infinity, + profileVisual, + minimal, + frames: [], + frameAt: [], + applied: [], + geometry: [], + blanks: 0, + longTasks: [], + layoutShifts: [], + visibleMounts: 0, + visibleUnmounts: 0, + visibleRows: new Set( + [...root.querySelectorAll("[data-timeline-key]")].filter((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > viewport.top && rect.top < viewport.bottom + }), + ), + visibleSubtreeMounts: [], + visibleSubtreeUnmounts: [], + visibleSubtreeReplacements: 0, + visibleSubtreeDropouts: [], + visibleSubtrees: new Map(), + subtreeKeys: new WeakMap(), + maxOverlap: 0, + maxGap: 0, + maxPartTopMovement: 0, + previousPartTop: part.getBoundingClientRect().top, + slowFrames: [], + scroll: { + calls: 0, + callNoops: 0, + sameFrameCalls: 0, + assignments: 0, + assignmentNoops: 0, + lastCallFrame: -1, + frame: 0, + }, + row, + markdown, + running: false, + previous: 0, + cleanup: () => {}, + start: () => {}, + } + ;(window as Window & { __timelineStreamBenchmark?: TimelineProbeState }).__timelineStreamBenchmark = state + const scrollTo = Element.prototype.scrollTo + const scrollTop = Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop")! + if (profileVisual) { + Element.prototype.scrollTo = function (...args) { + state.scroll.calls += 1 + const top = typeof args[0] === "object" ? args[0]?.top : args[1] + if (typeof top === "number") { + const target = Math.min(top, this.scrollHeight - this.clientHeight) + if (Math.abs(this.scrollTop - target) < 1) state.scroll.callNoops += 1 + } + if (state.scroll.lastCallFrame === state.scroll.frame) state.scroll.sameFrameCalls += 1 + state.scroll.lastCallFrame = state.scroll.frame + return scrollTo.apply(this, args) + } + Object.defineProperty(Element.prototype, "scrollTop", { + configurable: true, + get: scrollTop.get, + set(value) { + state.scroll.assignments += 1 + if (Math.abs(this.scrollTop - value) < 1) state.scroll.assignmentNoops += 1 + scrollTop.set!.call(this, value) + }, + }) + } + + const recordLongTasks = (entries: PerformanceEntry[]) => { + if (!state.running) return + state.longTasks.push( + ...entries + .filter((entry) => entry.startTime >= state.started && entry.startTime <= state.ended) + .map((entry) => entry.duration), + ) + } + const longTaskObserver = new PerformanceObserver((list) => recordLongTasks(list.getEntries())) + longTaskObserver.observe({ type: "longtask" }) + const recordLayoutShifts = (entries: PerformanceEntry[]) => { + if (!state.running) return + state.layoutShifts.push( + ...entries + .map((entry) => { + const shift = entry as LayoutShiftEntry + if (shift.startTime < state.started || shift.hadRecentInput) return + return shift.value + }) + .filter((value): value is number => value !== undefined), + ) + } + const layoutShiftObserver = profileVisual + ? new PerformanceObserver((list) => recordLayoutShifts(list.getEntries())) + : undefined + layoutShiftObserver?.observe({ type: "layout-shift", buffered: true }) + + const visible = (element: Element) => { + const rect = element.getBoundingClientRect() + const viewport = root.getBoundingClientRect() + const style = getComputedStyle(element) + return ( + element.isConnected && + rect.width > 0 && + rect.height > 0 && + rect.bottom > viewport.top && + rect.top < viewport.bottom && + style.display !== "none" && + style.visibility !== "hidden" && + Number(style.opacity) > 0 + ) + } + const critical = [ + "[data-timeline-part-id]", + '[data-component="edit-content"]', + '[data-component="apply-patch-file-diff"]', + '[data-component="file"]', + '[data-component="markdown-code"]', + "[data-markdown-block]", + ].join(",") + const describe = (element: Element) => { + const cached = state.subtreeKeys.get(element) + if (!element.isConnected && cached) return cached + const part = element.closest("[data-timeline-part-id]")?.dataset.timelinePartId ?? "unknown" + const block = element + .closest("[data-markdown-key]") + ?.dataset.markdownKey?.replace(/:(?:code|full|live)$/, "") + const component = + element.getAttribute("data-component") ?? element.getAttribute("data-markdown-block") ?? element.tagName + const key = `${part}:${block ?? "root"}:${component}` + state.subtreeKeys.set(element, key) + return key + } + const recordMutations = (records: MutationRecord[]) => { + if (!state.running) return + records.forEach((record) => { + record.addedNodes.forEach((node) => { + if (node instanceof HTMLElement && node.matches("[data-timeline-key]") && visible(node)) { + state.visibleMounts += 1 + state.visibleRows.add(node) + } + if (!(node instanceof Element)) return + const added = [node, ...node.querySelectorAll(critical)].filter((element) => element.matches(critical)) + added.forEach((element) => { + if (visible(element)) state.visibleSubtreeMounts.push(describe(element)) + }) + }) + record.removedNodes.forEach((node) => { + if (node instanceof HTMLElement && node.matches("[data-timeline-key]") && state.visibleRows.delete(node)) + state.visibleUnmounts += 1 + if (!(node instanceof Element)) return + const removed = [node, ...node.querySelectorAll(critical)].filter((element) => element.matches(critical)) + removed.forEach((element) => { + const key = describe(element) + if (state.visibleSubtrees.get(key) === element) state.visibleSubtreeUnmounts.push(key) + }) + }) + }) + } + const mutationObserver = profileVisual ? new MutationObserver(recordMutations) : undefined + mutationObserver?.observe(root, { childList: true, subtree: true }) + const currentPart = () => root.querySelector(`[data-timeline-part-id="${textPartID}"]`) + const observeProgress = (at: number) => { + if (!state.running) return + const content = currentPart()?.textContent ?? "" + const index = content.includes("benchmark-complete") + ? finalIndex + : Number(content.match(new RegExp(markerPattern, "g"))?.at(-1)?.match(/\d+/)?.[0] ?? -1) + if (index >= 0 && index !== state.applied.at(-1)?.index) state.applied.push({ at, index }) + } + const progressObserver = new MutationObserver(() => observeProgress(performance.now())) + progressObserver.observe(root, { characterData: true, childList: true, subtree: true }) + state.cleanup = () => { + recordLongTasks(longTaskObserver.takeRecords()) + recordLayoutShifts(layoutShiftObserver?.takeRecords() ?? []) + recordMutations(mutationObserver?.takeRecords() ?? []) + if (progressObserver.takeRecords().length) observeProgress(performance.now()) + longTaskObserver.disconnect() + layoutShiftObserver?.disconnect() + mutationObserver?.disconnect() + progressObserver.disconnect() + if (!profileVisual) return + Element.prototype.scrollTo = scrollTo + Object.defineProperty(Element.prototype, "scrollTop", scrollTop) + } + + const sample = (now: number) => { + if (!state.running) return + state.frameAt.push(now) + observeProgress(now) + if (minimal) { + state.frames.push(now - state.previous) + state.previous = now + requestAnimationFrame(sample) + return + } + setTimeout(() => { + if (!state.running) return + state.scroll.frame += 1 + const duration = now - state.previous + state.frames.push(duration) + state.previous = now + const virtualRoot = root.querySelector("[data-timeline-virtual-content]") + const header = root.querySelector("[data-session-title]") + state.geometry.push({ + scrollTop: root.scrollTop, + scrollHeight: root.scrollHeight, + clientHeight: root.clientHeight, + distance: root.scrollHeight - root.clientHeight - root.scrollTop, + virtualHeight: virtualRoot?.getBoundingClientRect().height ?? 0, + headerHeight: header?.getBoundingClientRect().height ?? 0, + }) + const viewport = root.getBoundingClientRect() + if (profileVisual) { + const visibleRows = [...root.querySelectorAll("[data-timeline-key]")] + .map((element) => ({ element, rect: element.getBoundingClientRect() })) + .filter((item) => item.rect.bottom > viewport.top && item.rect.top < viewport.bottom) + .sort((a, b) => a.rect.top - b.rect.top) + state.visibleRows = new Set(visibleRows.map((item) => item.element)) + const rows = visibleRows.map((item) => item.rect) + rows.slice(1).forEach((rect, index) => { + const previous = rows[index]! + state.maxOverlap = Math.max(state.maxOverlap, previous.bottom - rect.top) + state.maxGap = Math.max(state.maxGap, rect.top - previous.bottom) + }) + const partTop = part.getBoundingClientRect().top + state.maxPartTopMovement = Math.max(state.maxPartTopMovement, Math.abs(partTop - state.previousPartTop)) + state.previousPartTop = partTop + } + const visibleRow = [...root.querySelectorAll("[data-timeline-row]")].some((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > viewport.top && rect.top < viewport.bottom + }) + if (!visibleRow) state.blanks += 1 + if (profileVisual) { + const subtrees = new Map() + const visibleSubtrees = new Map() + root.querySelectorAll(critical).forEach((element) => { + const key = describe(element) + const rect = element.getBoundingClientRect() + const style = getComputedStyle(element) + const rendered = + element.isConnected && + rect.width > 0 && + rect.height > 0 && + style.display !== "none" && + style.visibility !== "hidden" && + Number(style.opacity) > 0 + subtrees.set(key, { element, rendered }) + if (rendered && rect.bottom > viewport.top && rect.top < viewport.bottom) { + const previous = state.visibleSubtrees.get(key) + if (previous && previous !== element && key.startsWith(`${textPartID}:`)) + state.visibleSubtreeReplacements += 1 + visibleSubtrees.set(key, element) + } + }) + state.visibleSubtrees.forEach((element, key) => { + const current = subtrees.get(key) + if (key.startsWith(`${textPartID}:`) && !current?.rendered) { + const markdown = part.querySelector('[data-component="markdown"]') + state.visibleSubtreeDropouts.push( + `${key}:projection=${markdown?.dataset.markdownProjectionLength}/${markdown?.dataset.markdownProjectionBlocks}:result=${markdown?.dataset.markdownResultLength}/${markdown?.dataset.markdownResultBlocks}:applied=${markdown?.dataset.markdownAppliedBlocks}:dom=${markdown?.children.length}`, + ) + } + if (element.matches('[data-component="file"]')) { + const hadLines = element.hasAttribute("data-profiler-had-lines") + const hasLines = element.shadowRoot?.querySelector("[data-line]") != null + if (hasLines) element.setAttribute("data-profiler-had-lines", "") + if (hadLines && !hasLines) state.visibleSubtreeDropouts.push(`${key}:shadow-lines`) + } + }) + state.visibleSubtrees = visibleSubtrees + } + if (profileVisual && duration > 33.34) { + const livePart = currentPart() + const content = livePart?.textContent ?? "" + const complete = content.includes("benchmark-complete") + const index = complete + ? finalIndex + : Number(content.match(new RegExp(markerPattern, "g"))?.at(-1)?.match(/\d+/)?.[0] ?? -1) + state.slowFrames.push({ + duration, + index, + phase: complete + ? "complete" + : index >= 0 && index % fragmentCount === 0 + ? "boundary" + : index >= 0 + ? "stream" + : "unknown", + tokenSpans: livePart?.querySelectorAll(".shiki span").length ?? 0, + blocks: livePart?.querySelectorAll("[data-markdown-block]").length ?? 0, + codeBlocks: livePart?.querySelectorAll('[data-component="markdown-code"]').length ?? 0, + height: livePart?.getBoundingClientRect().height ?? 0, + distance: root.scrollHeight - root.clientHeight - root.scrollTop, + }) + } + requestAnimationFrame(sample) + }, 0) + } + state.start = () => { + state.started = performance.now() + state.previous = state.started + state.running = true + requestAnimationFrame(sample) + } + }, + { ...options, markerPattern: STREAM_MARKER_PATTERN, fragmentCount: STREAM_FRAGMENT_COUNT }, + ) +} + +export function startTimelineStreamProbe(page: Page) { + return page.evaluate(() => { + const state = (window as Window & { __timelineStreamBenchmark?: TimelineProbeState }).__timelineStreamBenchmark + if (!state) throw new Error("missing streaming benchmark state") + state.start() + }) +} + +type LayoutShiftEntry = PerformanceEntry & { value: number; hadRecentInput?: boolean } + +export function layoutShiftValue( + entry: Pick, + start: number, +) { + if (entry.startTime < start || entry.hadRecentInput) return + return entry.value +} + +export function removeVisibleRow(visible: Set, row: T) { + return visible.delete(row) +} + +export function streamProgress(content: string) { + const index = Number(content.match(new RegExp(STREAM_MARKER_PATTERN, "g"))?.at(-1)?.match(/\d+/)?.[0] ?? -1) + return { + index, + phase: content.includes("benchmark-complete") + ? ("complete" as const) + : index >= 0 && index % STREAM_FRAGMENT_COUNT === 0 + ? ("boundary" as const) + : index >= 0 + ? ("stream" as const) + : ("unknown" as const), + } +} + +export async function collectTimelineStreamMetrics( + page: Page, + options: { textPartID: string; finalIndex: number; navigations: string[] }, +) { + return page.evaluate(({ textPartID, finalIndex, navigations }) => { + const state = (window as Window & { __timelineStreamBenchmark?: TimelineProbeState }).__timelineStreamBenchmark + if (!state) throw new Error(`missing streaming benchmark state after navigation: ${JSON.stringify(navigations)}`) + state.ended = performance.now() + state.cleanup() + state.running = false + const part = document.querySelector(`[data-timeline-part-id="${textPartID}"]`) + const row = part?.closest("[data-timeline-row]") + const markdown = part?.querySelector('[data-component="markdown"]') + const sorted = state.frames.slice().sort((a, b) => a - b) + const duration = state.frames.reduce((sum, value) => sum + value, 0) + const longestSlowStreak = state.frames.reduce( + (result, value) => { + const current = value > 33.34 ? result.current + 1 : 0 + return { current, longest: Math.max(result.longest, current) } + }, + { current: 0, longest: 0 }, + ).longest + const busyStart = state.applied.at(0)?.at + const completion = state.applied.find((value) => value.index === finalIndex) + const busyEnd = completion?.at + const busyFrames = + busyStart === undefined || busyEnd === undefined + ? [] + : state.frames.filter((_, index) => state.frameAt[index]! >= busyStart && state.frameAt[index]! <= busyEnd) + const busySorted = busyFrames.slice().sort((a, b) => a - b) + const busyDuration = busyFrames.reduce((sum, value) => sum + value, 0) + const completionObservedMs = (completion?.at ?? NaN) - state.started + const visual = state.profileVisual + ? { + layoutShiftValueSum: state.layoutShifts.reduce((sum, value) => sum + value, 0), + maxLayoutShiftValue: Math.max(0, ...state.layoutShifts), + visibleMounts: state.visibleMounts, + visibleUnmounts: state.visibleUnmounts, + visibleSubtreeMounts: state.visibleSubtreeMounts, + visibleSubtreeUnmounts: [...new Set(state.visibleSubtreeUnmounts)], + visibleSubtreeReplacements: state.visibleSubtreeReplacements, + visibleSubtreeDropouts: [...new Set(state.visibleSubtreeDropouts)], + maxOverlapPx: state.maxOverlap, + maxGapPx: state.maxGap, + maxPartTopMovementPx: state.maxPartTopMovement, + slowestRafGaps: state.slowFrames + .sort((a, b) => b.duration - a.duration) + .slice(0, 20) + .map((frame) => ({ + durationMs: frame.duration, + index: frame.index, + phase: frame.phase, + tokenSpans: frame.tokenSpans, + blocks: frame.blocks, + codeBlocks: frame.codeBlocks, + heightPx: frame.height, + distancePx: frame.distance, + })), + slowRafGapPhases: Object.fromEntries( + ["stream", "boundary", "complete", "unknown"].map((phase) => { + const frames = state.slowFrames.filter((frame) => frame.phase === phase) + return [ + phase, + { + count: frames.length, + totalMs: frames.reduce((sum, frame) => sum + frame.duration, 0), + maxMs: Math.max(0, ...frames.map((frame) => frame.duration)), + }, + ] + }), + ), + scroll: state.scroll, + } + : null + const geometry = state.minimal + ? null + : { + maxDistancePx: Math.max(0, ...state.geometry.map((sample) => sample.distance)), + finalDistancePx: state.geometry.at(-1)?.distance ?? 0, + final: state.geometry.at(-1), + distanceTransitionsPx: state.geometry + .map((sample) => Math.round(sample.distance)) + .filter((value, index, values) => index === 0 || value !== values[index - 1]), + bottomDriftTransitions: state.geometry.slice(1).filter((value, index) => { + const previous = state.geometry[index]?.distance ?? 0 + return previous <= 1 && value.distance > 1 + }).length, + blankSamples: state.blanks, + } + return { + capabilities: { visual: state.profileVisual, geometry: !state.minimal }, + completionObservedMs, + deltasPerSecond: Number.isFinite(completionObservedMs) ? finalIndex / (completionObservedMs / 1_000) : null, + rafGapSamples: state.frames.length, + rafCallbackRate: duration ? (state.frames.length * 1000) / duration : 0, + observedProgressWindowRafCallbackRate: busyDuration ? (busyFrames.length * 1000) / busyDuration : null, + observedProgressWindowRafGapP95Ms: busySorted[Math.floor(busySorted.length * 0.95)] ?? null, + observedProgressWindowRafGaps: busyFrames.length, + maxObservedProgressIndex: Math.max(-1, ...state.applied.map((value) => value.index)), + observedProgressTransitions: state.applied.length, + rafGapP50Ms: sorted[Math.floor(sorted.length * 0.5)] ?? 0, + rafGapP95Ms: sorted[Math.floor(sorted.length * 0.95)] ?? 0, + rafGapP99Ms: sorted[Math.floor(sorted.length * 0.99)] ?? 0, + maxRafGapMs: sorted.at(-1) ?? 0, + rafGapsOver33Ms: state.frames.filter((value) => value > 33.34).length, + rafGapsOver50Ms: state.frames.filter((value) => value > 50).length, + missedFrameBudgetEquivalents: state.frames.reduce( + (sum, value) => sum + Math.max(0, Math.round(value / 16.67) - 1), + 0, + ), + longestRafGapOver33MsStreak: longestSlowStreak, + longTaskCount: state.longTasks.length, + longTaskTimeMs: state.longTasks.reduce((sum, value) => sum + value, 0), + visual, + geometry, + rowReplaced: row !== state.row, + markdownReplaced: markdown !== state.markdown, + domTextCharacters: part?.textContent?.length ?? 0, + } + }, options) +} diff --git a/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts b/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts new file mode 100644 index 0000000000000000000000000000000000000000..529081a1d9e05540f2bfd4f8dea3bdd411bce37f --- /dev/null +++ b/packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts @@ -0,0 +1,369 @@ +const words = [ + "alpha", + "bravo", + "charlie", + "delta", + "echo", + "foxtrot", + "golf", + "hotel", + "india", + "juliet", + "kilo", + "lima", + "metro", + "nova", + "orbit", + "pixel", + "quartz", + "river", + "signal", + "vector", +] + +const sourceID = "ses_smoke_source" +const targetID = "ses_smoke_target" +const childID = "ses_smoke_child" +const directory = "C:/OpenCode/SmokeProject" +const projectID = "proj_smoke_timeline" +const model = { providerID: "opencode", modelID: "claude-opus-4-6", variant: "max" } + +type MessageInfo = Record & { id: string; role: "user" | "assistant" } +type MessagePart = Record & { id: string; type: string; text?: string; tool?: string } +type Message = { info: MessageInfo; parts: MessagePart[] } + +function lorem(seed: number, length: number) { + let out = "" + let i = seed + while (out.length < length) { + const word = words[i % words.length] + out += (out ? " " : "") + word + if (i % 17 === 0) out += ".\n\n" + i += 7 + } + return out.slice(0, length) +} + +function id(prefix: string, value: number) { + return `${prefix}_smoke_${String(value).padStart(4, "0")}` +} + +function userMessage(sessionID: string, index: number, textLength: number, diffs: unknown[] = []): Message { + const messageID = id("msg_user", index) + return { + info: { + id: messageID, + sessionID, + role: "user", + time: { created: 1700000000000 + index * 10_000 }, + summary: { diffs }, + agent: "build", + model, + }, + parts: [ + { + id: id("prt_user_text", index), + sessionID, + messageID, + type: "text", + text: lorem(index, textLength), + }, + ], + } +} + +function assistantMessage(sessionID: string, index: number, parentID: string, parts: MessagePart[]): Message { + const messageID = id("msg_assistant", index) + return { + info: { + id: messageID, + sessionID, + role: "assistant", + time: { created: 1700000000000 + index * 10_000 + 1_000, completed: 1700000000000 + index * 10_000 + 8_000 }, + parentID, + modelID: model.modelID, + providerID: model.providerID, + mode: "build", + agent: "build", + path: { cwd: directory, root: directory }, + cost: 0.01, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + variant: "max", + finish: "stop", + }, + parts: parts.map((part) => ({ + ...part, + sessionID, + messageID, + })), + } +} + +function textPart(index: number, partIndex: number, length: number): MessagePart { + const prose = lorem(index * 13 + partIndex, length) + const text = + index % 12 === 0 + ? `${prose}\n\n\`\`\`ts\n${code(index, 80)}\n\`\`\`` + : index % 5 === 0 + ? `${prose}\n\n\`\`\`ts\nexport const value = "${lorem(index, 220)}"\n\`\`\`` + : index % 7 === 0 + ? `${prose}\n\nThe wrapped inline value is \`${lorem(index, 180)}\`.` + : prose + return { id: id(`prt_text_${partIndex}`, index), type: "text", text } +} + +function reasoningPart(index: number, partIndex: number, length: number): MessagePart { + return { + id: id(`prt_reasoning_${partIndex}`, index), + type: "reasoning", + text: lorem(index * 19 + partIndex, length), + time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 500 }, + } +} + +function toolPart( + index: number, + partIndex: number, + tool: string, + input: Record, + outputLength = 160, + metadataOverride?: Record, +): MessagePart { + const metadata = + metadataOverride ?? + (tool === "apply_patch" + ? { files: [patchFile(index, "update"), patchFile(index + 1, index % 2 === 0 ? "add" : "delete")] } + : tool === "edit" || tool === "write" + ? { + filediff: fileDiff(String(input.filePath ?? `src/generated/file-${index}.ts`), index), + diff: patch(index, outputLength), + preview: patch(index + 1, 420), + } + : tool === "question" + ? { answers: [["Proceed"], ["Keep sample output"]] } + : {}) + return { + id: id(`prt_tool_${tool}_${partIndex}`, index), + type: "tool", + callID: id("call", index * 10 + partIndex), + tool, + state: { + status: "completed", + input, + output: lorem(index * 23 + partIndex, outputLength), + title: tool === "bash" ? "Verify generated output" : input.filePath || input.path || input.pattern || "completed", + metadata, + time: { start: 1700000000000 + index * 10_000, end: 1700000000000 + index * 10_000 + 400 }, + }, + } +} + +function patchFile(seed: number, type: "add" | "update" | "delete") { + return { + filePath: `src/generated/patch-${seed}.ts`, + relativePath: `src/generated/patch-${seed}.ts`, + type, + additions: (seed % 7) + 1, + deletions: type === "add" ? 0 : seed % 4, + patch: patch(seed, 520), + before: type === "add" ? undefined : code(seed, 18), + after: type === "delete" ? undefined : code(seed + 1, 24), + } +} + +function fileDiff(file: string, seed: number) { + const lines = seed % 12 === 0 ? 300 : seed % 8 === 0 ? 2 : 38 + const before = code(seed, lines, seed % 10 === 0 ? 280 : 32) + const after = + lines === 2 + ? before.replace("value1", "updatedValue1") + : lines === 300 + ? code(seed + 1, lines, seed % 10 === 0 ? 280 : 32) + : before.replace("value4", "updatedValue4").replace("value20", "updatedValue20") + return { + file, + additions: lines === 300 ? 300 : lines === 2 ? 1 : 2, + deletions: lines === 300 ? 300 : lines === 2 ? 1 : 2, + before, + after, + } +} + +function patch(seed: number, length: number) { + return `diff --git a/src/generated/file-${seed}.ts b/src/generated/file-${seed}.ts\n+${lorem(seed, length).replace(/\n/g, "\n+")}` +} + +function code(seed: number, lines: number, width = 32) { + return Array.from( + { length: lines }, + (_, index) => `export const value${index} = "${lorem(seed + index, width)}"`, + ).join("\n") +} + +function turn(index: number): Message[] { + const diff = index % 9 === 0 ? [fileDiff(`src/generated/summary-${index}.ts`, index)] : [] + const user = userMessage(targetID, index, 100 + (index % 4) * 80, diff) + const parts = [ + ...(index % 5 === 0 ? [reasoningPart(index, 0, 420)] : []), + ...(index % 3 === 0 + ? [ + toolPart(index, 0, "read", { filePath: `src/generated/file-${index}.ts`, offset: 0, limit: 80 }, 220), + toolPart(index, 5, "glob", { path: directory, pattern: `**/*sample-${index}*.ts` }, 140), + toolPart(index, 1, "grep", { path: directory, pattern: `sample-${index}`, include: "*.ts" }, 180), + toolPart(index, 6, "list", { path: `src/generated/${index}` }, 120), + ] + : []), + textPart(index, 2, 160 + (index % 6) * 90), + ...(index % 4 === 0 ? [toolPart(index, 3, "edit", { filePath: `src/generated/file-${index}.ts` }, 700)] : []), + ...(index % 6 === 0 + ? [toolPart(index, 7, "write", { filePath: `src/generated/write-${index}.ts`, content: code(index, 28) }, 560)] + : []), + ...(index % 8 === 0 + ? [toolPart(index, 8, "apply_patch", { files: [`src/generated/patch-${index}.ts`] }, 620)] + : []), + ...(index % 7 === 0 + ? [toolPart(index, 4, "bash", { command: "bun typecheck", description: "Verify generated output" }, 620)] + : []), + ...(index % 10 === 0 ? [toolPart(index, 9, "webfetch", { url: "https://example.com/docs/sample" }, 120)] : []), + ...(index % 11 === 0 ? [toolPart(index, 10, "websearch", { query: "sample movement notes" }, 240)] : []), + ...(index % 13 === 0 + ? [ + toolPart( + index, + 11, + "question", + { questions: [{ question: "Use generated fixture?" }, { question: "Keep same row shape?" }] }, + 120, + ), + ] + : []), + ...(index % 17 === 0 + ? [toolPart(index, 12, "task", { description: "Inspect generated fixture", subagent_type: "explore" }, 160)] + : []), + ] + return [user, assistantMessage(targetID, index, user.info.id, parts)] +} + +const targetMessages = Array.from({ length: 72 }, (_, index) => turn(index)).flat() +const sourceMessages = Array.from({ length: 12 }, (_, index) => [ + userMessage(sourceID, index + 1000, 120), + assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [ + textPart(index + 1000, 0, 240), + ...(index === 11 + ? [ + toolPart( + index + 1000, + 1, + "task", + { description: "Inspect child navigation", subagent_type: "explore" }, + 160, + { sessionId: childID }, + ), + ] + : []), + ]), +]).flat() +const childMessages = Array.from({ length: 4 }, (_, index) => [ + userMessage(childID, index + 2000, 120), + assistantMessage(childID, index + 2000, id("msg_user", index + 2000), [textPart(index + 2000, 0, 240)]), +]).flat() + +function renderable(part: MessagePart) { + if (part.type === "tool" && part.tool === "todowrite") return false + if (part.type === "text") return !!part.text.trim() + if (part.type === "reasoning") return !!part.text.trim() + return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch" +} + +function orderedParts(message: Message) { + return message.parts.slice().sort((a, b) => a.id.localeCompare(b.id)) +} + +export const fixture = { + directory, + project: { + id: projectID, + worktree: directory, + vcs: "git", + name: "smoke-project", + time: { created: 1700000000000, updated: 1700000000000 }, + sandboxes: [], + }, + provider: { + all: [ + { + id: "opencode", + name: "OpenCode", + models: { "claude-opus-4-6": { id: "claude-opus-4-6", name: "Claude Opus 4.6", limit: { context: 200_000 } } }, + }, + ], + connected: ["opencode"], + default: { providerID: "opencode", modelID: "claude-opus-4-6" }, + }, + sessions: [ + { + id: sourceID, + slug: "source", + projectID, + directory, + title: "Uncommitted changes inquiry", + version: "dev", + time: { created: 1700000000000, updated: 1700000000000 }, + }, + { + id: targetID, + slug: "target", + projectID, + directory, + title: "Example Game: sample jump movement & sample physics analysis", + version: "dev", + time: { created: 1700000001000, updated: 1700000001000 }, + }, + { + id: childID, + parentID: sourceID, + slug: "child", + projectID, + directory, + title: "Inspect child navigation", + version: "dev", + time: { created: 1700000002000, updated: 1700000002000 }, + }, + ], + sourceID, + targetID, + childID, + messages: { [sourceID]: sourceMessages, [targetID]: targetMessages, [childID]: childMessages }, + expected: { + sourceTitle: "Uncommitted changes inquiry", + targetTitle: "Example Game: sample jump movement & sample physics analysis", + childTitle: "Inspect child navigation", + sourceMessageIDs: sourceMessages + .filter((message) => message.info.role === "user") + .map((message) => message.info.id), + targetMessageIDs: targetMessages + .filter((message) => message.info.role === "user") + .map((message) => message.info.id), + childMessageIDs: childMessages.filter((message) => message.info.role === "user").map((message) => message.info.id), + targetPartIDs: targetMessages.flatMap((message) => + orderedParts(message) + .filter(renderable) + .map((part) => part.id), + ), + }, +} + +export function pageMessages(sessionID: string, limit: number, before?: string) { + const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] + const end = before + ? Math.max( + 0, + messages.findIndex((message) => message.info.id === before), + ) + : messages.length + const start = Math.max(0, end - limit) + return { + items: messages.slice(start, end), + cursor: start > 0 ? messages[start]!.info.id : undefined, + } +} diff --git a/packages/app/e2e/performance/timeline/timeline-test-helpers.ts b/packages/app/e2e/performance/timeline/timeline-test-helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..5028373c55f670951116195fde4646f9335172f4 --- /dev/null +++ b/packages/app/e2e/performance/timeline/timeline-test-helpers.ts @@ -0,0 +1,134 @@ +import type { Page } from "@playwright/test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { mockOpenCodeServer } from "../../utils/mock-server" +import { fixture, pageMessages } from "./session-timeline-stress.fixture" + +export async function installTimelineSettings(page: Page) { + await page.addInitScript(() => { + localStorage.setItem( + "settings.v3", + JSON.stringify({ + general: { + newLayoutDesigns: true, + editToolPartsExpanded: true, + shellToolPartsExpanded: true, + showReasoningSummaries: true, + }, + }), + ) + }) +} + +export function mockStressTimeline( + page: Page, + input?: { + onMessages?: (input: { sessionID: string; before?: string; phase: "start" | "end" }) => void + vcsDiff?: unknown[] + }, +) { + return mockOpenCodeServer(page, { + sessions: fixture.sessions, + provider: fixture.provider, + directory: fixture.directory, + project: fixture.project, + pageMessages, + onMessages: input?.onMessages, + vcsDiff: input?.vcsDiff, + }) +} + +export async function installStressSessionTabs(page: Page, input?: { draftID?: string; sessionIDs?: string[] }) { + const server = stressServer() + await page.addInitScript( + ({ directory, sessionIDs, dirBase64, server, draftID }) => { + localStorage.setItem( + "opencode.global.dat:server", + JSON.stringify({ + projects: { local: [{ worktree: directory, expanded: true }] }, + lastProject: { local: directory }, + }), + ) + localStorage.setItem( + "opencode.window.browser.dat:tabs", + JSON.stringify([ + ...sessionIDs.map((sessionId) => ({ + type: "session", + server, + dirBase64, + sessionId, + })), + ...(draftID ? [{ type: "draft", draftID, server, directory }] : []), + ]), + ) + }, + { + directory: fixture.directory, + sessionIDs: input?.sessionIDs ?? [fixture.sourceID, fixture.targetID], + dirBase64: base64Encode(fixture.directory), + server, + draftID: input?.draftID, + }, + ) +} + +export function stressSessionHref(sessionID: string) { + return `/server/${base64Encode(stressServer())}/session/${sessionID}` +} + +export function stressDraftHref(draftID: string) { + return `/new-session?draftId=${encodeURIComponent(draftID)}` +} + +function stressServer() { + return `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` +} + +export function createReviewDiffs() { + return Array.from({ length: Number(process.env.REVIEW_PANE_DIFF_COUNT ?? 72) }, (_, index) => { + const lines = index % 3 === 0 ? 300 : index % 3 === 1 ? 120 : 38 + const file = `src/review/generated-${String(index).padStart(3, "0")}.ts` + const before = reviewSource(index, lines) + const after = before + .replace(`value_${index}_4`, `updated_${index}_4`) + .replace( + `value_${index}_${Math.max(8, Math.floor(lines / 2))}`, + `updated_${index}_${Math.max(8, Math.floor(lines / 2))}`, + ) + .replace(`value_${index}_${lines - 4}`, `updated_${index}_${lines - 4}`) + return { + file, + patch: reviewPatch(file, before, after), + additions: 3, + deletions: 3, + status: "modified" as const, + } + }) +} + +function reviewSource(seed: number, lines: number) { + return Array.from( + { length: lines }, + (_, index) => `export const value_${seed}_${index} = "${reviewWords(seed + index, index % 5 === 0 ? 180 : 42)}"`, + ).join("\n") +} + +function reviewPatch(file: string, before: string, after: string) { + const beforeLines = before.split("\n") + const afterLines = after.split("\n") + return [ + `diff --git a/${file} b/${file}`, + `--- a/${file}`, + `+++ b/${file}`, + `@@ -1,${beforeLines.length} +1,${afterLines.length} @@`, + ...beforeLines.flatMap((line, index) => { + const next = afterLines[index]! + if (line === next) return [` ${line}`] + return [`-${line}`, `+${next}`] + }), + ].join("\n") +} + +function reviewWords(seed: number, length: number) { + const words = ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliet"] + return Array.from({ length: Math.ceil(length / 7) }, (_, index) => words[(seed + index * 3) % words.length]).join(" ") +} diff --git a/packages/app/e2e/performance/unit/chrome-trace-write.test.ts b/packages/app/e2e/performance/unit/chrome-trace-write.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..456020ff30d7ca180de55f01f0087e704ee3c281 --- /dev/null +++ b/packages/app/e2e/performance/unit/chrome-trace-write.test.ts @@ -0,0 +1,15 @@ +import { expect, test } from "bun:test" +import { mkdtemp, rm } from "node:fs/promises" +import path from "node:path" +import os from "node:os" +import { prepareChromeTrace } from "../chrome-trace" + +test("creates the configured trace directory", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "opencode-trace-")) + try { + const file = await prepareChromeTrace(path.join(root, "nested", "traces"), "session/tab", false, "test") + expect(file).toEndWith("-session-tab-458ed9e3-test.json") + } finally { + await rm(root, { recursive: true, force: true }) + } +}) diff --git a/packages/app/e2e/performance/unit/first-navigation-metrics.test.ts b/packages/app/e2e/performance/unit/first-navigation-metrics.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0e5474d6e91168ba61ec0d10831f24c0be7e20f --- /dev/null +++ b/packages/app/e2e/performance/unit/first-navigation-metrics.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from "bun:test" +import { summarizeFirstNavigation } from "../timeline/first-navigation-metrics" + +test("reports blank frames before first destination and stable paint", () => { + expect( + summarizeFirstNavigation([ + { observedAtMs: 16, source: true, destination: false, content: true }, + { observedAtMs: 32, source: false, destination: false, content: false }, + { observedAtMs: 48, source: false, destination: true, content: true }, + { observedAtMs: 64, source: false, destination: true, content: true }, + { observedAtMs: 80, source: false, destination: true, content: true }, + ]), + ).toEqual({ + samples: 5, + firstDestinationObservedMs: 48, + stableDestinationObservedMs: 80, + sourceSamples: 1, + blankSamples: 1, + unknownSamples: 0, + destinationSamples: 3, + }) +}) + +test("does not report stability for interrupted destination frames", () => { + expect( + summarizeFirstNavigation([ + { observedAtMs: 16, source: false, destination: true, content: true }, + { observedAtMs: 32, source: false, destination: false, content: true }, + { observedAtMs: 48, source: false, destination: true, content: true }, + ]).stableDestinationObservedMs, + ).toBeNull() +}) diff --git a/packages/app/e2e/performance/unit/mock-server.test.ts b/packages/app/e2e/performance/unit/mock-server.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..83308c0a866b1695696421147fd0644b6e251185 --- /dev/null +++ b/packages/app/e2e/performance/unit/mock-server.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from "bun:test" +import type { Page, Route } from "@playwright/test" +import { mockOpenCodeServer } from "../../utils/mock-server" + +test("applies message latency after a list response gate is released", async () => { + const events: string[] = [] + const gate = Promise.withResolvers() + let handler: ((route: Route) => Promise) | undefined + const page = { + route: (_url: string, callback: (route: Route) => Promise) => { + handler = callback + return Promise.resolve() + }, + } as unknown as Page + await mockOpenCodeServer(page, { + provider: {}, + directory: "C:/OpenCode", + project: {}, + sessions: [{ id: "session" }], + messageDelay: 25, + beforeMessagesResponse: () => { + events.push("before") + return gate.promise + }, + onMessages: (request) => events.push(request.phase), + pageMessages: () => { + events.push("page") + return { items: [] } + }, + }) + + const response = handler!({ + request: () => ({ url: () => "http://127.0.0.1:4096/session/session/message" }), + fulfill: () => { + events.push("fulfill") + return Promise.resolve() + }, + } as unknown as Route) + expect(events).toEqual(["start", "before"]) + + const released = performance.now() + gate.resolve() + await response + expect(performance.now() - released).toBeGreaterThanOrEqual(20) + expect(events).toEqual(["start", "before", "page", "end", "fulfill"]) +}) diff --git a/packages/app/e2e/performance/unit/navigation-milestones.test.ts b/packages/app/e2e/performance/unit/navigation-milestones.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e22be67063d26e83f2e28fca826394e89735c47d --- /dev/null +++ b/packages/app/e2e/performance/unit/navigation-milestones.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from "bun:test" +import { summarizeNavigationMilestones } from "../timeline/navigation-milestones" + +test("reports first and stable paint for each navigation milestone", () => { + expect( + summarizeNavigationMilestones([ + { observedAtMs: 16, milestones: { content: false, tab: false } }, + { observedAtMs: 32, milestones: { content: true, tab: false } }, + { observedAtMs: 48, milestones: { content: true, tab: true } }, + { observedAtMs: 64, milestones: { content: true, tab: true } }, + { observedAtMs: 80, milestones: { content: true, tab: true } }, + ]), + ).toEqual({ + samples: 5, + milestones: { + content: { firstObservedMs: 32, stableObservedMs: 64 }, + tab: { firstObservedMs: 48, stableObservedMs: 80 }, + }, + all: { firstObservedMs: 48, stableObservedMs: 80 }, + }) +}) + +test("reports missing stability when a milestone appears in the final samples", () => { + expect( + summarizeNavigationMilestones([ + { observedAtMs: 16, milestones: { content: false } }, + { observedAtMs: 32, milestones: { content: true } }, + ]), + ).toEqual({ + samples: 2, + milestones: { content: { firstObservedMs: 32, stableObservedMs: null } }, + all: { firstObservedMs: 32, stableObservedMs: null }, + }) +}) diff --git a/packages/app/e2e/performance/unit/session-tab-repaint-probe.test.ts b/packages/app/e2e/performance/unit/session-tab-repaint-probe.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d20c03a00f42e3e5495867dbb17b188a1070cd4 --- /dev/null +++ b/packages/app/e2e/performance/unit/session-tab-repaint-probe.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from "bun:test" +import { compressCachedRepaintTrace, layoutShiftSample } from "../timeline/session-tab-repaint-probe" + +test("compresses repeated repaint states without losing frame samples", () => { + const state = { + root: 1, + scrollTop: 10, + scrollHeight: 20, + bottomErrorPx: 0, + last: true, + rows: [{ key: "row", node: 2, top: 0, bottom: 10 }], + mounted: 1, + center: "content", + } + const trace = { + timeOriginEpochMs: 1_000, + startedAtPerformanceMs: 100, + samples: [ + { observedAtMs: 16, ...state, destination: ["target"], source: [] }, + { observedAtMs: 32, ...state, destination: ["target"], source: [] }, + { observedAtMs: 48, ...state, scrollTop: 11, destination: ["target"], source: [] }, + ], + mutations: [{ observedAtMs: 20, changed: [{ type: "add", node: 2 }] }], + shifts: [{ occurredAtMs: 24, value: 0.1 }], + windowMs: 1_000, + running: false, + stop() {}, + } + const compressed = compressCachedRepaintTrace(trace) + const samples = compressed.samples.flatMap((group) => + group.observedAtMs.map((observedAtMs) => ({ observedAtMs, ...group.state })), + ) + + expect(samples).toEqual(trace.samples) + expect(compressed.mutations).toEqual(trace.mutations) + expect(compressed.shifts).toEqual(trace.shifts) +}) + +test("records layout shifts at occurrence time within the probe window", () => { + expect(layoutShiftSample({ startTime: 99, value: 0.1 }, 100)).toBeUndefined() + expect(layoutShiftSample({ startTime: 124, value: 0.2 }, 100)).toEqual({ occurredAtMs: 24, value: 0.2 }) +}) diff --git a/packages/app/e2e/performance/unit/session-tab-switch-metrics.test.ts b/packages/app/e2e/performance/unit/session-tab-switch-metrics.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b824d9dfb0d480a9b86739c56d515f7c234a2bc --- /dev/null +++ b/packages/app/e2e/performance/unit/session-tab-switch-metrics.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from "bun:test" +import { classifySessionSwitch } from "../timeline/session-tab-switch-metrics" + +test("counts source and blank samples before the destination is observed", () => { + const result = classifySessionSwitch([ + { observedAtMs: 16, destination: [], source: ["source"], hasVisibleRows: true, last: false }, + { observedAtMs: 32, destination: [], source: [], hasVisibleRows: false, last: false }, + { observedAtMs: 48, destination: ["destination"], source: [], hasVisibleRows: true, last: true, bottomErrorPx: 0 }, + { observedAtMs: 64, destination: ["destination"], source: [], hasVisibleRows: true, last: true, bottomErrorPx: 0 }, + { observedAtMs: 80, destination: ["destination"], source: [], hasVisibleRows: true, last: true, bottomErrorPx: 0 }, + ]) + + expect(result.blankSamples).toBe(1) + expect(result.sourceSamples).toBe(1) + expect(result.unknownSamples).toBe(0) + expect(result.firstDestinationObservedMs).toBe(48) + expect(result.stableObservedMs).toBe(80) +}) + +test("does not classify mixed source and destination content as correct", () => { + const result = classifySessionSwitch([ + { + observedAtMs: 16, + destination: ["destination"], + source: ["source"], + hasVisibleRows: true, + last: true, + bottomErrorPx: 0, + }, + { observedAtMs: 32, destination: ["destination"], source: [], hasVisibleRows: true, last: true, bottomErrorPx: 0 }, + { observedAtMs: 48, destination: ["destination"], source: [], hasVisibleRows: true, last: true, bottomErrorPx: 0 }, + { observedAtMs: 64, destination: ["destination"], source: [], hasVisibleRows: true, last: true, bottomErrorPx: 0 }, + ]) + + expect(result.firstCorrectObservedMs).toBe(32) + expect(result.stableObservedMs).toBe(64) +}) + +test("reports missing correctness without throwing", () => { + const result = classifySessionSwitch([ + { + observedAtMs: 16, + destination: ["destination"], + source: ["source"], + hasVisibleRows: true, + last: true, + bottomErrorPx: 0, + }, + ]) + + expect(result.firstDestinationObservedMs).toBe(16) + expect(result.firstCorrectObservedMs).toBeNull() + expect(result.stableObservedMs).toBeNull() +}) + +test("requires an explicitly tracked part to be visible", () => { + const result = classifySessionSwitch([ + { + observedAtMs: 16, + destination: ["destination"], + source: [], + hasVisibleRows: true, + last: true, + requiredPartVisible: false, + bottomErrorPx: 0, + }, + ]) + + expect(result.firstCorrectObservedMs).toBeNull() +}) + +test("can measure content correctness without requiring a bottom anchor", () => { + const result = classifySessionSwitch([ + { + observedAtMs: 16, + destination: ["destination"], + source: [], + hasVisibleRows: true, + last: true, + requiredPartVisible: true, + bottomAnchorRequired: false, + }, + ]) + + expect(result.firstCorrectObservedMs).toBe(16) +}) diff --git a/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts b/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5d7f09a69ef116ba352885886efb40a5d8c4369 --- /dev/null +++ b/packages/app/e2e/performance/unit/session-tab-switch-probe.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from "bun:test" +import type { Page } from "@playwright/test" +import { measureSessionSwitch } from "../timeline/session-tab-switch-probe" + +function testPage(waitFailure?: Error) { + const stops: unknown[] = [] + const page = { + evaluate: async (_callback: unknown, input?: unknown) => { + if (input) return + stops.push(undefined) + }, + waitForFunction: async () => { + if (waitFailure) throw waitFailure + }, + } as unknown as Page + return { page, stops } +} + +function input(run: () => Promise) { + return { + destinationIDs: ["destination"], + sourceIDs: ["source"], + lastID: "destination", + href: "/session/destination", + switch: run, + } +} + +test("stops sampling when the session switch fails", async () => { + const failure = new Error("switch failed") + const context = testPage() + + await expect( + measureSessionSwitch( + context.page, + input(async () => Promise.reject(failure)), + ), + ).rejects.toBe(failure) + + expect(context.stops).toHaveLength(1) +}) + +test("stops sampling when the stable wait fails", async () => { + const failure = new Error("stable wait failed") + const context = testPage(failure) + + await expect( + measureSessionSwitch( + context.page, + input(async () => {}), + ), + ).rejects.toBe(failure) + + expect(context.stops).toHaveLength(1) +}) diff --git a/packages/app/e2e/performance/unit/timeline-test-helpers.test.ts b/packages/app/e2e/performance/unit/timeline-test-helpers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..98b661d91bbab6f38ce595b97419656fe21f0627 --- /dev/null +++ b/packages/app/e2e/performance/unit/timeline-test-helpers.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from "bun:test" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { fixture } from "../timeline/session-timeline-stress.fixture" +import { stressSessionHref } from "../timeline/timeline-test-helpers" + +test("builds stress session links for the benchmark server", () => { + expect(stressSessionHref(fixture.sourceID)).toBe( + `/server/${base64Encode("http://127.0.0.1:4096")}/session/${fixture.sourceID}`, + ) +}) diff --git a/packages/app/e2e/performance/unit/visual-stability.test.ts b/packages/app/e2e/performance/unit/visual-stability.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fc3be316660b0c7c0b476128e6cd2120727e88c --- /dev/null +++ b/packages/app/e2e/performance/unit/visual-stability.test.ts @@ -0,0 +1,392 @@ +import { expect, test } from "bun:test" +import { + analyzeVisualStability, + analyzeVisualStabilityByMarker, + type VisualStabilityTrace, +} from "../../utils/visual-stability" +import { analyzeVisualObservations } from "../../utils/visual-stability/analyzer" +import { legacyVisualPlan, visualPlan, type VisualInvariant } from "../../utils/visual-stability/invariant" +import { defineVisualRegions, mapVisualRegions } from "../../utils/visual-stability/regions" + +function trace(samples: VisualStabilityTrace["samples"]): VisualStabilityTrace { + return { markers: [], samples } +} + +test("accepts continuous visible motion", () => { + expect( + analyzeVisualStability( + trace([ + frame(0, region({ width: 80, bottom: 40 }), region({ top: 40, bottom: 60 })), + frame(16, region({ width: 75, bottom: 45 }), region({ top: 45, bottom: 65 })), + frame(32, region({ width: 70, bottom: 50 }), region({ top: 50, bottom: 70 })), + ]), + { flow: ["changing", "following"] }, + ), + ).toEqual([]) +}) + +test("reports repeated geometry reversals", () => { + const issues = analyzeVisualStability( + trace([ + frame(0, region({ width: 80 })), + frame(16, region({ width: 60 })), + frame(32, region({ width: 78 })), + frame(48, region({ width: 62 })), + ]), + ) + + expect(issues.some((issue) => issue.includes("changing width reversed 2 times"))).toBe(true) +}) + +test("reports visible blanking, label reversal, and overlap", () => { + const issues = analyzeVisualStability( + trace([ + frame(0, region({ label: "Exploring", opacity: 1, bottom: 40 }), region({ top: 40, bottom: 60 })), + frame(16, region({ label: "Explored", opacity: 0.2, bottom: 50 }), region({ top: 49, bottom: 69 })), + frame(32, region({ label: "Exploring", opacity: 1, bottom: 50 }), region({ top: 50, bottom: 70 })), + ]), + { flow: ["changing", "following"] }, + ) + + expect(issues.some((issue) => issue.includes("opacity fell to 0.2"))).toBe(true) + expect(issues.some((issue) => issue.includes("label reverted"))).toBe(true) + expect(issues.some((issue) => issue.includes("overlapped following by 1px"))).toBe(true) +}) + +test("reports duplicate regions and unexpected remounts", () => { + const issues = analyzeVisualStability( + trace([frame(0, region({ node: 1 })), frame(16, region({ node: 2, count: 2 })), frame(32, region({ node: 2 }))]), + { stable: ["changing"], unique: ["changing"] }, + ) + + expect(issues.some((issue) => issue.includes("changing appeared 2 times"))).toBe(true) + expect(issues.some((issue) => issue.includes("changing remounted"))).toBe(true) +}) + +test("reports bottom anchor loss but permits movement while scrolled away", () => { + const anchored = analyzeVisualStability( + trace([ + { at: 0, regions: { changing: region() }, viewport: viewport(0) }, + { at: 16, regions: { changing: region() }, viewport: viewport(24) }, + ]), + { preserveBottomAnchor: true }, + ) + const away = analyzeVisualStability( + trace([ + { at: 0, regions: { changing: region() }, viewport: viewport(80) }, + { at: 16, regions: { changing: region() }, viewport: viewport(104) }, + ]), + { preserveBottomAnchor: true }, + ) + + expect(anchored.some((issue) => issue.includes("bottom anchor moved to 24px"))).toBe(true) + expect(away).toEqual([]) +}) + +test("reports up down up movement while preserving a bottom anchor", () => { + const issues = analyzeVisualStability( + trace([ + { at: 0, regions: { changing: region({ top: 200, bottom: 240 }) }, viewport: viewport(0) }, + { at: 16, regions: { changing: region({ top: 180, bottom: 220 }) }, viewport: viewport(0) }, + { at: 32, regions: { changing: region({ top: 196, bottom: 236 }) }, viewport: viewport(0) }, + { at: 48, regions: { changing: region({ top: 176, bottom: 216 }) }, viewport: viewport(0) }, + ]), + { preserveBottomAnchor: true, maxPositionReversals: 0 }, + ) + + expect(issues.some((issue) => issue.includes("changing top reversed 2 times"))).toBe(true) + expect(issues.some((issue) => issue.includes("changing bottom reversed 2 times"))).toBe(true) +}) + +test("accepts monotonic upward movement while preserving a bottom anchor", () => { + expect( + analyzeVisualStability( + trace([ + { at: 0, regions: { changing: region({ top: 200, bottom: 240 }) }, viewport: viewport(0) }, + { at: 16, regions: { changing: region({ top: 190, bottom: 230 }) }, viewport: viewport(0) }, + { at: 32, regions: { changing: region({ top: 180, bottom: 220 }) }, viewport: viewport(0) }, + ]), + { preserveBottomAnchor: true, maxPositionReversals: 0 }, + ), + ).toEqual([]) +}) + +test("ignores overlap entirely outside the clipped timeline viewport", () => { + expect( + analyzeVisualStability( + trace([ + { + at: 0, + regions: { + changing: region({ top: -200, bottom: -100 }), + following: region({ top: -150, bottom: -50 }), + }, + viewport: viewport(0), + }, + ]), + { flow: ["changing", "following"] }, + ), + ).toEqual([]) +}) + +test("reports visible anchor movement while allowing virtual scrollbar movement", () => { + const issues = analyzeVisualStability( + trace([ + { at: 0, regions: { anchor: region({ top: 100, bottom: 120 }) }, viewport: { ...viewport(100), scrollTop: 40 } }, + { at: 16, regions: { anchor: region({ top: 100, bottom: 120 }) }, viewport: { ...viewport(120), scrollTop: 60 } }, + ]), + { fixed: ["anchor"] }, + ) + + expect(issues).toEqual([]) + + const moved = analyzeVisualStability( + trace([ + { at: 0, regions: { anchor: region({ top: 100, bottom: 120 }) }, viewport: viewport(100) }, + { at: 16, regions: { anchor: region({ top: 112, bottom: 132 }) }, viewport: viewport(100) }, + ]), + { fixed: ["anchor"] }, + ) + expect(moved.some((issue) => issue.includes("anchor moved 12px in the viewport"))).toBe(true) +}) + +test("analyzes each marked event independently", () => { + const input: VisualStabilityTrace = { + markers: [ + { at: 10, label: "grow" }, + { at: 40, label: "shrink" }, + ], + samples: [ + frame(0, region({ top: 100 })), + frame(16, region({ top: 90 })), + frame(32, region({ top: 80 })), + frame(48, region({ top: 90 })), + frame(64, region({ top: 100 })), + ], + } + + expect(analyzeVisualStability(input, { maxPositionReversals: 0 })).toContain("changing top reversed 1 times") + expect( + analyzeVisualStabilityByMarker(input, { + maxPositionReversals: 0, + motion: ["changing"], + aggregateMotion: false, + }), + ).toEqual([]) +}) + +test("reports cross-event motion reversals by default", () => { + const input: VisualStabilityTrace = { + markers: [ + { at: 10, label: "up" }, + { at: 40, label: "down" }, + ], + samples: [ + frame(0, region({ top: 100 })), + frame(16, region({ top: 90 })), + frame(32, region({ top: 80 })), + frame(48, region({ top: 90 })), + frame(64, region({ top: 100 })), + ], + } + + expect(analyzeVisualStabilityByMarker(input, { maxPositionReversals: 0 })).toContain("changing top reversed 1 times") +}) + +test("reports regions rendered in the wrong flow order", () => { + const issues = analyzeVisualStability( + trace([frame(0, region({ top: 100, bottom: 120 }), region({ top: 60, bottom: 80 }))]), + { flow: ["changing", "following"] }, + ) + + expect(issues.some((issue) => issue.includes("changing rendered after following"))).toBe(true) +}) + +test("uses painted bounds instead of clipped layout overflow", () => { + expect( + analyzeVisualStability( + trace([ + frame( + 0, + region({ top: 100, bottom: 140, height: 40, layoutTop: 100, layoutBottom: 300 }), + region({ top: 140, bottom: 180 }), + ), + ]), + { flow: ["changing", "following"] }, + ), + ).toEqual([]) +}) + +test("does not report disappearance when a present row moves outside the viewport", () => { + expect( + analyzeVisualStability( + trace([ + frame(0, region({ visible: true })), + frame(16, region({ visible: false, inViewport: false, top: -100, bottom: -80 })), + frame(32, region({ visible: true })), + ]), + ), + ).toEqual([]) +}) + +test("reports an in-viewport transparent frame between visible frames", () => { + const issues = analyzeVisualStability( + trace([ + frame(0, region()), + frame(16, region({ visible: false, opacity: 0, inViewport: true })), + frame(32, region()), + ]), + ) + + expect(issues.some((issue) => issue.includes("blanked between visible frames"))).toBe(true) +}) + +test("reports an in-viewport display-none frame between visible frames", () => { + const issues = analyzeVisualStability( + trace([ + frame(0, region()), + frame(16, region({ visible: false, width: 0, height: 0, inViewport: true, cssHidden: true })), + frame(32, region()), + ]), + ) + + expect(issues.some((issue) => issue.includes("blanked between visible frames"))).toBe(true) +}) + +test("can limit motion analysis to unaffected regions", () => { + expect( + analyzeVisualStability( + trace([ + frame(0, region({ height: 20 }), region()), + frame(16, region({ height: 40 }), region()), + frame(32, region({ height: 30 }), region()), + ]), + { motion: ["following"] }, + ), + ).toEqual([]) +}) + +test("reports a blank frame across replacement surfaces", () => { + const issues = analyzeVisualStability( + { + markers: [], + samples: [ + { at: 0, regions: { thinking: region(), error: region({ present: false, visible: false }) } }, + { + at: 16, + regions: { + thinking: region({ present: false, visible: false }), + error: region({ present: false, visible: false }), + }, + }, + { at: 32, regions: { thinking: region({ present: false, visible: false }), error: region() } }, + ], + }, + { continuousAny: [["thinking", "error"]] }, + ) + + expect(issues.some((issue) => issue.includes("thinking | error blanked"))).toBe(true) +}) + +test("reports failure to acquire the bottom anchor", () => { + const issues = analyzeVisualStability( + trace([ + { at: 0, regions: { changing: region() }, viewport: viewport(600) }, + { at: 16, regions: { changing: region() }, viewport: viewport(120) }, + ]), + { acquireBottomAnchor: true }, + ) + + expect(issues.some((issue) => issue.includes("did not acquire bottom anchor"))).toBe(true) +}) + +test("reports a required region that never renders", () => { + const issues = analyzeVisualStability( + trace([ + { + at: 0, + regions: { changing: region({ present: false, visible: false }) }, + }, + ]), + { required: ["changing"] }, + ) + + expect(issues).toContain("changing never rendered") +}) + +test("preserves typed region names while mapping definitions", () => { + const regions = defineVisualRegions({ + changing: { selector: "[data-changing]" }, + following: { selector: "[data-following]", closest: "[data-row]" }, + }) + const selectors = mapVisualRegions(regions, (region) => region.selector) + + expect(selectors).toEqual({ changing: "[data-changing]", following: "[data-following]" }) + const name: keyof typeof selectors = "changing" + expect(name).toBe("changing") +}) + +test("evaluates the typed invariant algebra over explicit observations", () => { + const regions = defineVisualRegions({ + changing: { selector: "[data-changing]" }, + following: { selector: "[data-following]" }, + }) + const invariants = [ + { type: "required", regions: ["changing"] }, + { type: "flow", regions: ["changing", "following"] }, + ] satisfies VisualInvariant[] + // @ts-expect-error Plans reject names that are not in the region definition. + const invalid = { type: "required", regions: ["missing"] } satisfies VisualInvariant + const plan = visualPlan(regions, invariants, { perMarker: true }) + + expect(invalid.regions).toEqual(["missing"]) + expect(plan.perMarker).toBe(true) + expect(analyzeVisualObservations([frame(0, region({ bottom: 50 }), region({ top: 49, bottom: 69 }))], plan)).toEqual([ + "changing overlapped following by 1px at 0ms", + ]) +}) + +test("legacy plan adapter preserves analyzer messages and order", () => { + const input = trace([ + frame(0, region({ label: "Exploring", opacity: 1, bottom: 40 }), region({ top: 40, bottom: 60 })), + frame(16, region({ label: "Explored", opacity: 0.2, bottom: 50 }), region({ top: 49, bottom: 69 })), + frame(32, region({ label: "Exploring", opacity: 1, bottom: 50 }), region({ top: 50, bottom: 70 })), + ]) + const options = { flow: ["changing", "following"], stable: ["changing"] } + + expect(analyzeVisualObservations(input.samples, legacyVisualPlan(options))).toEqual( + analyzeVisualStability(input, options), + ) +}) + +function frame( + at: number, + changing: VisualStabilityTrace["samples"][number]["regions"][string], + following?: VisualStabilityTrace["samples"][number]["regions"][string], +) { + return { at, regions: { changing, ...(following ? { following } : {}) } } +} + +function region(input: Partial = {}) { + return { + present: true, + visible: true, + inViewport: true, + top: 0, + bottom: 20, + width: 100, + height: 20, + opacity: 1, + count: 1, + node: 1, + label: "", + text: "", + layoutTop: input.top ?? 0, + layoutBottom: input.bottom ?? 20, + ...input, + } +} + +function viewport(distanceFromBottom: number) { + return { top: 0, bottom: 400, scrollTop: 100, scrollHeight: 500, clientHeight: 400, distanceFromBottom } +} diff --git a/packages/app/e2e/reproduction/timeline-suspense/index.html b/packages/app/e2e/reproduction/timeline-suspense/index.html new file mode 100644 index 0000000000000000000000000000000000000000..995008a9f7a6c210cc9503cc7127f27f671fa044 --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/index.html @@ -0,0 +1,34 @@ + + + + + + Timeline Suspense Reproduction + + + +
+ + + diff --git a/packages/app/e2e/reproduction/timeline-suspense/main.tsx b/packages/app/e2e/reproduction/timeline-suspense/main.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cdcd809be3be9c8e02a9a7b63ca9853eab3f951c --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/main.tsx @@ -0,0 +1,315 @@ +import { createResource, createSignal, For, onMount, Suspense } from "solid-js" +import { render } from "solid-js/web" +import { createVirtualizer, observeElementOffset, observeElementRect } from "@tanstack/solid-virtual" +import { observeElementOffsetReconnectAware } from "../../../src/pages/session/timeline/observe-element-offset" + +const rowCount = 2_000 +const rowHeight = 40 +const parameters = new URLSearchParams(location.search) +const resourceMode = parameters.get("resource") === "guard" ? "guard" : "baseline" +const reconnectMode = parameters.get("reconnect") === "candidate" ? "candidate" : "baseline" + +type MutationEvent = { + kind: "removed" | "added" + callbackTime: number + callbackFrame: number + routeConnectedInCallback: boolean + nativeOffsetInCallback: number +} + +type Snapshot = { + mode: { + resource: "baseline" | "guard" + reconnect: "baseline" | "candidate" + } + operation: { + sequence: number + phase: string + time: number + frame: number + } + resourceState: string + routeConnected: boolean + viewportConnected: boolean + viewportOwnedByRoute: boolean + sameRoute: boolean + sameViewport: boolean + sameSurface: boolean + sameMountedRows: boolean + nativeOffset: number + coreOffset: number + rangeStart: number + rangeEnd: number + indexes: number[] + domIndexes: number[] + logicalSurfaceHeight: number + renderedSurfaceHeight: number + viewportClientHeight: number + viewportScrollHeight: number + visibleRows: number + minimumRowTop: number + domScrollEvents: number + lastScrollTrusted: boolean + coreOffsetCallbackCalls: number + offsetCallbackSources: "observer"[] + rectObserverCallbacks: number + ignoredDetachedZeroRects: number + syntheticScrollDispatches: number + mutationEvents: MutationEvent[] +} + +declare global { + interface Window { + timelineSuspense: { + prepare: () => Promise + trigger: () => void + resolve: () => void + frames: (count?: number) => Promise + snapshot: () => Snapshot + } + } +} + +function App() { + const [refresh, setRefresh] = createSignal(false) + let resolveResource: (() => void) | undefined + const [resource] = createResource( + refresh, + (version) => + new Promise((resolve) => { + resolveResource = () => resolve(`settled-${version}`) + }), + { initialValue: "settled" }, + ) + + function Route() { + let route: HTMLElement | undefined + let viewport: HTMLDivElement | undefined + let surface: HTMLDivElement | undefined + let initialRoute: HTMLElement | undefined + let initialViewport: HTMLDivElement | undefined + let initialSurface: HTMLDivElement | undefined + let initialRows: HTMLElement[] = [] + let phase = "mounting" + let browserFrame = 0 + let snapshotSequence = 0 + let domScrollEvents = 0 + let lastScrollTrusted = false + let coreOffsetCallbackCalls = 0 + let rectObserverCallbacks = 0 + let ignoredDetachedZeroRects = 0 + const offsetCallbackSources: "observer"[] = [] + const mutationEvents: MutationEvent[] = [] + const virtualizer = createVirtualizer({ + count: rowCount, + getScrollElement: () => viewport ?? null, + estimateSize: () => rowHeight, + initialRect: { width: 900, height: 600 }, + overscan: 2, + observeElementRect: (instance, callback) => + observeElementRect(instance, (rect) => { + rectObserverCallbacks++ + // A fixed 600px viewport has no usable geometry while detached. Keep the last connected rect. + if (!instance.scrollElement?.isConnected && rect.height === 0) { + ignoredDetachedZeroRects++ + return + } + callback(rect) + }), + observeElementOffset: (instance, callback) => { + const deliver = (offset: number, isScrolling: boolean) => { + coreOffsetCallbackCalls++ + offsetCallbackSources.push("observer") + callback(offset, isScrolling) + } + if (reconnectMode === "candidate") return observeElementOffsetReconnectAware(instance, deliver) + return observeElementOffset(instance, deliver) + }, + }) + + const frames = async (count = 2) => { + for (let index = 0; index < count; index++) { + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + } + } + const mountedRows = () => [...(surface?.querySelectorAll("[data-row-index]") ?? [])] + const snapshot = (): Snapshot => { + const rows = mountedRows() + const view = viewport?.getBoundingClientRect() + const visibleRows = + viewport?.isConnected && view + ? rows.filter((row) => { + const rect = row.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }).length + : 0 + return { + mode: { resource: resourceMode, reconnect: reconnectMode }, + operation: { + sequence: ++snapshotSequence, + phase, + time: performance.now(), + frame: browserFrame, + }, + resourceState: resource.state, + routeConnected: route?.isConnected ?? false, + viewportConnected: viewport?.isConnected ?? false, + viewportOwnedByRoute: !!route && !!viewport && route.contains(viewport), + sameRoute: route === initialRoute, + sameViewport: viewport === initialViewport, + sameSurface: surface === initialSurface, + sameMountedRows: + initialRows.length > 0 && + initialRows.length === rows.length && + initialRows.every((row, index) => row === rows[index]), + nativeOffset: viewport?.scrollTop ?? -1, + coreOffset: virtualizer.scrollOffset ?? -1, + rangeStart: virtualizer.range?.startIndex ?? -1, + rangeEnd: virtualizer.range?.endIndex ?? -1, + indexes: virtualizer.getVirtualItems().map((item) => item.index), + domIndexes: rows.map((row) => Number(row.dataset.rowIndex)), + logicalSurfaceHeight: Number.parseFloat(surface?.style.height ?? "-1"), + renderedSurfaceHeight: surface?.getBoundingClientRect().height ?? -1, + viewportClientHeight: viewport?.clientHeight ?? -1, + viewportScrollHeight: viewport?.scrollHeight ?? -1, + visibleRows, + minimumRowTop: + rows.length && view ? Math.min(...rows.map((row) => row.getBoundingClientRect().top - view.top)) : -1, + domScrollEvents, + lastScrollTrusted, + coreOffsetCallbackCalls, + offsetCallbackSources: [...offsetCallbackSources], + rectObserverCallbacks, + ignoredDetachedZeroRects, + syntheticScrollDispatches: 0, + mutationEvents: mutationEvents.map((event) => ({ ...event })), + } + } + + onMount(() => { + if (!route || !viewport || !surface) throw new Error("Timeline fixture did not mount") + const routeRoot = route.parentElement + if (!routeRoot) throw new Error("Timeline route root did not mount") + initialRoute = route + initialViewport = viewport + initialSurface = surface + viewport.addEventListener("scroll", (event) => { + domScrollEvents++ + lastScrollTrusted = event.isTrusted + }) + const countFrames = () => { + browserFrame++ + requestAnimationFrame(countFrames) + } + requestAnimationFrame(countFrames) + new MutationObserver((records) => { + const callbackTime = performance.now() + records.forEach((record) => { + ;([...(record.removedNodes ?? [])] as Node[]).forEach((node) => { + if (node !== route) return + phase = "detached" + mutationEvents.push({ + kind: "removed", + callbackTime, + callbackFrame: browserFrame, + routeConnectedInCallback: route.isConnected, + nativeOffsetInCallback: viewport.scrollTop, + }) + }) + ;([...(record.addedNodes ?? [])] as Node[]).forEach((node) => { + if (node !== route) return + phase = "reinserted" + mutationEvents.push({ + kind: "added", + callbackTime, + callbackFrame: browserFrame, + routeConnectedInCallback: route.isConnected, + nativeOffsetInCallback: viewport.scrollTop, + }) + }) + }) + }).observe(routeRoot, { childList: true }) + window.timelineSuspense = { + prepare: async () => { + phase = "preparing" + await frames(2) + viewport.scrollTop = viewport.scrollHeight + await frames(3) + await new Promise((resolve) => setTimeout(resolve, 200)) + await frames(2) + initialRows = mountedRows() + phase = "prepared" + return snapshot() + }, + trigger: () => { + phase = "triggering" + setRefresh(true) + }, + resolve: () => { + if (!resolveResource) throw new Error("Resource is not pending") + phase = "resolving" + resolveResource() + }, + frames, + snapshot, + } + }) + + return ( +
+ +
+
+ + {(item) => ( +
+ logical row {item.index} +
+ )} +
+
+
+
+ ) + } + + return ( +
+ + + +
+ ) +} + +render(() => , document.getElementById("root")!) diff --git a/packages/app/e2e/reproduction/timeline-suspense/playwright.config.ts b/packages/app/e2e/reproduction/timeline-suspense/playwright.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fff1737aef1bfc33ced5401a44a58109cc30f04 --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/playwright.config.ts @@ -0,0 +1,34 @@ +import { defineConfig, devices } from "@playwright/test" + +const port = Number(process.env.PLAYWRIGHT_TIMELINE_SUSPENSE_PORT ?? 4317) + +export default defineConfig({ + testDir: ".", + testMatch: "timeline-suspense.repro.ts", + outputDir: "../../test-results/timeline-suspense", + fullyParallel: false, + workers: 1, + retries: 0, + reporter: "line", + timeout: 30_000, + expect: { + timeout: 10_000, + }, + webServer: { + command: `bunx vite --config vite.config.ts --host 127.0.0.1 --port ${port} --strictPort`, + cwd: import.meta.dirname, + url: `http://127.0.0.1:${port}`, + reuseExistingServer: false, + }, + use: { + baseURL: `http://127.0.0.1:${port}`, + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}) diff --git a/packages/app/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts b/packages/app/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts new file mode 100644 index 0000000000000000000000000000000000000000..3edae33a9ba66bac940eac8a749a2951d064ed2c --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/timeline-suspense.repro.ts @@ -0,0 +1,179 @@ +import { expect, test, type Page } from "@playwright/test" + +test.beforeEach(async ({ page }) => { + page.on("pageerror", (error) => console.error(error)) + await page.goto("/") + await expect.poll(() => page.evaluate(() => !!window.timelineSuspense)).toBe(true) +}) + +test("desired: preserves visible timeline continuity across descendant resource suspension", async ({ page }) => { + await page.goto("/?reconnect=candidate") + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().mode.reconnect)).toBe("candidate") + const before = await prepare(page) + await triggerBaselineSuspension(page) + const pending = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(pending.nativeOffset).toBe(0) + expect(pending.coreOffset).toBe(before.coreOffset) + expect(pending.indexes).toEqual(before.indexes) + expect(pending.sameRoute).toBe(true) + expect(pending.sameViewport).toBe(true) + expect(pending.sameSurface).toBe(true) + expect(pending.sameMountedRows).toBe(true) + + await resolveSuspension(page) + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().coreOffset)).toBe(0) + await page.waitForTimeout(250) + await page.evaluate(() => window.timelineSuspense.frames(2)) + const after = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(after.sameRoute).toBe(true) + expect(after.sameViewport).toBe(true) + expect(after.sameSurface).toBe(true) + expect(after.nativeOffset).toBe(0) + expect(after.coreOffset).toBe(0) + expect(after.rangeStart).toBeLessThan(10) + expect(after.visibleRows, diagnostic({ before, pending, after })).toBeGreaterThan(0) + expect(after.domScrollEvents).toBe(before.domScrollEvents) + expect(after.coreOffsetCallbackCalls).toBe(before.coreOffsetCallbackCalls + 1) + expect(after.offsetCallbackSources.at(-1)).toBe("observer") + expect(after.syntheticScrollDispatches).toBe(0) +}) + +test("forensic: proves detached same-node viewport leaves TanStack's bottom range blank until a real scroll", async ({ + page, +}) => { + const before = await prepare(page) + const beforeRows = before.domIndexes + expect(before.mode).toEqual({ resource: "baseline", reconnect: "baseline" }) + expect(before.logicalSurfaceHeight).toBe(80_000) + expect(before.renderedSurfaceHeight).toBe(80_000) + expect(before.viewportClientHeight).toBe(600) + expect(before.viewportScrollHeight).toBe(80_000) + expect(before.rangeStart).toBeGreaterThan(1_900) + expect(before.nativeOffset).toBe(before.coreOffset) + expect(before.visibleRows).toBeGreaterThan(0) + + await triggerBaselineSuspension(page) + const pending = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(pending.resourceState).toBe("refreshing") + expect(pending.routeConnected).toBe(false) + expect(pending.viewportConnected).toBe(false) + expect(pending.viewportOwnedByRoute).toBe(true) + expect(pending.nativeOffset).toBe(0) + expect(pending.coreOffset).toBe(before.coreOffset) + expect(pending.rangeStart).toBe(before.rangeStart) + expect(pending.rangeEnd).toBe(before.rangeEnd) + expect(pending.indexes).toEqual(before.indexes) + expect(pending.domIndexes).toEqual(beforeRows) + expect(pending.sameMountedRows).toBe(true) + expect(pending.domScrollEvents).toBe(before.domScrollEvents) + expect(pending.coreOffsetCallbackCalls).toBe(before.coreOffsetCallbackCalls) + expect(pending.ignoredDetachedZeroRects).toBeGreaterThan(before.ignoredDetachedZeroRects) + expect(pending.mutationEvents).toHaveLength(1) + expect(pending.mutationEvents[0]).toMatchObject({ + kind: "removed", + routeConnectedInCallback: false, + nativeOffsetInCallback: 0, + }) + expect(pending.mutationEvents[0]!.callbackTime).toBeLessThanOrEqual(pending.operation.time) + expect(pending.mutationEvents[0]!.callbackFrame).toBeLessThanOrEqual(pending.operation.frame) + + const after = await resolveSuspension(page) + expect(after.resourceState).toBe("ready") + expect(after.routeConnected).toBe(true) + expect(after.viewportConnected).toBe(true) + expect(after.viewportOwnedByRoute).toBe(true) + expect(after.sameRoute).toBe(true) + expect(after.sameViewport).toBe(true) + expect(after.sameSurface).toBe(true) + expect(after.sameMountedRows).toBe(true) + expect(after.nativeOffset).toBe(0) + expect(after.coreOffset).toBe(before.coreOffset) + expect(after.rangeStart).toBe(before.rangeStart) + expect(after.rangeEnd).toBe(before.rangeEnd) + expect(after.indexes).toEqual(before.indexes) + expect(after.domIndexes).toEqual(beforeRows) + expect(after.domScrollEvents).toBe(before.domScrollEvents) + expect(after.coreOffsetCallbackCalls).toBe(before.coreOffsetCallbackCalls) + expect(after.mutationEvents).toHaveLength(2) + expect(after.mutationEvents[1]).toMatchObject({ + kind: "added", + routeConnectedInCallback: true, + nativeOffsetInCallback: 0, + }) + expect(after.mutationEvents[1]!.callbackTime).toBeLessThanOrEqual(after.operation.time) + expect(after.mutationEvents[1]!.callbackFrame).toBeLessThanOrEqual(after.operation.frame) + expect(after.visibleRows).toBe(0) + expect(after.minimumRowTop).toBeGreaterThan(50_000) + expect(after.syntheticScrollDispatches).toBe(0) + + await page.locator("[data-viewport]").hover() + await page.mouse.wheel(0, 80) + await expect + .poll(() => + page.evaluate(() => { + const value = window.timelineSuspense.snapshot() + return value.nativeOffset > 0 && value.coreOffset === value.nativeOffset + }), + ) + .toBe(true) + await page.evaluate(() => window.timelineSuspense.frames(2)) + const recovered = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(recovered.domScrollEvents).toBeGreaterThan(after.domScrollEvents) + expect(recovered.coreOffsetCallbackCalls).toBeGreaterThan(after.coreOffsetCallbackCalls) + expect(recovered.offsetCallbackSources.at(-1)).toBe("observer") + expect(recovered.lastScrollTrusted).toBe(true) + expect(recovered.rangeStart).toBeLessThan(10) + expect(recovered.visibleRows).toBeGreaterThan(0) +}) + +test("matrix: fixture-only settled-resource guard keeps the route connected", async ({ page }) => { + await page.goto("/?resource=guard") + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().mode.resource)).toBe("guard") + const before = await prepare(page) + + await page.evaluate(() => window.timelineSuspense.trigger()) + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().resourceState)).toBe("refreshing") + await page.evaluate(() => window.timelineSuspense.frames(3)) + const pending = await page.evaluate(() => window.timelineSuspense.snapshot()) + expect(pending.routeConnected).toBe(true) + expect(pending.mutationEvents).toEqual([]) + expect(pending.nativeOffset).toBe(before.nativeOffset) + expect(pending.coreOffset).toBe(before.coreOffset) + expect(pending.visibleRows).toBeGreaterThan(0) + + const after = await resolveSuspension(page) + expect(after.routeConnected).toBe(true) + expect(after.nativeOffset).toBe(before.nativeOffset) + expect(after.coreOffset).toBe(before.coreOffset) + expect(after.visibleRows).toBeGreaterThan(0) +}) + +async function prepare(page: Page) { + const before = await page.evaluate(() => window.timelineSuspense.prepare()) + expect(before.routeConnected).toBe(true) + expect(before.viewportConnected).toBe(true) + expect(before.viewportOwnedByRoute).toBe(true) + expect(before.sameMountedRows).toBe(true) + expect(before.rangeStart).toBeGreaterThan(1_900) + expect(before.nativeOffset).toBe(before.coreOffset) + return before +} + +async function triggerBaselineSuspension(page: Page) { + await page.evaluate(() => window.timelineSuspense.trigger()) + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().resourceState)).toBe("refreshing") + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().routeConnected)).toBe(false) + await page.evaluate(() => window.timelineSuspense.frames(3)) +} + +async function resolveSuspension(page: Page) { + await page.evaluate(() => window.timelineSuspense.resolve()) + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().resourceState)).toBe("ready") + await expect.poll(() => page.evaluate(() => window.timelineSuspense.snapshot().routeConnected)).toBe(true) + await page.evaluate(() => window.timelineSuspense.frames(3)) + return page.evaluate(() => window.timelineSuspense.snapshot()) +} + +function diagnostic(value: unknown) { + return JSON.stringify(value, null, 2) +} diff --git a/packages/app/e2e/reproduction/timeline-suspense/vite.config.ts b/packages/app/e2e/reproduction/timeline-suspense/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..efb75ab731c6df6d925605976e74479cf1488e73 --- /dev/null +++ b/packages/app/e2e/reproduction/timeline-suspense/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite" +import solid from "vite-plugin-solid" + +export default defineConfig({ + root: import.meta.dirname, + plugins: [solid()], +}) diff --git a/packages/app/e2e/tsconfig.json b/packages/app/e2e/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..4a6046e4faaf428f842ec5c48f4981df5c142c0a --- /dev/null +++ b/packages/app/e2e/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "..", + "types": ["node", "bun"] + }, + "include": [ + "./performance/timeline-stability/**/*.spec.ts", + "./performance/timeline-stability/fixture.test.ts", + "./performance/timeline-stability/fixture.ts", + "./performance/unit/visual-stability.test.ts", + "./reproduction/timeline-suspense/**/*.ts", + "./reproduction/timeline-suspense/**/*.tsx", + "../src/pages/session/timeline/observe-element-offset.ts", + "./regression/new-session-panel-corner.spec.ts", + "./regression/session-timeline-context-resize.spec.ts", + "./utils/**/*.ts" + ] +} diff --git a/packages/app/e2e/utils/visual-stability/analyzer.ts b/packages/app/e2e/utils/visual-stability/analyzer.ts new file mode 100644 index 0000000000000000000000000000000000000000..902d222e1da87f157861a83b63a7077391c2d2c5 --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/analyzer.ts @@ -0,0 +1,209 @@ +import type { VisualInvariant, VisualPlan } from "./invariant" +import type { VisualObservation, VisualStabilityTrace } from "./model" + +export function analyzeVisualObservations( + observations: readonly VisualObservation[], + plan: VisualPlan, +) { + const issues: string[] = [] + const invariants = plan.invariants + const names = [...new Set(observations.flatMap((sample) => Object.keys(sample.regions) as RegionName[]))] + const required = regions(invariants, "required") + const continuousAny = invariants.filter( + (invariant): invariant is Extract, { type: "continuous-any" }> => + invariant.type === "continuous-any", + ) + const unique = new Set(regions(invariants, "unique")) + const stable = new Set(regions(invariants, "stable")) + const fixed = invariants.filter( + (invariant): invariant is Extract, { type: "fixed" }> => invariant.type === "fixed", + ) + const opacity = invariants.filter( + (invariant): invariant is Extract, { type: "opacity" }> => invariant.type === "opacity", + ) + const continuity = invariants.filter( + (invariant): invariant is Extract, { type: "continuity" }> => + invariant.type === "continuity", + ) + const motion = invariants.filter( + (invariant): invariant is Extract, { type: "motion" }> => invariant.type === "motion", + ) + const labelStability = invariants.filter( + (invariant): invariant is Extract, { type: "label-stability" }> => + invariant.type === "label-stability", + ) + + for (const name of new Set(required)) { + if (!observations.some((sample) => sample.regions[name]?.visible)) issues.push(`${name} never rendered`) + } + for (const invariant of continuousAny) { + if (!invariant.regions.some((name) => observations.some((sample) => sample.regions[name]?.visible))) + issues.push(`${invariant.regions.join(" | ")} never rendered`) + } + + for (const name of names) { + const samples = observations.flatMap((observation) => { + const region = observation.regions[name] + if (!region) return [] + const clipped = + observation.viewport && (region.bottom <= observation.viewport.top || region.top >= observation.viewport.bottom) + return [{ at: observation.at, ...region, visible: region.visible && !clipped }] + }) + const visible = samples.filter((sample) => sample.visible) + if (visible.length === 0) continue + if (unique.has(name)) { + const duplicate = samples.find((sample) => sample.count > 1) + if (duplicate) issues.push(`${name} appeared ${duplicate.count} times at ${Math.round(duplicate.at)}ms`) + } + if (stable.has(name)) { + const identities = [...new Set(visible.map((sample) => sample.node).filter((node) => node > 0))] + if (identities.length > 1) issues.push(`${name} remounted ${identities.length - 1} times`) + } + for (const invariant of fixed.filter((invariant) => includes(invariant.regions, name))) { + const origin = visible[0] + const movement = origin ? Math.max(0, ...visible.map((sample) => Math.abs(sample.top - origin.top))) : 0 + if (movement > (invariant.tolerance ?? 1)) + issues.push(`${name} moved ${Math.round(movement * 10) / 10}px in the viewport`) + } + for (const invariant of opacity.filter((invariant) => includes(invariant.regions, name))) { + for (const sample of visible) { + if (sample.opacity < (invariant.floor ?? 0.65)) + issues.push(`${name} opacity fell to ${sample.opacity} at ${Math.round(sample.at)}ms`) + } + } + if (continuity.some((invariant) => includes(invariant.regions, name))) { + const firstPresent = samples.findIndex((sample) => sample.present) + const lastPresent = samples.findLastIndex((sample) => sample.present) + if (samples.slice(firstPresent, lastPresent + 1).some((sample) => !sample.present)) + issues.push(`${name} disappeared between present frames`) + const firstVisible = samples.findIndex((sample) => sample.visible) + const lastVisible = samples.findLastIndex((sample) => sample.visible) + if ( + firstVisible >= 0 && + samples.slice(firstVisible, lastVisible + 1).some((sample) => !sample.visible && sample.inViewport) + ) + issues.push(`${name} blanked between visible frames`) + } + for (const invariant of motion.filter((invariant) => includes(invariant.regions, name))) { + for (const metric of ["top", "bottom", "width", "height"] as const) { + const directions = visible + .slice(1) + .map((sample, index) => sample[metric] - visible[index]![metric]) + .filter((delta) => Math.abs(delta) > (invariant.tolerance ?? 1)) + .map(Math.sign) + const reversals = directions.slice(1).filter((direction, index) => direction !== directions[index]).length + const allowed = + metric === "top" || metric === "bottom" + ? (invariant.maxPositionReversals ?? invariant.maxReversals ?? 1) + : (invariant.maxReversals ?? 1) + if (reversals > allowed) issues.push(`${name} ${metric} reversed ${reversals} times`) + } + } + if (labelStability.some((invariant) => includes(invariant.regions, name))) { + const labels = samples + .map((sample) => sample.label) + .filter((label) => label.length > 0) + .filter((label, index, all) => label !== all[index - 1]) + if (labels.some((label, index) => labels.indexOf(label) !== index)) + issues.push(`${name} label reverted: ${labels.join(" -> ")}`) + } + } + + if (invariants.some((invariant) => invariant.type === "preserve-bottom-anchor")) { + const viewports = observations.flatMap((sample) => (sample.viewport ? [sample.viewport] : [])) + if (viewports[0] && viewports[0].distanceFromBottom <= 4) { + const lost = viewports.find((viewport) => viewport.distanceFromBottom > 4) + if (lost) issues.push(`bottom anchor moved to ${lost.distanceFromBottom}px`) + } + } + if (invariants.some((invariant) => invariant.type === "acquire-bottom-anchor")) { + const final = observations.findLast((sample) => sample.viewport)?.viewport + if (!final || final.distanceFromBottom > 4) + issues.push(`did not acquire bottom anchor${final ? ` (${final.distanceFromBottom}px away)` : ""}`) + } + + for (const invariant of continuousAny) { + const active = observations.map((sample) => invariant.regions.some((name) => sample.regions[name]?.visible)) + const first = active.indexOf(true) + const last = active.lastIndexOf(true) + if (first >= 0 && active.slice(first, last + 1).some((value) => !value)) + issues.push(`${invariant.regions.join(" | ")} blanked between visible frames`) + } + + for (const invariant of invariants.filter( + (item): item is Extract, { type: "flow" }> => item.type === "flow", + )) { + for (const [before, after] of invariant.regions + .slice(1) + .map((after, index) => [invariant.regions[index]!, after])) { + let maximum: { overlap: number; at: number } | undefined + let inverted: { at: number } | undefined + for (const sample of observations) { + const first = sample.regions[before] + const second = sample.regions[after] + if (!first?.visible || !second?.visible) continue + if ( + sample.viewport && + (first.bottom <= sample.viewport.top || + first.top >= sample.viewport.bottom || + second.bottom <= sample.viewport.top || + second.top >= sample.viewport.bottom) + ) + continue + const overlap = first.bottom - second.top + if (first.top > second.top && !inverted) inverted = { at: sample.at } + if (overlap > (invariant.overlapTolerance ?? 0.5) && (!maximum || overlap > maximum.overlap)) + maximum = { overlap, at: sample.at } + } + if (inverted) issues.push(`${before} rendered after ${after} at ${Math.round(inverted.at)}ms`) + if (maximum) + issues.push( + `${before} overlapped ${after} by ${Math.round(maximum.overlap * 10) / 10}px at ${Math.round(maximum.at)}ms`, + ) + } + } + return [...new Set(issues)] +} + +export function analyzeVisualTraceByMarker( + trace: VisualStabilityTrace, + plan: VisualPlan, +) { + if (trace.markers.length === 0) return analyzeVisualObservations(trace.samples, plan) + const required = [...new Set(plan.markerRequired ?? regions(plan.invariants, "required"))].flatMap((name) => + trace.samples.some((sample) => sample.regions[name]?.visible) ? [] : [`${name} never rendered`], + ) + const withoutRequired = plan.invariants.filter((invariant) => invariant.type !== "required") + const windows = trace.markers.flatMap((marker, index) => { + const end = trace.markers[index + 1]?.at ?? Infinity + const before = trace.samples.findLast((sample) => sample.at < marker.at) + const samples = [ + ...(before ? [before] : []), + ...trace.samples.filter((sample) => sample.at >= marker.at && sample.at < end), + ] + if (samples.length < 2) return [] + return analyzeVisualObservations(samples, { ...plan, perMarker: false, invariants: withoutRequired }).map( + (issue) => `${marker.label}: ${issue}`, + ) + }) + const aggregateMotion = + plan.aggregateMotion === false + ? [] + : analyzeVisualObservations(trace.samples, { + invariants: plan.invariants.filter((invariant) => invariant.type === "motion"), + }).filter((issue) => / (?:top|bottom|width|height) reversed \d+ times$/.test(issue)) + return [...new Set([...required, ...aggregateMotion, ...windows])] +} + +function regions["type"]>( + invariants: readonly VisualInvariant[], + type: Type, +) { + return invariants.flatMap((invariant) => + invariant.type === type && "regions" in invariant && invariant.regions !== "all" ? [...invariant.regions] : [], + ) as RegionName[] +} + +function includes(regions: readonly RegionName[] | "all", name: RegionName) { + return regions === "all" || regions.includes(name) +} diff --git a/packages/app/e2e/utils/visual-stability/capture.ts b/packages/app/e2e/utils/visual-stability/capture.ts new file mode 100644 index 0000000000000000000000000000000000000000..3bb583a9d5a4a0c38dfcb8fbb26ba88d373ef7bb --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/capture.ts @@ -0,0 +1,51 @@ +import type { CDPSession, Page } from "@playwright/test" +import type { CapturedFrame } from "./model" + +export type VisualCapture = { + session: CDPSession + frames: CapturedFrame[] + startedAtEpoch: number + running: boolean + capture: Promise +} + +export async function startVisualCapture(page: Page, startedAtEpoch: number) { + if (process.env.OPENCODE_STABILITY_CAPTURE !== "1") return + const session = await page.context().newCDPSession(page) + await session.send("Page.enable") + const recording: VisualCapture = { + session, + frames: [], + startedAtEpoch, + running: true, + capture: Promise.resolve(), + } + recording.capture = (async () => { + try { + while (recording.running && recording.frames.length < 900) { + const frame = await session.send("Page.captureScreenshot", { + format: "jpeg", + quality: 80, + captureBeyondViewport: false, + optimizeForSpeed: true, + }) + recording.frames.push({ at: Date.now() - recording.startedAtEpoch, data: frame.data }) + await new Promise((resolve) => setTimeout(resolve, 50)) + } + } catch { + recording.running = false + } + })() + return recording +} + +export async function stopVisualCapture(recording: VisualCapture | undefined) { + if (!recording) return [] + recording.running = false + try { + await recording.capture + } finally { + await recording.session.detach().catch(() => undefined) + } + return recording.frames +} diff --git a/packages/app/e2e/utils/visual-stability/index.ts b/packages/app/e2e/utils/visual-stability/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6fdbf4acfc6055f08f0f3c105632b805f2f214f9 --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/index.ts @@ -0,0 +1,8 @@ +export * from "./analyzer" +export * from "./capture" +export * from "./invariant" +export * from "./model" +export * from "./probe" +export * from "./regions" +export * from "./reporter" +export * from "./scenario" diff --git a/packages/app/e2e/utils/visual-stability/invariant.ts b/packages/app/e2e/utils/visual-stability/invariant.ts new file mode 100644 index 0000000000000000000000000000000000000000..17f6ef191361523578119c6a3588c1037c5d1bcb --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/invariant.ts @@ -0,0 +1,112 @@ +import type { VisualRegionDefinition } from "./regions" + +type RegionSet = readonly RegionName[] | "all" + +export type VisualInvariant = + | { type: "required"; regions: readonly RegionName[] } + | { type: "continuous-any"; regions: readonly RegionName[] } + | { type: "unique"; regions: readonly RegionName[] } + | { type: "stable"; regions: readonly RegionName[] } + | { type: "fixed"; regions: readonly RegionName[]; tolerance?: number } + | { type: "opacity"; regions: RegionSet; floor?: number } + | { + type: "motion" + regions: RegionSet + tolerance?: number + maxReversals?: number + maxPositionReversals?: number + } + | { type: "continuity"; regions: RegionSet } + | { type: "label-stability"; regions: RegionSet } + | { type: "flow"; regions: readonly RegionName[]; overlapTolerance?: number } + | { type: "preserve-bottom-anchor" } + | { type: "acquire-bottom-anchor" } + +export type VisualPlan = { + regionNames?: readonly RegionName[] + invariants: readonly VisualInvariant[] + markerRequired?: readonly RegionName[] + perMarker?: boolean + aggregateMotion?: boolean +} + +export type LegacyVisualStabilityOptions = { + flow?: RegionName[] + motionTolerance?: number + opacityFloor?: number + overlapTolerance?: number + maxReversals?: number + maxPositionReversals?: number + stable?: RegionName[] + fixed?: RegionName[] + motion?: RegionName[] + unique?: RegionName[] + preserveBottomAnchor?: boolean + acquireBottomAnchor?: boolean + perMarker?: boolean + continuousAny?: RegionName[][] + required?: RegionName[] + aggregateMotion?: boolean + inferRequired?: boolean +} + +export function visualPlan>( + regions: Regions, + invariants: readonly VisualInvariant>[], + options: Omit>, "regionNames" | "invariants"> = {}, +): VisualPlan> { + return { ...options, regionNames: Object.keys(regions) as Extract[], invariants } +} + +export function legacyVisualPlan( + options: LegacyVisualStabilityOptions = {}, +): VisualPlan { + const inferred = + options.inferRequired === false + ? [] + : [ + ...(options.stable ?? []), + ...(options.fixed ?? []), + ...(options.unique ?? []), + ...(options.motion ?? []), + ...(options.flow ?? []), + ] + return { + perMarker: options.perMarker, + aggregateMotion: options.aggregateMotion, + markerRequired: [ + ...(options.required ?? []), + ...(options.stable ?? []), + ...(options.fixed ?? []), + ...(options.unique ?? []), + ...(options.motion ?? []), + ...(options.flow ?? []), + ], + invariants: [ + { type: "required", regions: [...(options.required ?? []), ...inferred] }, + ...(options.continuousAny ?? []).map( + (regions): VisualInvariant => ({ type: "continuous-any", regions }), + ), + ...(options.unique ? [{ type: "unique" as const, regions: options.unique }] : []), + ...(options.stable ? [{ type: "stable" as const, regions: options.stable }] : []), + ...(options.fixed + ? [{ type: "fixed" as const, regions: options.fixed, tolerance: options.motionTolerance }] + : []), + { type: "opacity", regions: "all", floor: options.opacityFloor }, + { type: "continuity", regions: "all" }, + { + type: "motion", + regions: options.motion ?? "all", + tolerance: options.motionTolerance, + maxReversals: options.maxReversals, + maxPositionReversals: options.maxPositionReversals, + }, + { type: "label-stability", regions: "all" }, + ...(options.preserveBottomAnchor ? [{ type: "preserve-bottom-anchor" as const }] : []), + ...(options.acquireBottomAnchor ? [{ type: "acquire-bottom-anchor" as const }] : []), + ...(options.flow + ? [{ type: "flow" as const, regions: options.flow, overlapTolerance: options.overlapTolerance }] + : []), + ], + } +} diff --git a/packages/app/e2e/utils/visual-stability/model.ts b/packages/app/e2e/utils/visual-stability/model.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b966638c9ce179409dffd756a188e2767813113 --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/model.ts @@ -0,0 +1,47 @@ +export type VisualRegionSample = { + present: boolean + visible: boolean + inViewport: boolean + cssHidden?: boolean + top: number + bottom: number + layoutTop?: number + layoutBottom?: number + width: number + height: number + opacity: number + count: number + node: number + label: string + text: string +} + +export type VisualViewportSample = { + top: number + bottom: number + scrollTop: number + scrollHeight: number + clientHeight: number + distanceFromBottom: number +} + +export type VisualObservation = { + at: number + regions: string extends RegionName + ? Record + : Partial> + viewport?: VisualViewportSample +} + +export type VisualMarker = { at: number; label: string } + +export type VisualStabilityTrace = { + markers: VisualMarker[] + samples: VisualObservation[] +} + +export type CapturedFrame = { at: number; data: string } + +export type VisualProbeResult = VisualStabilityTrace & { + frames: CapturedFrame[] +} diff --git a/packages/app/e2e/utils/visual-stability/probe.ts b/packages/app/e2e/utils/visual-stability/probe.ts new file mode 100644 index 0000000000000000000000000000000000000000..abf274d3592a85ac068bc6c50769430cd26c65c8 --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/probe.ts @@ -0,0 +1,226 @@ +import type { Page } from "@playwright/test" +import { startVisualCapture, stopVisualCapture, type VisualCapture } from "./capture" +import type { VisualMarker, VisualObservation, VisualProbeResult } from "./model" +import type { VisualRegionDefinition } from "./regions" + +type ProbeWindow = Window & { + __visualStabilityProbe?: { + startedAt: number + markers: VisualMarker[] + samples: VisualObservation[] + stop: () => void + } +} + +const captures = new WeakMap() + +export async function startVisualProbe>( + page: Page, + regions: Regions, +) { + await stopCapture(page) + await page.evaluate(() => { + ;(window as ProbeWindow).__visualStabilityProbe?.stop() + }) + const startedAtEpoch = await page.evaluate((regions) => { + const samples: VisualObservation[] = [] + const markers: VisualMarker[] = [] + const startedAt = performance.now() + const nodes = new WeakMap() + const lastBounds = new Map() + let nextNode = 1 + let running = true + const round = (value: number) => Math.round(value * 10) / 10 + const opacity = (element: Element) => Number(getComputedStyle(element).opacity) + const sample = () => { + if (!running) return + setTimeout(() => { + if (!running) return + const viewport = [...document.querySelectorAll(".scroll-view__viewport")].find((element) => + element.querySelector("[data-timeline-row]"), + ) + const viewportRect = viewport?.getBoundingClientRect() + samples.push({ + at: performance.now() - startedAt, + viewport: viewport + ? { + top: round(viewportRect!.top), + bottom: round(viewportRect!.bottom), + scrollTop: round(viewport.scrollTop), + scrollHeight: round(viewport.scrollHeight), + clientHeight: round(viewport.clientHeight), + distanceFromBottom: round(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop), + } + : undefined, + regions: Object.fromEntries( + Object.entries(regions).map(([name, config]) => { + const found = document.querySelector(config.selector) + const count = document.querySelectorAll(config.selector).length + const element = config.closest ? found?.closest(config.closest) : found + if (!element) + return [ + name, + { + present: false, + visible: false, + inViewport: false, + top: 0, + bottom: 0, + width: 0, + height: 0, + opacity: 0, + count, + node: 0, + label: "", + text: "", + }, + ] + const rect = element.getBoundingClientRect() + const style = getComputedStyle(element) + if (rect.height > 0) lastBounds.set(name, { top: rect.top, bottom: rect.bottom }) + const known = rect.height > 0 ? rect : lastBounds.get(name) + const painted = (() => { + const result = { top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right } + let parent = element.parentElement + while (parent) { + const parentStyle = getComputedStyle(parent) + if (["hidden", "clip", "scroll", "auto"].includes(parentStyle.overflowY)) { + const parentRect = parent.getBoundingClientRect() + result.top = Math.max(result.top, parentRect.top) + result.bottom = Math.min(result.bottom, parentRect.bottom) + } + if (["hidden", "clip", "scroll", "auto"].includes(parentStyle.overflowX)) { + const parentRect = parent.getBoundingClientRect() + result.left = Math.max(result.left, parentRect.left) + result.right = Math.min(result.right, parentRect.right) + } + if (parent === viewport) break + parent = parent.parentElement + } + if (viewportRect) { + result.top = Math.max(result.top, viewportRect.top) + result.bottom = Math.min(result.bottom, viewportRect.bottom) + result.left = Math.max(result.left, viewportRect.left) + result.right = Math.min(result.right, viewportRect.right) + } + return result + })() + const contentOpacity = config.opacitySelectors?.length + ? Math.max( + 0, + ...config.opacitySelectors.flatMap((selector) => + [...element.querySelectorAll(selector)].map((node) => { + let value = 1 + let current: Element | null = node + while (current) { + value *= opacity(current) + if (current === element) break + current = current.parentElement + } + return value + }), + ), + ) + : opacity(element) + let visibleOpacity = contentOpacity + let ancestor = element.parentElement + let ancestorHidden = false + while (ancestor) { + const ancestorStyle = getComputedStyle(ancestor) + visibleOpacity *= Number(ancestorStyle.opacity) + if (ancestorStyle.display === "none" || ancestorStyle.visibility === "hidden") ancestorHidden = true + if (ancestor === viewport) break + ancestor = ancestor.parentElement + } + const cssHidden = + ancestorHidden || style.display === "none" || style.visibility === "hidden" || visibleOpacity === 0 + return [ + name, + { + present: true, + visible: + style.display !== "none" && + style.visibility !== "hidden" && + visibleOpacity > 0 && + painted.right > painted.left && + painted.bottom > painted.top, + inViewport: + !viewportRect || (!!known && known.bottom > viewportRect.top && known.top < viewportRect.bottom), + cssHidden, + top: round(painted.top), + bottom: round(painted.bottom), + layoutTop: round(rect.top), + layoutBottom: round(rect.bottom), + width: round(painted.right - painted.left), + height: round(painted.bottom - painted.top), + opacity: round(visibleOpacity), + count, + node: (() => { + const current = nodes.get(element) + if (current) return current + nodes.set(element, nextNode) + return nextNode++ + })(), + label: element.getAttribute("aria-label") ?? "", + text: (element.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 500), + }, + ] + }), + ), + }) + requestAnimationFrame(sample) + }, 0) + } + ;(window as ProbeWindow).__visualStabilityProbe = { + startedAt, + markers, + samples, + stop: () => { + running = false + }, + } + requestAnimationFrame(sample) + return new Promise((resolve) => { + const ready = () => { + if (samples.length > 0) return resolve(performance.timeOrigin + startedAt) + requestAnimationFrame(ready) + } + ready() + }) + }, regions) + const capture = await startVisualCapture(page, startedAtEpoch) + if (capture) captures.set(page, capture) +} + +export async function stopVisualProbe( + page: Page, +): Promise> { + return page + .evaluate(() => { + const probe = (window as ProbeWindow).__visualStabilityProbe + if (!probe) throw new Error("Visual stability probe is not running") + probe.stop() + return { markers: probe.markers, samples: probe.samples } + }) + .then( + async (trace) => ({ ...trace, frames: await stopCapture(page) }) as unknown as VisualProbeResult, + async (error: unknown) => { + await stopCapture(page) + throw error + }, + ) +} + +export async function markVisualProbe(page: Page, label: string) { + await page.evaluate((label) => { + const probe = (window as ProbeWindow).__visualStabilityProbe + if (!probe) return + probe.markers.push({ at: performance.now() - probe.startedAt, label }) + }, label) +} + +async function stopCapture(page: Page) { + const capture = captures.get(page) + if (capture) captures.delete(page) + return stopVisualCapture(capture) +} diff --git a/packages/app/e2e/utils/visual-stability/regions.ts b/packages/app/e2e/utils/visual-stability/regions.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c0b34e78a2a3ca80db4082a0da12b114558c278 --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/regions.ts @@ -0,0 +1,18 @@ +export type VisualRegionDefinition = { + selector: string + closest?: string + opacitySelectors?: readonly string[] +} + +export function defineVisualRegions>(regions: Regions) { + return regions +} + +export function mapVisualRegions, Result>( + regions: Regions, + map: (region: Regions[keyof Regions], name: keyof Regions) => Result, +) { + return Object.fromEntries( + Object.entries(regions).map(([name, region]) => [name, map(region as Regions[keyof Regions], name)]), + ) as { [Name in keyof Regions]: Result } +} diff --git a/packages/app/e2e/utils/visual-stability/reporter.ts b/packages/app/e2e/utils/visual-stability/reporter.ts new file mode 100644 index 0000000000000000000000000000000000000000..db283072a846eedc8130d4231780b77090663ce8 --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/reporter.ts @@ -0,0 +1,63 @@ +import { expect, type TestInfo } from "@playwright/test" +import { writeFile } from "node:fs/promises" +import { analyzeVisualObservations, analyzeVisualTraceByMarker } from "./analyzer" +import type { VisualPlan } from "./invariant" +import type { VisualProbeResult } from "./model" + +export async function reportVisualStability( + testInfo: TestInfo, + name: string, + result: VisualProbeResult, + plan: VisualPlan, +) { + const trace = { markers: result.markers, samples: result.samples } + const issues = plan.perMarker + ? analyzeVisualTraceByMarker(trace, plan) + : analyzeVisualObservations(result.samples, plan) + const tracePath = testInfo.outputPath(`${name}-visual-trace.json`) + const issuesPath = testInfo.outputPath(`${name}-visual-issues.json`) + await writeFile(tracePath, JSON.stringify(trace, null, 2)) + await writeFile( + issuesPath, + JSON.stringify({ issues, markers: result.markers, capturedFrameCount: result.frames.length }, null, 2), + ) + await testInfo.attach(`${name}-visual-trace`, { path: tracePath, contentType: "application/json" }) + await testInfo.attach(`${name}-visual-issues`, { path: issuesPath, contentType: "application/json" }) + if (issues.length) await attachViolationFrames(testInfo, name, result, issues) + expect(issues, `${name}: ${issues.join("\n")}`).toEqual([]) +} + +async function attachViolationFrames( + testInfo: TestInfo, + name: string, + result: VisualProbeResult, + issues: string[], +) { + if (result.frames.length === 0) return + const targets = [ + ...new Set( + issues.flatMap((issue) => { + const match = issue.match(/ at (\d+)ms/) + if (match) return [Number(match[1])] + const marker = result.markers.find((item) => issue.startsWith(`${item.label}:`)) + return marker ? [marker.at] : [] + }), + ), + ].slice(0, 6) + for (const [violation, target] of targets.entries()) { + const nearest = result.frames.reduce( + (best, frame, index) => (Math.abs(frame.at - target) < Math.abs(result.frames[best]!.at - target) ? index : best), + 0, + ) + for (const [label, index] of [ + ["before", Math.max(0, nearest - 1)], + ["violation", nearest], + ["after", Math.min(result.frames.length - 1, nearest + 1)], + ] as const) { + await testInfo.attach(`${name}-${violation + 1}-${label}-${Math.round(result.frames[index]!.at)}ms`, { + body: Buffer.from(result.frames[index]!.data, "base64"), + contentType: "image/jpeg", + }) + } + } +} diff --git a/packages/app/e2e/utils/visual-stability/scenario.ts b/packages/app/e2e/utils/visual-stability/scenario.ts new file mode 100644 index 0000000000000000000000000000000000000000..2136302071443bc4c540455fb11414f64f44f8ae --- /dev/null +++ b/packages/app/e2e/utils/visual-stability/scenario.ts @@ -0,0 +1,20 @@ +import type { Page, TestInfo } from "@playwright/test" +import type { VisualPlan } from "./invariant" +import { startVisualProbe, stopVisualProbe } from "./probe" +import type { VisualRegionDefinition } from "./regions" +import { reportVisualStability } from "./reporter" + +export async function runVisualStabilityScenario>(input: { + page: Page + testInfo: TestInfo + name: string + regions: Regions + plan: VisualPlan> + run: () => Promise +}) { + await startVisualProbe(input.page, input.regions) + await input.run() + const result = await stopVisualProbe>(input.page) + await reportVisualStability(input.testInfo, input.name, result, input.plan) + return result +} diff --git a/packages/app/public/_headers b/packages/app/public/_headers new file mode 100644 index 0000000000000000000000000000000000000000..f5157b1debcb9bb63b4e323ab8462cba4dafa4c8 --- /dev/null +++ b/packages/app/public/_headers @@ -0,0 +1,17 @@ +/assets/*.js + Content-Type: application/javascript + +/assets/*.mjs + Content-Type: application/javascript + +/assets/*.css + Content-Type: text/css + +/*.js + Content-Type: application/javascript + +/*.mjs + Content-Type: application/javascript + +/*.css + Content-Type: text/css diff --git a/packages/app/public/apple-touch-icon-v3.png b/packages/app/public/apple-touch-icon-v3.png new file mode 100644 index 0000000000000000000000000000000000000000..70fd01b0ea38ea8b3d0429a6438f5b8e85aba94a Binary files /dev/null and b/packages/app/public/apple-touch-icon-v3.png differ diff --git a/packages/app/public/favicon-96x96-v3.png b/packages/app/public/favicon-96x96-v3.png new file mode 100644 index 0000000000000000000000000000000000000000..15266d28f159a74d5c54634d1b3ef548e97f6c8c Binary files /dev/null and b/packages/app/public/favicon-96x96-v3.png differ diff --git a/packages/app/public/favicon-96x96.png b/packages/app/public/favicon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..15266d28f159a74d5c54634d1b3ef548e97f6c8c Binary files /dev/null and b/packages/app/public/favicon-96x96.png differ diff --git a/packages/app/public/favicon-v3.ico b/packages/app/public/favicon-v3.ico new file mode 100644 index 0000000000000000000000000000000000000000..34ca0b9c01b23ca30f64ed17bd0705bb08cca4b1 Binary files /dev/null and b/packages/app/public/favicon-v3.ico differ diff --git a/packages/app/public/favicon-v3.svg b/packages/app/public/favicon-v3.svg new file mode 100644 index 0000000000000000000000000000000000000000..157edc4d7522f38d3d34aff1317d304cb7958752 --- /dev/null +++ b/packages/app/public/favicon-v3.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/app/public/favicon.svg b/packages/app/public/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..157edc4d7522f38d3d34aff1317d304cb7958752 --- /dev/null +++ b/packages/app/public/favicon.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/packages/app/public/oc-theme-preload.js b/packages/app/public/oc-theme-preload.js new file mode 100644 index 0000000000000000000000000000000000000000..11c9b39ec4cdb0ee4469e53914b5a1f554a993c6 --- /dev/null +++ b/packages/app/public/oc-theme-preload.js @@ -0,0 +1,40 @@ +;(function () { + var key = "opencode-theme-id" + var themeId = localStorage.getItem(key) || "oc-2" + + if (themeId === "oc-1") { + themeId = "oc-2" + localStorage.setItem(key, themeId) + localStorage.removeItem("opencode-theme-css-light") + localStorage.removeItem("opencode-theme-css-dark") + } + + var scheme = localStorage.getItem("opencode-color-scheme") || "system" + var isDark = scheme === "dark" || (scheme === "system" && matchMedia("(prefers-color-scheme: dark)").matches) + var mode = isDark ? "dark" : "light" + + document.documentElement.dataset.theme = themeId + document.documentElement.dataset.colorScheme = mode + document.documentElement.style.backgroundColor = isDark ? "#080808" : "#fafafa" + + // Update theme-color meta tag to match app color scheme + var metas = document.querySelectorAll("meta[name='theme-color']") + if (metas.length > 0) metas[0].setAttribute("content", isDark ? "#080808" : "#fafafa") + + if (themeId === "oc-2") return + + var css = localStorage.getItem("opencode-theme-css-" + mode) + if (css) { + var style = document.createElement("style") + style.id = "oc-theme-preload" + style.textContent = + ":root{color-scheme:" + + mode + + ";--text-mix-blend-mode:" + + (isDark ? "plus-lighter" : "multiply") + + ";" + + css + + "}" + document.head.appendChild(style) + } +})() diff --git a/packages/app/public/site.webmanifest b/packages/app/public/site.webmanifest new file mode 100644 index 0000000000000000000000000000000000000000..427c3adf423df124d854c5d95149e40875a39010 --- /dev/null +++ b/packages/app/public/site.webmanifest @@ -0,0 +1,24 @@ +{ + "name": "OpenCode", + "short_name": "OpenCode", + "id": "/", + "start_url": "/", + "scope": "/", + "icons": [ + { + "src": "/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#080808", + "background_color": "#080808", + "display": "standalone" +} diff --git a/packages/app/public/social-share.png b/packages/app/public/social-share.png new file mode 100644 index 0000000000000000000000000000000000000000..e3d5267a92b115b9124cea457645321db23ecfc0 Binary files /dev/null and b/packages/app/public/social-share.png differ diff --git a/packages/app/public/web-app-manifest-512x512.png b/packages/app/public/web-app-manifest-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..f6f0cf514e89b5a9fc7294eb2ab41388bf614b4e Binary files /dev/null and b/packages/app/public/web-app-manifest-512x512.png differ diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0c950871a3354032962d6bbd96015d919afc9fd7 --- /dev/null +++ b/packages/app/src/app.tsx @@ -0,0 +1,667 @@ +import "@/index.css" +import * as Sentry from "@sentry/solid" +import { I18nProvider } from "@opencode-ai/ui/context" +import { DialogProvider } from "@opencode-ai/ui/context/dialog" +import { FileComponentProvider } from "@opencode-ai/ui/context/file" +import { File } from "@opencode-ai/session-ui/file" +import { Font } from "@opencode-ai/ui/font" +import { Splash } from "@opencode-ai/ui/logo" +import { ThemeProvider } from "@opencode-ai/ui/theme/context" +import { MetaProvider } from "@solidjs/meta" +import { + type BaseRouterProps, + Navigate, + Route, + Router, + useLocation, + useNavigate, + useParams, + useSearchParams, +} from "@solidjs/router" +import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" +import { Effect } from "effect" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { + type Component, + createEffect, + createMemo, + createRenderEffect, + createResource, + createSignal, + ErrorBoundary, + For, + type JSX, + lazy, + onCleanup, + type ParentProps, + Show, +} from "solid-js" +import { Dynamic } from "solid-js/web" +import { makeEventListener } from "@solid-primitives/event-listener" +import { CommandProvider, useCommand, type CommandOption } from "@/context/command" +import { CommentsProvider } from "@/context/comments" +import { FileProvider } from "@/context/file" +import { ServerSDKProvider } from "@/context/server-sdk" +import { ServerSyncProvider, useServerSync } from "@/context/server-sync" +import { GlobalProvider, useGlobal } from "@/context/global" +import { HighlightsProvider } from "@/context/highlights" +import { LanguageProvider, type Locale, useLanguage } from "@/context/language" +import { LayoutProvider } from "@/context/layout" +import { ModelsProvider } from "@/context/models" +import { NotificationProvider } from "@/context/notification" +import { PermissionProvider } from "@/context/permission" +import { usePlatform } from "@/context/platform" +import { PromptProvider } from "@/context/prompt" +import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" +import { SettingsProvider, useSettings } from "@/context/settings" +import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs" +import { SDKProvider, useSDK } from "@/context/sdk" +import { WslServersProvider } from "@/wsl/context" +import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout" +import LegacyLayout from "@/pages/layout" +import NewLayout from "@/pages/layout-new" +import { ErrorPage } from "./pages/error" +import { useCheckServerHealth } from "./utils/server-health" +import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route" +import { createSessionLineage } from "@/pages/session/session-lineage" + +import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session" +import { NewHome } from "@/pages/home" +import { LegacyHome } from "@/pages/home/legacy-home" + +const NewSession = lazy(() => import("@/pages/new-session")) + +const SessionRoute = () => { + const settings = useSettings() + const params = useParams() + const [search] = useSearchParams<{ draftId?: string; prompt?: string }>() + const sdk = useSDK() + const server = useServer() + const tabs = useTabs() + + if (params.id && settings.general.newLayoutDesigns()) { + const sessionID = params.id + return ( + + {(_) => { + const persisted = tabs.store.filter((item) => item.type === "session") + return + }} + + ) + } + + // When the new layout is enabled, the legacy new-session route (/:dir/session with no id) + // is replaced by a draft at /new-session?draftId=… + createEffect(() => { + if (!settings.general.newLayoutDesigns()) return + if (params.id || search.draftId) return + if (!tabs.ready() || !sdk().directory) return + tabs.newDraft({ server: server.key, directory: sdk().directory }, search.prompt) + }) + + return ( + + + + ) +} + +function TargetServerRoute(props: ParentProps) { + const params = useParams<{ serverKey: string; id: string }>() + const global = useGlobal() + const conn = createMemo(() => { + const key = requireServerKey(params.serverKey) + return global.servers.list().find((item) => ServerConnection.key(item) === key) + }) + + return ( + // Owns the server-identity remount. Session changes must NOT remount this + // subtree (SessionRouteErrorBoundary resets and createSessionLineage + // re-resolves reactively instead); both rely on this key for server changes. + + + {props.children} + + + ) +} + +const TargetSessionRoute = () => ( + + + +) + +function LegacyTargetSessionRoute() { + const params = useParams<{ serverKey: string; id: string }>() + return ( + + + + + + ) +} + +function LegacyTargetSessionRedirect() { + const params = useParams<{ id: string }>() + const navigate = useNavigate() + const sync = useServerSync() + const current = createSessionLineage( + () => params.id, + () => sync().session.lineage, + ) + + createEffect(() => { + const directory = current()?.session.directory + if (!directory) return + navigate(legacySessionHref(directory, params.id), { replace: true }) + }) + + return null +} + +// Wraps the non-draft routes. They are gated on (and keyed to) the globally selected +// server via ServerKey, then provide the server-scoped shell for that server. +function SelectedServerProviders(props: ParentProps) { + return ( + + + {props.children} + + + ) +} + +function LegacyServerLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) { + return ( + + {props.children} + + ) +} + +function DraftRoute() { + const [search] = useSearchParams<{ draftId?: string }>() + const settings = useSettings() + const tabs = useTabs() + return ( + + tab.type === "draft" && tab.draftID === search.draftId)} + keyed + fallback={} + > + {(draft) => ( + } + > + + + )} + + + ) +} + +function ResolvedDraftRoute(props: { draft: DraftTab }) { + const global = useGlobal() + const conn = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === props.draft.server)) + const directory = () => props.draft.directory + const serverKey = () => props.draft.server + + return ( + + + + + + + + + + + + + + + + ) +} + +function UiI18nBridge(props: ParentProps) { + const language = useLanguage() + return ( + + {props.children} + + ) +} + +function LayoutCompatibility(props: ParentProps) { + const global = useGlobal() + const navigate = useNavigate() + const server = useServer() + const settings = useSettings() + + createEffect(() => { + if (settings.general.newLayoutDesigns()) return + const current = server.current + if (!current) return + const protocol = global.ensureServerCtx(current).sdk.protocolKind() + if (protocol !== "v2") return + const next = global.servers.list().find((s) => { + if (ServerConnection.key(s) === ServerConnection.key(current)) return false + return global.ensureServerCtx(s).sdk.protocolKind() !== "v2" + }) + if (!next) return + navigate("/") + queueMicrotask(() => server.setActive(ServerConnection.key(next))) + }) + + return <>{props.children} +} + +declare global { + interface Window { + __OPENCODE__?: { + deepLinks?: string[] + } + api?: { + setTitlebar?: (theme: { mode: "light" | "dark"; scheme?: "system" | "light" | "dark" }) => Promise + exportDebugLogs?: () => Promise + } + } +} + +function QueryProvider(props: ParentProps) { + const client = new QueryClient({ + defaultOptions: { + queries: { + refetchOnReconnect: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + }, + }, + }) + return {props.children} +} + +function BodyDesignClass() { + const settings = useSettings() + + createRenderEffect(() => { + if (typeof document === "undefined") return + + const enabled = settings.general.newLayoutDesigns() + document.body.toggleAttribute("data-new-layout", enabled) + document.body.classList.toggle("text-12-regular", !enabled) + document.body.classList.toggle("font-(family-name:--font-family-text)", enabled) + document.body.classList.toggle("text-[13px]", enabled) + document.body.classList.toggle("font-[440]", enabled) + }) + + return null +} + +// Server-agnostic providers shared across every route. These live in the shared +// shell (router root) so they stay mounted regardless of the active server/route. +function SharedProviders(props: ParentProps) { + return ( + <> + + + + {props.children} + + + ) +} + +function DesktopCommands() { + const command = useCommand() + const language = useLanguage() + const platform = usePlatform() + + command.register("desktop", () => { + const commands: CommandOption[] = [] + if (platform.platform === "desktop" && platform.exportDebugLogs) { + commands.push({ + id: "logs.export", + title: language.t("command.logs.export"), + category: language.t("command.category.settings"), + onSelect: () => { + void platform.exportDebugLogs?.() + }, + }) + } + return commands + }) + + return null +} + +// Server-scoped providers shared by the legacy shell and the top-level new shell. +type ServerScopedShellProps = ParentProps<{ + directory?: () => string | undefined + serverScoped?: JSX.Element +}> + +function ServerScopedProviders(props: ServerScopedShellProps) { + return ( + + {props.serverScoped} + {props.children} + + ) +} + +function LegacyServerScopedShell(props: ServerScopedShellProps) { + return ( + + {props.children} + + ) +} + +function NewAppLayout(props: ParentProps<{ serverScoped?: JSX.Element }>) { + return ( + + + {props.children} + + + ) +} + +// The draft page only renders the prompt composer, so it drops TerminalProvider. +// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context. +function DraftProviders(props: ParentProps) { + return ( + + + {props.children} + + + ) +} + +export function AppBaseProviders( + props: ParentProps<{ + locale?: Locale + onNativeTranslations?: Parameters[0]["onNativeTranslations"] + }>, +) { + return ( + + + { + void window.api?.setTitlebar?.({ mode, scheme }) + }} + > + + + { + Sentry.captureException(error) + return + }} + > + + + + {props.children} + + + + + + + + + ) +} + +function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean; startup?: Promise }>) { + const server = useServer() + const checkServerHealth = useCheckServerHealth() + + const [checkMode, setCheckMode] = createSignal<"blocking" | "background">("blocking") + + // performs repeated health check with a grace period for + // non-http connections, otherwise fails instantly + const [startupHealthCheck, healthCheckActions] = createResource(() => + props.disableHealthCheck + ? true + : Effect.gen(function* () { + if (!server.current) return true + const { http, type } = server.current + + while (true) { + const res = yield* Effect.promise(() => checkServerHealth(http)) + if (res.healthy) return true + if (checkMode() === "background" || type === "http") return false + } + }).pipe( + Effect.timeoutOrElse({ duration: "10 seconds", orElse: () => Effect.succeed(false) }), + Effect.ensuring(Effect.sync(() => setCheckMode("background"))), + Effect.runPromise, + ), + ) + const checking = createMemo( + () => checkMode() === "blocking" && ["unresolved", "pending"].includes(startupHealthCheck.state), + ) + const [startup] = createResource(async () => { + if (!props.startup) return true + await props.startup.catch((error) => { + console.error("[startup] startup gate failed", error) + }) + return true + }) + const startupChecking = createMemo( + () => startupHealthCheck.latest === true && ["unresolved", "pending"].includes(startup.state), + ) + const loading = createMemo(() => checking() || startupChecking()) + + return ( + <> + + { + if (checkMode() === "background") void healthCheckActions.refetch() + }} + onServerSelected={(key) => { + setCheckMode("blocking") + server.setActive(key) + void healthCheckActions.refetch() + }} + /> + } + > + {props.children} + + + +
+ +
+
+ + ) +} + +function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) { + const language = useLanguage() + const server = useServer() + const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key) + const name = createMemo(() => server.name || server.key) + const serverToken = "\u0000server\u0000" + const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken)) + + const timer = setInterval(() => props.onRetry?.(), 1000) + onCleanup(() => clearInterval(timer)) + + return ( +
+
+ +

+ {unreachable()[0]} + {name()} + {unreachable()[1]} +

+

{language.t("app.server.retrying")}

+
+ 0}> +
+ {language.t("app.server.otherServers")} +
+ + {(conn) => { + const key = ServerConnection.key(conn) + return ( + + ) + }} + +
+
+
+
+ ) +} + +function ServerKey(props: ParentProps) { + const server = useServer() + return ( + + {props.children} + + ) +} + +export function AppInterface(props: { + children?: JSX.Element + defaultServer: ServerConnection.Key + canonicalLocalServer?: ServerConnection.Key + servers?: Array + router?: Component + disableHealthCheck?: boolean + startup?: Promise + serverScoped?: JSX.Element +}) { + // The visual new layout lives in the router root so it remains mounted across + // route changes. Draft and session routes override only their server-bound data + // providers beneath it. + const ServerShell = (shellProps: ParentProps) => ( + + + {props.children} + {shellProps.children} + + + ) + + return ( + + + + + + ( + + + + + + {routerProps.children} + + + + + + )} + > + + + + + + + + ) +} + +function Routes(props: { serverScoped?: JSX.Element }) { + const settings = useSettings() + + return ( + <> + ( + {routeProps.children} + )} + > + + { + <> + + + + } + + + } /> + + + + + + + + + + + ) +} + +function NewLayoutLegacySessionRedirect() { + const server = useServer() + const tabs = useTabs() + const params = useParams<{ id: string }>() + + return ( + + item.type === "session"), + params.id, + server.key, + ), + params.id, + )} + /> + + ) +} diff --git a/packages/app/src/assets/help/placeholder.png b/packages/app/src/assets/help/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..c651d06cc92019b01c2abe1c38f112e4cc759d60 Binary files /dev/null and b/packages/app/src/assets/help/placeholder.png differ diff --git a/packages/app/src/components/prompt-input/attachments.test.ts b/packages/app/src/components/prompt-input/attachments.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..104921697da54c6d6562f53b69d7c2600d9d0cd3 --- /dev/null +++ b/packages/app/src/components/prompt-input/attachments.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from "bun:test" +import { attachmentMime, pickAttachmentFiles } from "./files" +import { pasteMode } from "./paste" + +describe("attachmentMime", () => { + test("keeps PDFs when the browser reports the mime", async () => { + const file = new File(["%PDF-1.7"], "guide.pdf", { type: "application/pdf" }) + expect(await attachmentMime(file)).toBe("application/pdf") + }) + + test("normalizes structured text types to text/plain", async () => { + const file = new File(['{"ok":true}\n'], "data.json", { type: "application/json" }) + expect(await attachmentMime(file)).toBe("text/plain") + }) + + test("accepts text files even with a misleading browser mime", async () => { + const file = new File(["export const x = 1\n"], "main.ts", { type: "video/mp2t" }) + expect(await attachmentMime(file)).toBe("text/plain") + }) + + test("rejects binary files", async () => { + const file = new File([Uint8Array.of(0, 255, 1, 2)], "blob.bin", { type: "application/octet-stream" }) + expect(await attachmentMime(file)).toBeUndefined() + }) +}) + +describe("pickAttachmentFiles", () => { + test("reads the current project directory for every native picker invocation", async () => { + const paths: string[] = [] + const files: File[] = [] + const file = new File(["hello"], "hello.txt", { type: "text/plain" }) + let directory = "C:\\Projects\\LoremIpsum" + const picker = async (options?: { defaultPath?: string }, onFile?: (file: File) => Promise) => { + paths.push(options?.defaultPath ?? "") + await onFile?.(file) + } + + pickAttachmentFiles({ + picker, + directory: () => directory, + fallback: () => undefined, + onFile: async (selected) => files.push(selected), + onError: () => undefined, + }) + await Promise.resolve() + directory = "C:\\Projects\\DolorSit" + pickAttachmentFiles({ + picker, + directory: () => directory, + fallback: () => undefined, + onFile: async (selected) => files.push(selected), + onError: () => undefined, + }) + await Promise.resolve() + expect(files).toEqual([file, file]) + expect(paths).toEqual(["C:\\Projects\\LoremIpsum", "C:\\Projects\\DolorSit"]) + }) + + test("uses the browser file input when no native picker exists", async () => { + let fallback = 0 + pickAttachmentFiles({ + directory: () => "/projects/consectetur-adipiscing", + fallback: () => { + fallback += 1 + }, + onFile: async () => undefined, + onError: () => undefined, + }) + expect(fallback).toBe(1) + }) + + test("reports native picker failures without rejecting", async () => { + const error = new Error("picker unavailable") + const errors: unknown[] = [] + const handled = Promise.withResolvers() + pickAttachmentFiles({ + picker: async () => Promise.reject(error), + directory: () => "C:\\Projects\\LoremIpsum", + fallback: () => undefined, + onFile: async () => undefined, + onError: (cause) => { + errors.push(cause) + handled.resolve() + }, + }) + await handled.promise + expect(errors).toEqual([error]) + }) +}) + +describe("pasteMode", () => { + test("uses native paste for short single-line text", () => { + expect(pasteMode("hello world")).toBe("native") + }) + + test("uses manual paste for multiline text", () => { + expect( + pasteMode(`{ + "ok": true +}`), + ).toBe("manual") + expect(pasteMode("a\r\nb")).toBe("manual") + }) + + test("uses manual paste for large text", () => { + expect(pasteMode("x".repeat(8000))).toBe("manual") + }) +}) diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f3ef57c66e244c40631a2d8ac516ba71dff36da --- /dev/null +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -0,0 +1,213 @@ +import { onMount } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import { showToast } from "@/utils/toast" +import { type ContentPart, type ImageAttachmentPart, type usePrompt } from "@/context/prompt" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { uuid } from "@/utils/uuid" +import { getCursorPosition } from "./editor-dom" +import { createBlobReference, type DraftStore } from "@/utils/draft-store" +import { attachmentMime } from "./files" +import { normalizePaste, pasteMode } from "./paste" + +type PromptTarget = Pick["capture"]>, "current" | "cursor" | "set"> +type AttachmentTarget = { prompt: PromptTarget; cursor: number | undefined } + +type PromptAttachmentsCoreInput = { + capture: () => PromptTarget + editor: () => HTMLDivElement | undefined + focusEditor?: () => void + addPart?: (part: ContentPart) => boolean + warn?: () => void + readClipboardImage?: () => Promise + getPathForFile?: (file: File) => string + draftStore?: DraftStore +} + +export type PromptAttachmentsInput = { + prompt: ReturnType + editor: () => HTMLDivElement | undefined + isDialogActive: () => boolean + setDraggingType: (type: "image" | "@mention" | null) => void + focusEditor: () => void + addPart: (part: ContentPart) => boolean + readClipboardImage?: () => Promise + getPathForFile?: (file: File) => string +} + +export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) { + const capture = (): AttachmentTarget | undefined => { + const prompt = input.capture() + const editor = input.editor() + if (!editor) return + return { prompt, cursor: prompt.cursor() ?? getCursorPosition(editor) } + } + + const add = async (file: File, toast = true, target = capture()) => { + if (!target) return false + const mime = await attachmentMime(file) + if (!mime) { + if (toast) input.warn?.() + return false + } + + const attachment: ImageAttachmentPart = { + type: "image", + id: uuid(), + filename: file.name, + sourcePath: input.getPathForFile?.(file) || undefined, + mime, + blob: input.draftStore ? await input.draftStore.putBlob(file) : await createBlobReference(file), + } + target.prompt.set([...target.prompt.current(), attachment], target.cursor) + return true + } + + const addAttachment = (file: File) => add(file) + + const addAttachments = async (files: File[], toast = true, target = capture()) => { + let found = false + + for (const file of files) { + const ok = await add(file, false, target) + if (ok) found = true + } + + if (!found && files.length > 0 && toast) input.warn?.() + return found + } + + const addClipboardAttachment = async (pending: Promise, target = capture()) => { + const file = await pending + if (!file) return false + return add(file, true, target) + } + + const removeAttachment = (id: string) => { + const target = input.capture() + const current = target.current() + const next = current.filter((part) => part.type !== "image" || part.id !== id) + target.set(next, target.cursor()) + } + + const handlePaste = async (event: ClipboardEvent) => { + const clipboardData = event.clipboardData + if (!clipboardData) return + const target = capture() + if (!target) return + + event.preventDefault() + event.stopPropagation() + + const files = Array.from(clipboardData.items).flatMap((item) => { + if (item.kind !== "file") return [] + const file = item.getAsFile() + return file ? [file] : [] + }) + + if (files.length > 0) { + await addAttachments(files, true, target) + return + } + + const plainText = clipboardData.getData("text/plain") ?? "" + + // Desktop: Browser clipboard has no images and no text, try platform's native clipboard for images + if (input.readClipboardImage && !plainText) { + if (await addClipboardAttachment(input.readClipboardImage(), target)) return + } + + if (!plainText) return + + const text = normalizePaste(plainText) + + const put = () => { + if (input.addPart?.({ type: "text", content: text, start: 0, end: 0 })) return true + input.focusEditor?.() + return input.addPart?.({ type: "text", content: text, start: 0, end: 0 }) ?? false + } + + if (pasteMode(text) === "manual") { + put() + return + } + + const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, text) + if (inserted) return + + put() + } + + return { + addAttachment, + addAttachments, + addClipboardAttachment, + removeAttachment, + handlePaste, + } +} + +export function createPromptAttachments(input: PromptAttachmentsInput) { + const language = useLanguage() + const platform = usePlatform() + const attachments = createPromptAttachmentsCore({ + ...input, + draftStore: platform.draftStore, + capture: input.prompt.capture, + warn: () => { + showToast({ + title: language.t("prompt.toast.pasteUnsupported.title"), + description: language.t("prompt.toast.pasteUnsupported.description"), + }) + }, + }) + + const handleGlobalDragOver = (event: DragEvent) => { + if (input.isDialogActive()) return + + event.preventDefault() + const hasFiles = event.dataTransfer?.types.includes("Files") + const hasText = event.dataTransfer?.types.includes("text/plain") + if (hasFiles) { + input.setDraggingType("image") + } else if (hasText) { + input.setDraggingType("@mention") + } + } + + const handleGlobalDragLeave = (event: DragEvent) => { + if (input.isDialogActive()) return + if (!event.relatedTarget) { + input.setDraggingType(null) + } + } + + const handleGlobalDrop = async (event: DragEvent) => { + if (input.isDialogActive()) return + + event.preventDefault() + input.setDraggingType(null) + + const plainText = event.dataTransfer?.getData("text/plain") + const filePrefix = "file:" + if (plainText?.startsWith(filePrefix)) { + const filePath = plainText.slice(filePrefix.length) + input.focusEditor() + input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 }) + return + } + + const dropped = event.dataTransfer?.files + if (!dropped) return + + await attachments.addAttachments(Array.from(dropped)) + } + + onMount(() => { + makeEventListener(document, "dragover", handleGlobalDragOver) + makeEventListener(document, "dragleave", handleGlobalDragLeave) + makeEventListener(document, "drop", handleGlobalDrop) + }) + + return attachments +} diff --git a/packages/app/src/components/prompt-input/build-request-parts.test.ts b/packages/app/src/components/prompt-input/build-request-parts.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab84cb6eae81ed613b7ebafb9810472f33c2070a --- /dev/null +++ b/packages/app/src/components/prompt-input/build-request-parts.test.ts @@ -0,0 +1,396 @@ +import { describe, expect, test } from "bun:test" +import type { Prompt } from "@/context/prompt" +import { buildRequestParts } from "./build-request-parts" + +describe("buildRequestParts", () => { + test("builds typed request and optimistic parts without cast path", () => { + const prompt: Prompt = [ + { type: "text", content: "hello", start: 0, end: 5 }, + { + type: "file", + path: "src/foo.ts", + content: "@src/foo.ts", + start: 5, + end: 16, + selection: { startLine: 4, startChar: 1, endLine: 6, endChar: 1 }, + }, + { type: "agent", name: "planner", content: "@planner", start: 16, end: 24 }, + ] + + const result = buildRequestParts({ + prompt, + context: [{ key: "ctx:1", type: "file", path: "src/bar.ts", comment: "check this" }], + images: [ + { type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, + ], + text: "hello @src/foo.ts @planner", + messageID: "msg_1", + sessionID: "ses_1", + sessionDirectory: "/repo", + }) + + expect(result.requestParts[0]?.type).toBe("text") + expect(result.requestParts.some((part) => part.type === "agent")).toBe(true) + expect( + result.requestParts.some((part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts")), + ).toBe(true) + expect(result.requestParts.some((part) => part.type === "text" && part.synthetic)).toBe(true) + expect( + result.requestParts.some( + (part) => + part.type === "text" && + part.synthetic && + part.metadata?.opencodeComment && + (part.metadata.opencodeComment as { comment?: string }).comment === "check this", + ), + ).toBe(true) + + expect(result.optimisticParts).toHaveLength(result.requestParts.length) + expect(result.optimisticParts.every((part) => part.sessionID === "ses_1" && part.messageID === "msg_1")).toBe(true) + }) + + test("keeps multiple uploaded attachments in order", () => { + const result = buildRequestParts({ + prompt: [{ type: "text", content: "check these", start: 0, end: 11 }], + context: [], + images: [ + { type: "image", id: "img_1", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, + { + type: "image", + id: "img_2", + filename: "b.pdf", + mime: "application/pdf", + dataUrl: "data:application/pdf;base64,BBB", + }, + ], + text: "check these", + messageID: "msg_multi", + sessionID: "ses_multi", + sessionDirectory: "/repo", + }) + + const files = result.requestParts.filter((part) => part.type === "file" && part.url.startsWith("data:")) + + expect(files).toHaveLength(2) + expect(files.map((part) => (part.type === "file" ? part.filename : ""))).toEqual(["a.png", "b.pdf"]) + }) + + test("preserves an external attachment source path for the model", () => { + const result = buildRequestParts({ + prompt: [], + context: [], + images: [ + { + type: "image", + id: "img_external", + filename: "opencode.global.dat", + sourcePath: "C:\\Users\\Luke\\AppData\\Roaming\\ai.opencode.desktop.beta\\opencode.global.dat", + mime: "text/plain", + dataUrl: "data:text/plain;base64,AAA", + }, + ], + text: "inspect this", + messageID: "msg_external", + sessionID: "ses_external", + sessionDirectory: "C:\\Repos\\sst\\opencode", + }) + + expect(result.requestParts.find((part) => part.type === "file")?.filename).toBe( + "C:\\Users\\Luke\\AppData\\Roaming\\ai.opencode.desktop.beta\\opencode.global.dat", + ) + }) + + test("preserves reference aliases as directory file parts", () => { + const result = buildRequestParts({ + prompt: [ + { + type: "file", + path: "/repo/../docs", + content: "@docs", + start: 0, + end: 5, + mime: "application/x-directory", + filename: "docs", + }, + ], + context: [], + images: [], + text: "@docs", + messageID: "msg_reference", + sessionID: "ses_reference", + sessionDirectory: "/repo/app", + }) + + const filePart = result.requestParts.find((part) => part.type === "file") + expect(filePart).toBeDefined() + if (filePart?.type === "file") { + expect(filePart.mime).toBe("application/x-directory") + expect(filePart.filename).toBe("docs") + expect(filePart.url).toBe("file:///repo/../docs") + expect(filePart.source?.type).toBe("file") + if (filePart.source?.type === "file") { + expect(filePart.source.path).toBe("/repo/../docs") + expect(filePart.source.text.value).toBe("@docs") + } + } + }) + + test("deduplicates context files when prompt already includes same path", () => { + const prompt: Prompt = [{ type: "file", path: "src/foo.ts", content: "@src/foo.ts", start: 0, end: 11 }] + + const result = buildRequestParts({ + prompt, + context: [ + { key: "ctx:dup", type: "file", path: "src/foo.ts" }, + { key: "ctx:comment", type: "file", path: "src/foo.ts", comment: "focus here" }, + ], + images: [], + text: "@src/foo.ts", + messageID: "msg_2", + sessionID: "ses_2", + sessionDirectory: "/repo", + }) + + const fooFiles = result.requestParts.filter( + (part) => part.type === "file" && part.url.startsWith("file:///repo/src/foo.ts"), + ) + const synthetic = result.requestParts.filter((part) => part.type === "text" && part.synthetic) + + expect(fooFiles).toHaveLength(2) + expect(synthetic).toHaveLength(1) + }) + + test("adds file parts for @mentions inside comment text", () => { + const result = buildRequestParts({ + prompt: [{ type: "text", content: "look", start: 0, end: 4 }], + context: [ + { + key: "ctx:comment-mention", + type: "file", + path: "src/review.ts", + comment: "Compare with @src/shared.ts and @src/review.ts.", + }, + ], + images: [], + text: "look", + messageID: "msg_comment_mentions", + sessionID: "ses_comment_mentions", + sessionDirectory: "/repo", + }) + + const files = result.requestParts.filter((part) => part.type === "file") + expect(files).toHaveLength(2) + expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/review.ts")).toBe(true) + expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/shared.ts")).toBe(true) + }) + + test("handles Windows paths correctly (simulated on macOS)", () => { + const prompt: Prompt = [{ type: "file", path: "src\\foo.ts", content: "@src\\foo.ts", start: 0, end: 11 }] + + const result = buildRequestParts({ + prompt, + context: [], + images: [], + text: "@src\\foo.ts", + messageID: "msg_win_1", + sessionID: "ses_win_1", + sessionDirectory: "D:\\projects\\myapp", // Windows path + }) + + // Should create valid file URLs + const filePart = result.requestParts.find((part) => part.type === "file") + expect(filePart).toBeDefined() + if (filePart?.type === "file") { + // URL should be parseable + expect(() => new URL(filePart.url)).not.toThrow() + // Should not have encoded backslashes in wrong place + expect(filePart.url).not.toContain("%5C") + // Should have normalized to forward slashes + expect(filePart.url).toContain("/src/foo.ts") + } + }) + + test("handles Windows absolute path with special characters", () => { + const prompt: Prompt = [{ type: "file", path: "file#name.txt", content: "@file#name.txt", start: 0, end: 14 }] + + const result = buildRequestParts({ + prompt, + context: [], + images: [], + text: "@file#name.txt", + messageID: "msg_win_2", + sessionID: "ses_win_2", + sessionDirectory: "C:\\Users\\test\\Documents", // Windows path + }) + + const filePart = result.requestParts.find((part) => part.type === "file") + expect(filePart).toBeDefined() + if (filePart?.type === "file") { + // URL should be parseable + expect(() => new URL(filePart.url)).not.toThrow() + // Special chars should be encoded + expect(filePart.url).toContain("file%23name.txt") + // Should have Windows drive letter properly encoded + expect(filePart.url).toMatch(/file:\/\/\/[A-Z]:/) + } + }) + + test("handles Linux absolute paths correctly", () => { + const prompt: Prompt = [{ type: "file", path: "src/app.ts", content: "@src/app.ts", start: 0, end: 10 }] + + const result = buildRequestParts({ + prompt, + context: [], + images: [], + text: "@src/app.ts", + messageID: "msg_linux_1", + sessionID: "ses_linux_1", + sessionDirectory: "/home/user/project", + }) + + const filePart = result.requestParts.find((part) => part.type === "file") + expect(filePart).toBeDefined() + if (filePart?.type === "file") { + // URL should be parseable + expect(() => new URL(filePart.url)).not.toThrow() + // Should be a normal Unix path + expect(filePart.url).toBe("file:///home/user/project/src/app.ts") + } + }) + + test("handles macOS paths correctly", () => { + const prompt: Prompt = [{ type: "file", path: "README.md", content: "@README.md", start: 0, end: 9 }] + + const result = buildRequestParts({ + prompt, + context: [], + images: [], + text: "@README.md", + messageID: "msg_mac_1", + sessionID: "ses_mac_1", + sessionDirectory: "/Users/kelvin/Projects/opencode", + }) + + const filePart = result.requestParts.find((part) => part.type === "file") + expect(filePart).toBeDefined() + if (filePart?.type === "file") { + // URL should be parseable + expect(() => new URL(filePart.url)).not.toThrow() + // Should be a normal Unix path + expect(filePart.url).toBe("file:///Users/kelvin/Projects/opencode/README.md") + } + }) + + test("handles context files with Windows paths", () => { + const prompt: Prompt = [] + + const result = buildRequestParts({ + prompt, + context: [ + { key: "ctx:1", type: "file", path: "src\\utils\\helper.ts" }, + { key: "ctx:2", type: "file", path: "test\\unit.test.ts", comment: "check tests" }, + ], + images: [], + text: "test", + messageID: "msg_win_ctx", + sessionID: "ses_win_ctx", + sessionDirectory: "D:\\workspace\\app", + }) + + const fileParts = result.requestParts.filter((part) => part.type === "file") + expect(fileParts).toHaveLength(2) + + // All file URLs should be valid + fileParts.forEach((part) => { + if (part.type === "file") { + expect(() => new URL(part.url)).not.toThrow() + expect(part.url).not.toContain("%5C") // No encoded backslashes + } + }) + }) + + test("handles absolute Windows paths (user manually specifies full path)", () => { + const prompt: Prompt = [ + { type: "file", path: "D:\\other\\project\\file.ts", content: "@D:\\other\\project\\file.ts", start: 0, end: 25 }, + ] + + const result = buildRequestParts({ + prompt, + context: [], + images: [], + text: "@D:\\other\\project\\file.ts", + messageID: "msg_abs", + sessionID: "ses_abs", + sessionDirectory: "C:\\current\\project", + }) + + const filePart = result.requestParts.find((part) => part.type === "file") + expect(filePart).toBeDefined() + if (filePart?.type === "file") { + // Should handle absolute path that differs from sessionDirectory + expect(() => new URL(filePart.url)).not.toThrow() + expect(filePart.url).toContain("/D:/other/project/file.ts") + } + }) + + test("handles selection with query parameters on Windows", () => { + const prompt: Prompt = [ + { + type: "file", + path: "src\\App.tsx", + content: "@src\\App.tsx", + start: 0, + end: 11, + selection: { startLine: 10, startChar: 0, endLine: 20, endChar: 5 }, + }, + ] + + const result = buildRequestParts({ + prompt, + context: [], + images: [], + text: "@src\\App.tsx", + messageID: "msg_sel", + sessionID: "ses_sel", + sessionDirectory: "C:\\project", + }) + + const filePart = result.requestParts.find((part) => part.type === "file") + expect(filePart).toBeDefined() + if (filePart?.type === "file") { + // Should have query parameters + expect(filePart.url).toContain("?start=10&end=20") + // Should be valid URL + expect(() => new URL(filePart.url)).not.toThrow() + // Query params should parse correctly + const url = new URL(filePart.url) + expect(url.searchParams.get("start")).toBe("10") + expect(url.searchParams.get("end")).toBe("20") + } + }) + + test("handles file paths with dots and special segments on Windows", () => { + const prompt: Prompt = [ + { type: "file", path: "..\\..\\shared\\util.ts", content: "@..\\..\\shared\\util.ts", start: 0, end: 21 }, + ] + + const result = buildRequestParts({ + prompt, + context: [], + images: [], + text: "@..\\..\\shared\\util.ts", + messageID: "msg_dots", + sessionID: "ses_dots", + sessionDirectory: "C:\\projects\\myapp\\src", + }) + + const filePart = result.requestParts.find((part) => part.type === "file") + expect(filePart).toBeDefined() + if (filePart?.type === "file") { + // Should be valid URL + expect(() => new URL(filePart.url)).not.toThrow() + // Should preserve .. segments (backend normalizes) + expect(filePart.url).toContain("/..") + } + }) +}) diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1e3448b31176e81971f48607b392e408f7aa1b9 --- /dev/null +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -0,0 +1,213 @@ +import { getFilename } from "@opencode-ai/core/util/path" +import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client" +import type { FileSelection } from "@/context/file" +import { encodeFilePath } from "@/context/file/path" +import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt" +import { Identifier } from "@/utils/id" +import { createCommentMetadata, formatCommentNote } from "@/utils/comment-note" + +type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string } + +type ContextFile = { + key: string + type: "file" + path: string + selection?: FileSelection + comment?: string + commentID?: string + commentOrigin?: "review" | "file" + preview?: string +} + +type BuildRequestPartsInput = { + prompt: Prompt + context: ContextFile[] + images: (Omit & { dataUrl: string })[] + text: string + messageID: string + sessionID: string + sessionDirectory: string +} + +const absolute = (directory: string, path: string) => { + if (path.startsWith("/")) return path + if (/^[A-Za-z]:[\\/]/.test(path) || /^[A-Za-z]:$/.test(path)) return path + if (path.startsWith("\\\\") || path.startsWith("//")) return path + return `${directory.replace(/[\\/]+$/, "")}/${path}` +} + +const fileQuery = (selection: FileSelection | undefined) => + selection ? `?start=${selection.startLine}&end=${selection.endLine}` : "" + +const mention = /(^|[\s([{"'])@(\S+)/g + +const parseCommentMentions = (comment: string) => { + return Array.from(comment.matchAll(mention)).flatMap((match) => { + const path = (match[2] ?? "").replace(/[.,!?;:)}\]"']+$/, "") + if (!path) return [] + return [path] + }) +} + +const isFileAttachment = (part: Prompt[number]): part is FileAttachmentPart => part.type === "file" +const isAgentAttachment = (part: Prompt[number]): part is AgentPart => part.type === "agent" + +const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID: string): Part => { + if (part.type === "text") { + return { + id: part.id, + type: "text", + text: part.text, + synthetic: part.synthetic, + ignored: part.ignored, + time: part.time, + metadata: part.metadata, + sessionID, + messageID, + } + } + if (part.type === "file") { + return { + id: part.id, + type: "file", + mime: part.mime, + filename: part.filename, + url: part.url, + source: part.source, + sessionID, + messageID, + } + } + return { + id: part.id, + type: "agent", + name: part.name, + source: part.source, + sessionID, + messageID, + } +} + +export function buildRequestParts(input: BuildRequestPartsInput) { + const requestParts: PromptRequestPart[] = input.text.trim() + ? [ + { + id: Identifier.ascending("part"), + type: "text", + text: input.text, + }, + ] + : [] + + const files = input.prompt.filter(isFileAttachment).map((attachment) => { + const path = absolute(input.sessionDirectory, attachment.path) + const source = attachment.source + ? { + ...attachment.source, + text: { + value: attachment.content, + start: attachment.start, + end: attachment.end, + }, + } + : { + type: "file" as const, + text: { + value: attachment.content, + start: attachment.start, + end: attachment.end, + }, + path, + } + return { + id: Identifier.ascending("part"), + type: "file", + mime: attachment.mime ?? "text/plain", + url: attachment.url ?? `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`, + filename: attachment.filename ?? getFilename(attachment.path), + source, + } satisfies PromptRequestPart + }) + + const agents = input.prompt.filter(isAgentAttachment).map((attachment) => { + return { + id: Identifier.ascending("part"), + type: "agent", + name: attachment.name, + source: { + value: attachment.content, + start: attachment.start, + end: attachment.end, + }, + } satisfies PromptRequestPart + }) + + const used = new Set(files.map((part) => part.url)) + const context = input.context.flatMap((item) => { + const path = absolute(input.sessionDirectory, item.path) + const url = `file://${encodeFilePath(path)}${fileQuery(item.selection)}` + const comment = item.comment?.trim() + if (!comment && used.has(url)) return [] + used.add(url) + + const filePart = { + id: Identifier.ascending("part"), + type: "file", + mime: "text/plain", + url, + filename: getFilename(item.path), + } satisfies PromptRequestPart + + if (!comment) return [filePart] + + const mentions = parseCommentMentions(comment).flatMap((path) => { + const url = `file://${encodeFilePath(absolute(input.sessionDirectory, path))}` + if (used.has(url)) return [] + used.add(url) + return [ + { + id: Identifier.ascending("part"), + type: "file", + mime: "text/plain", + url, + filename: getFilename(path), + } satisfies PromptRequestPart, + ] + }) + + return [ + { + id: Identifier.ascending("part"), + type: "text", + text: formatCommentNote({ path: item.path, selection: item.selection, comment }), + synthetic: true, + metadata: createCommentMetadata({ + path: item.path, + selection: item.selection, + comment, + preview: item.preview, + origin: item.commentOrigin, + }), + } satisfies PromptRequestPart, + filePart, + ...mentions, + ] + }) + + const images = input.images.map((attachment) => { + return { + id: Identifier.ascending("part"), + type: "file", + mime: attachment.mime, + url: attachment.dataUrl, + filename: attachment.sourcePath ?? attachment.filename, + } satisfies PromptRequestPart + }) + + requestParts.push(...files, ...context, ...agents, ...images) + + return { + requestParts, + optimisticParts: requestParts.map((part) => toOptimisticPart(part, input.sessionID, input.messageID)), + } +} diff --git a/packages/app/src/components/prompt-input/context-items.tsx b/packages/app/src/components/prompt-input/context-items.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1f03f199ab8ff565619a8553c6190626e92dada7 --- /dev/null +++ b/packages/app/src/components/prompt-input/context-items.tsx @@ -0,0 +1,98 @@ +import { Component, For, Show } from "solid-js" +import { Dynamic } from "solid-js/web" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path" +import type { ContextItem } from "@/context/prompt" + +type PromptContextItem = ContextItem & { key: string } + +type ContextItemsProps = { + items: PromptContextItem[] + active: (item: PromptContextItem) => boolean + openComment: (item: PromptContextItem) => void + remove: (item: PromptContextItem) => void + newLayoutDesigns: boolean + t: (key: string) => string +} + +export const PromptContextItems: Component = (props) => { + return ( + 0}> +
+ + {(item) => { + const directory = getDirectory(item.path) + const filename = getFilename(item.path) + const label = getFilenameTruncated(item.path, 14) + const selected = props.active(item) + + return ( + + + {directory} + + {filename} + + } + placement="top" + openDelay={800} + > +
props.openComment(item)} + > +
+ +
+ {label} + + {(sel) => ( + + {sel().startLine === sel().endLine + ? `:${sel().startLine}` + : `:${sel().startLine}-${sel().endLine}`} + + )} + +
+ { + e.stopPropagation() + props.remove(item) + }} + aria-label={props.t("prompt.context.removeFile")} + /> +
+ + {(comment) =>
{comment()}
} +
+
+
+ ) + }} +
+
+
+ ) +} diff --git a/packages/app/src/components/prompt-input/contracts.ts b/packages/app/src/components/prompt-input/contracts.ts new file mode 100644 index 0000000000000000000000000000000000000000..a38f45d9256e58dc292bb0b31868c944fe8172b9 --- /dev/null +++ b/packages/app/src/components/prompt-input/contracts.ts @@ -0,0 +1,57 @@ +import type { useLocal } from "@/context/local" +import type { Prompt, usePrompt } from "@/context/prompt" +import type { PromptInputHistory } from "./history-store" +import type { FollowupDraft } from "./submit" + +export type PromptInputState = ReturnType + +export type PromptInputSubmission = { + abort: () => Promise | void + handleSubmit: (event: Event) => Promise | void +} + +export type PromptInputControls = { + agents: { + available: { name: string; hidden?: boolean; mode: string }[] + options: string[] + current: string + loading: boolean + visible: boolean + select: (name: string | undefined) => void + } + model: { + selection: ReturnType["model"] + paid: boolean + loading: boolean + } + session: { + id?: string + tabs: { + active: () => string | undefined + all: () => string[] + open: (tab: string) => void | Promise + setActive: (tab: string) => void + } + reviewPanel: { + opened: () => boolean + open: () => void + } + } +} + +export interface PromptInputProps { + class?: string + state?: PromptInputState + history?: PromptInputHistory + submission?: PromptInputSubmission + controls: PromptInputControls + ref?: (el: HTMLDivElement) => void + newSessionWorktree?: string + onNewSessionWorktreeReset?: () => void + edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] } + onEditLoaded?: () => void + shouldQueue?: () => boolean + onQueue?: (draft: FollowupDraft) => void + onAbort?: () => void + onSubmit?: () => void +} diff --git a/packages/app/src/components/prompt-input/drag-overlay.tsx b/packages/app/src/components/prompt-input/drag-overlay.tsx new file mode 100644 index 0000000000000000000000000000000000000000..41962ce536e3d034f2a43379e3d7887dfb0c6abb --- /dev/null +++ b/packages/app/src/components/prompt-input/drag-overlay.tsx @@ -0,0 +1,25 @@ +import { Component, Show } from "solid-js" +import { Icon } from "@opencode-ai/ui/icon" + +type PromptDragOverlayProps = { + type: "image" | "@mention" | null + label: string +} + +const kindToIcon = { + image: "photo", + "@mention": "link", +} as const + +export const PromptDragOverlay: Component = (props) => { + return ( + +
+
+ + {props.label} +
+
+
+ ) +} diff --git a/packages/app/src/components/prompt-input/editor-dom.test.ts b/packages/app/src/components/prompt-input/editor-dom.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..3088522a59f62a17188b5a20a10e51472db27b20 --- /dev/null +++ b/packages/app/src/components/prompt-input/editor-dom.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, test } from "bun:test" +import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom" + +describe("prompt-input editor dom", () => { + test("createTextFragment preserves newlines with consecutive br nodes", () => { + const fragment = createTextFragment("foo\n\nbar") + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(4) + expect(container.childNodes[0]?.textContent).toBe("foo") + expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") + expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR") + expect(container.childNodes[3]?.textContent).toBe("bar") + }) + + test("createTextFragment keeps trailing newline as terminal break", () => { + const fragment = createTextFragment("foo\n") + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(2) + expect(container.childNodes[0]?.textContent).toBe("foo") + expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") + }) + + test("createTextFragment avoids break-node explosion for large multiline content", () => { + const content = Array.from({ length: 220 }, () => "line").join("\n") + const fragment = createTextFragment(content) + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(1) + expect(container.childNodes[0]?.nodeType).toBe(Node.TEXT_NODE) + expect(container.textContent).toBe(content) + }) + + test("createTextFragment keeps terminal break in large multiline fallback", () => { + const content = `${Array.from({ length: 220 }, () => "line").join("\n")}\n` + const fragment = createTextFragment(content) + const container = document.createElement("div") + container.appendChild(fragment) + + expect(container.childNodes.length).toBe(2) + expect(container.childNodes[0]?.textContent).toBe(content.slice(0, -1)) + expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR") + }) + + test("length helpers treat breaks as one char and ignore zero-width chars", () => { + const container = document.createElement("div") + container.appendChild(document.createTextNode("ab\u200B")) + container.appendChild(document.createElement("br")) + container.appendChild(document.createTextNode("cd")) + + expect(getNodeLength(container.childNodes[0]!)).toBe(2) + expect(getNodeLength(container.childNodes[1]!)).toBe(1) + expect(getTextLength(container)).toBe(5) + }) + + test("setCursorPosition and getCursorPosition round-trip with pills and breaks", () => { + const container = document.createElement("div") + const pill = document.createElement("span") + pill.dataset.type = "file" + pill.textContent = "@file" + container.appendChild(document.createTextNode("ab")) + container.appendChild(pill) + container.appendChild(document.createElement("br")) + container.appendChild(document.createTextNode("cd")) + document.body.appendChild(container) + + setCursorPosition(container, 2) + expect(getCursorPosition(container)).toBe(2) + + setCursorPosition(container, 7) + expect(getCursorPosition(container)).toBe(7) + + setCursorPosition(container, 8) + expect(getCursorPosition(container)).toBe(8) + + container.remove() + }) + + test("setCursorPosition and getCursorPosition round-trip across blank lines", () => { + const container = document.createElement("div") + container.appendChild(document.createTextNode("a")) + container.appendChild(document.createElement("br")) + container.appendChild(document.createElement("br")) + container.appendChild(document.createTextNode("b")) + document.body.appendChild(container) + + setCursorPosition(container, 2) + expect(getCursorPosition(container)).toBe(2) + + setCursorPosition(container, 3) + expect(getCursorPosition(container)).toBe(3) + + container.remove() + }) +}) diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts new file mode 100644 index 0000000000000000000000000000000000000000..8575140d7d54f48ce3bab79fd9b52afc7149cfd3 --- /dev/null +++ b/packages/app/src/components/prompt-input/editor-dom.ts @@ -0,0 +1,148 @@ +const MAX_BREAKS = 200 + +export function createTextFragment(content: string): DocumentFragment { + const fragment = document.createDocumentFragment() + let breaks = 0 + for (const char of content) { + if (char !== "\n") continue + breaks += 1 + if (breaks > MAX_BREAKS) { + const tail = content.endsWith("\n") + const text = tail ? content.slice(0, -1) : content + if (text) fragment.appendChild(document.createTextNode(text)) + if (tail) fragment.appendChild(document.createElement("br")) + return fragment + } + } + + const segments = content.split("\n") + segments.forEach((segment, index) => { + if (segment) { + fragment.appendChild(document.createTextNode(segment)) + } + if (index < segments.length - 1) { + fragment.appendChild(document.createElement("br")) + } + }) + return fragment +} + +export function getNodeLength(node: Node): number { + if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1 + return (node.textContent ?? "").replace(/\u200B/g, "").length +} + +export function getTextLength(node: Node): number { + if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? "").replace(/\u200B/g, "").length + if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR") return 1 + let length = 0 + for (const child of Array.from(node.childNodes)) { + length += getTextLength(child) + } + return length +} + +export function getCursorPosition(parent: HTMLElement): number { + const selection = window.getSelection() + if (!selection || selection.rangeCount === 0) return 0 + const range = selection.getRangeAt(0) + if (!parent.contains(range.startContainer)) return 0 + const preCaretRange = range.cloneRange() + preCaretRange.selectNodeContents(parent) + preCaretRange.setEnd(range.startContainer, range.startOffset) + return getTextLength(preCaretRange.cloneContents()) +} + +export function setCursorPosition(parent: HTMLElement, position: number) { + let remaining = position + let node = parent.firstChild + while (node) { + const length = getNodeLength(node) + const isText = node.nodeType === Node.TEXT_NODE + const isPill = + node.nodeType === Node.ELEMENT_NODE && + ((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent") + const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR" + + if (isText && remaining <= length) { + const range = document.createRange() + const selection = window.getSelection() + range.setStart(node, remaining) + range.collapse(true) + selection?.removeAllRanges() + selection?.addRange(range) + return + } + + if ((isPill || isBreak) && remaining <= length) { + const range = document.createRange() + const selection = window.getSelection() + if (remaining === 0) { + range.setStartBefore(node) + } + if (remaining > 0 && isPill) { + range.setStartAfter(node) + } + if (remaining > 0 && isBreak) { + const next = node.nextSibling + if (next && next.nodeType === Node.TEXT_NODE) { + range.setStart(next, 0) + } + if (!next || next.nodeType !== Node.TEXT_NODE) { + range.setStartAfter(node) + } + } + range.collapse(true) + selection?.removeAllRanges() + selection?.addRange(range) + return + } + + remaining -= length + node = node.nextSibling + } + + const fallbackRange = document.createRange() + const fallbackSelection = window.getSelection() + const last = parent.lastChild + if (last && last.nodeType === Node.TEXT_NODE) { + const len = last.textContent ? last.textContent.length : 0 + fallbackRange.setStart(last, len) + } + if (!last || last.nodeType !== Node.TEXT_NODE) { + fallbackRange.selectNodeContents(parent) + } + fallbackRange.collapse(false) + fallbackSelection?.removeAllRanges() + fallbackSelection?.addRange(fallbackRange) +} + +export function setRangeEdge(parent: HTMLElement, range: Range, edge: "start" | "end", offset: number) { + let remaining = offset + const nodes = Array.from(parent.childNodes) + + for (const node of nodes) { + const length = getNodeLength(node) + const isText = node.nodeType === Node.TEXT_NODE + const isPill = + node.nodeType === Node.ELEMENT_NODE && + ((node as HTMLElement).dataset.type === "file" || (node as HTMLElement).dataset.type === "agent") + const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR" + + if (isText && remaining <= length) { + if (edge === "start") range.setStart(node, remaining) + if (edge === "end") range.setEnd(node, remaining) + return + } + + if ((isPill || isBreak) && remaining <= length) { + if (edge === "start" && remaining === 0) range.setStartBefore(node) + if (edge === "start" && remaining > 0) range.setStartAfter(node) + if (edge === "end" && remaining === 0) range.setEndBefore(node) + if (edge === "end" && remaining > 0) range.setEndAfter(node) + return + } + + remaining -= length + } +} diff --git a/packages/app/src/components/prompt-input/files.ts b/packages/app/src/components/prompt-input/files.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c6ad1ff0824e86b2fbef50efbe5045d97873ba3 --- /dev/null +++ b/packages/app/src/components/prompt-input/files.ts @@ -0,0 +1,98 @@ +import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker" + +export { ACCEPTED_FILE_TYPES } + +type AttachmentPicker = ( + options: { + defaultPath?: string + multiple?: boolean + accept?: string[] + }, + onFile: (file: File) => Promise, +) => Promise + +export function pickAttachmentFiles(input: { + picker?: AttachmentPicker + directory: () => string + fallback: () => void + onFile: (file: File) => Promise + onError: (error: unknown) => void +}) { + if (!input.picker) { + input.fallback() + return + } + void input + .picker( + { + defaultPath: input.directory(), + multiple: true, + accept: ACCEPTED_FILE_TYPES, + }, + input.onFile, + ) + .catch(input.onError) +} + +const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES) +const IMAGE_EXTS = new Map([ + ["gif", "image/gif"], + ["jpeg", "image/jpeg"], + ["jpg", "image/jpeg"], + ["png", "image/png"], + ["webp", "image/webp"], +]) +const TEXT_MIMES = new Set([ + "application/json", + "application/ld+json", + "application/toml", + "application/x-toml", + "application/x-yaml", + "application/xml", + "application/yaml", +]) + +const SAMPLE = 4096 + +function kind(type: string) { + return type.split(";", 1)[0]?.trim().toLowerCase() ?? "" +} + +function ext(name: string) { + const idx = name.lastIndexOf(".") + if (idx === -1) return "" + return name.slice(idx + 1).toLowerCase() +} + +function textMime(type: string) { + if (!type) return false + if (type.startsWith("text/")) return true + if (TEXT_MIMES.has(type)) return true + if (type.endsWith("+json")) return true + return type.endsWith("+xml") +} + +function textBytes(bytes: Uint8Array) { + if (bytes.length === 0) return true + let count = 0 + for (const byte of bytes) { + if (byte === 0) return false + if (byte < 9 || (byte > 13 && byte < 32)) count += 1 + } + return count / bytes.length <= 0.3 +} + +export async function attachmentMime(file: File) { + const type = kind(file.type) + if (IMAGE_MIMES.has(type)) return type + if (type === "application/pdf") return type + + const suffix = ext(file.name) + const fallback = IMAGE_EXTS.get(suffix) ?? (suffix === "pdf" ? "application/pdf" : undefined) + if ((!type || type === "application/octet-stream") && fallback) return fallback + + if (textMime(type)) return "text/plain" + const bytes = new Uint8Array(await file.slice(0, SAMPLE).arrayBuffer()) + if (!textBytes(bytes)) return + return "text/plain" +} diff --git a/packages/app/src/components/prompt-input/history-store.ts b/packages/app/src/components/prompt-input/history-store.ts new file mode 100644 index 0000000000000000000000000000000000000000..3858b8b9bc6140d62177a719081b322c794a3f6a --- /dev/null +++ b/packages/app/src/components/prompt-input/history-store.ts @@ -0,0 +1,63 @@ +import { createStore, type SetStoreFunction, type Store } from "solid-js/store" +import type { Prompt } from "@/context/prompt" +import { Persist, persisted } from "@/utils/persist" +import { + clonePromptHistoryComments, + clonePromptParts, + prependHistoryEntry, + type PromptHistoryComment, + type PromptHistoryStoredEntry, +} from "./history" + +export type PromptInputHistory = { + entries: (mode: "normal" | "shell") => PromptHistoryStoredEntry[] + add: (prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) => void +} + +type PromptHistoryState = { entries: PromptHistoryStoredEntry[] } + +function createPromptInputHistoryStore( + normal: Store, + setNormal: SetStoreFunction, + shell: Store, + setShell: SetStoreFunction, +): PromptInputHistory { + return { + entries: (mode) => (mode === "shell" ? shell.entries : normal.entries), + add(prompt, mode, comments) { + const current = mode === "shell" ? shell : normal + const setCurrent = mode === "shell" ? setShell : setNormal + const next = prependHistoryEntry(current.entries, prompt, comments) + if (next === current.entries) return + setCurrent("entries", next) + }, + } +} + +export function createPromptInputHistory(): PromptInputHistory { + const [normal, setNormal] = createStore({ entries: [] }) + const [shell, setShell] = createStore({ entries: [] }) + return createPromptInputHistoryStore(normal, setNormal, shell, setShell) +} + +export function createPersistedPromptInputHistory() { + const [normal, setNormal, normalInit] = persisted( + Persist.prompt(Persist.global("prompt-history", ["prompt-history.v1"])), + createStore({ entries: [] }), + ) + const [shell, setShell, shellInit] = persisted( + Persist.prompt(Persist.global("prompt-history-shell", ["prompt-history-shell.v1"])), + createStore({ entries: [] }), + ) + const history = createPromptInputHistoryStore(normal, setNormal, shell, setShell) + return { + ...history, + add(prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) { + const ready = mode === "shell" ? shellInit : normalInit + if (!(ready instanceof Promise)) return history.add(prompt, mode, comments) + const saved = clonePromptParts(prompt) + const metadata = clonePromptHistoryComments(comments) + void ready.then(() => history.add(saved, mode, metadata)) + }, + } +} diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..94f24ce935ff8801b69acd8a558d7a7f59c2f7c6 --- /dev/null +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, test } from "bun:test" +import type { Prompt } from "@/context/prompt" +import { + canNavigateHistoryAtCursor, + clonePromptParts, + normalizePromptHistoryEntry, + navigatePromptHistory, + prependHistoryEntry, + promptLength, + type PromptHistoryComment, +} from "./history" + +const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] + +const text = (value: string): Prompt => [{ type: "text", content: value, start: 0, end: value.length }] +const comment = (id: string, value = "note"): PromptHistoryComment => ({ + id, + path: "src/a.ts", + selection: { start: 2, end: 4 }, + comment: value, + time: 1, + origin: "review", + preview: "const a = 1", +}) + +describe("prompt-input history", () => { + test("prependHistoryEntry skips empty prompt and deduplicates consecutive entries", () => { + const first = prependHistoryEntry([], DEFAULT_PROMPT) + expect(first).toEqual([]) + + const commentsOnly = prependHistoryEntry([], DEFAULT_PROMPT, [comment("c1")]) + expect(commentsOnly).toHaveLength(1) + + const withOne = prependHistoryEntry([], text("hello")) + expect(withOne).toHaveLength(1) + + const deduped = prependHistoryEntry(withOne, text("hello")) + expect(deduped).toBe(withOne) + + const dedupedComments = prependHistoryEntry(commentsOnly, DEFAULT_PROMPT, [comment("c1")]) + expect(dedupedComments).toBe(commentsOnly) + }) + + test("navigatePromptHistory restores saved prompt when moving down from newest", () => { + const entries = [text("third"), text("second"), text("first")] + const up = navigatePromptHistory({ + direction: "up", + entries, + historyIndex: -1, + currentPrompt: text("draft"), + currentComments: [comment("draft")], + savedPrompt: null, + }) + expect(up.handled).toBe(true) + if (!up.handled) throw new Error("expected handled") + expect(up.historyIndex).toBe(0) + expect(up.cursor).toBe("start") + expect(up.entry.comments).toEqual([]) + + const down = navigatePromptHistory({ + direction: "down", + entries, + historyIndex: up.historyIndex, + currentPrompt: text("ignored"), + currentComments: [], + savedPrompt: up.savedPrompt, + }) + expect(down.handled).toBe(true) + if (!down.handled) throw new Error("expected handled") + expect(down.historyIndex).toBe(-1) + expect(down.entry.prompt[0]?.type === "text" ? down.entry.prompt[0].content : "").toBe("draft") + expect(down.entry.comments).toEqual([comment("draft")]) + }) + + test("navigatePromptHistory keeps entry comments when moving through history", () => { + const entries = [ + { + prompt: text("with comment"), + comments: [comment("c1")], + }, + ] + + const up = navigatePromptHistory({ + direction: "up", + entries, + historyIndex: -1, + currentPrompt: text("draft"), + currentComments: [], + savedPrompt: null, + }) + + expect(up.handled).toBe(true) + if (!up.handled) throw new Error("expected handled") + expect(up.entry.prompt[0]?.type === "text" ? up.entry.prompt[0].content : "").toBe("with comment") + expect(up.entry.comments).toEqual([comment("c1")]) + }) + + test("normalizePromptHistoryEntry supports legacy prompt arrays", () => { + const entry = normalizePromptHistoryEntry(text("legacy")) + expect(entry.prompt[0]?.type === "text" ? entry.prompt[0].content : "").toBe("legacy") + expect(entry.comments).toEqual([]) + }) + + test("helpers clone prompt and count text content length", () => { + const original: Prompt = [ + { type: "text", content: "one", start: 0, end: 3 }, + { + type: "file", + path: "src/a.ts", + content: "@src/a.ts", + start: 3, + end: 12, + selection: { startLine: 1, startChar: 1, endLine: 2, endChar: 1 }, + }, + { type: "image", id: "1", filename: "img.png", mime: "image/png", blob: { id: "blob", url: "blob:test" } }, + ] + const copy = clonePromptParts(original) + expect(copy).not.toBe(original) + expect(promptLength(copy)).toBe(12) + if (copy[1]?.type !== "file") throw new Error("expected file") + copy[1].selection!.startLine = 9 + if (original[1]?.type !== "file") throw new Error("expected file") + expect(original[1].selection?.startLine).toBe(1) + }) + + test("canNavigateHistoryAtCursor only allows prompt boundaries", () => { + const value = "a\nb\nc" + + expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(false) + expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false) + + expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false) + expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false) + + expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false) + expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true) + + expect(canNavigateHistoryAtCursor("up", "abc", 0)).toBe(false) + expect(canNavigateHistoryAtCursor("down", "abc", 3)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(false) + expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(false) + + expect(canNavigateHistoryAtCursor("up", "", 0)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "", 0)).toBe(true) + + expect(canNavigateHistoryAtCursor("up", "abc", 0, true)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 3, true)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "abc", 0, true)).toBe(true) + expect(canNavigateHistoryAtCursor("down", "abc", 3, true)).toBe(true) + expect(canNavigateHistoryAtCursor("up", "abc", 1, true)).toBe(false) + expect(canNavigateHistoryAtCursor("down", "abc", 1, true)).toBe(false) + }) +}) diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts new file mode 100644 index 0000000000000000000000000000000000000000..79e8abc0d9ebd7fb1c7d56534b148d0cb39f1824 --- /dev/null +++ b/packages/app/src/components/prompt-input/history.ts @@ -0,0 +1,256 @@ +import type { Prompt } from "@/context/prompt" +import type { SelectedLineRange } from "@/context/file" + +const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }] + +export const MAX_HISTORY = 100 + +export type PromptHistoryComment = { + id: string + path: string + selection: SelectedLineRange + comment: string + time: number + origin?: "review" | "file" + preview?: string +} + +export type PromptHistoryEntry = { + prompt: Prompt + comments: PromptHistoryComment[] +} + +export type PromptHistoryStoredEntry = Prompt | PromptHistoryEntry + +export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number, inHistory = false) { + const position = Math.max(0, Math.min(cursor, text.length)) + const atStart = position === 0 + const atEnd = position === text.length + if (inHistory) return atStart || atEnd + if (direction === "up") return position === 0 && text.length === 0 + return position === text.length +} + +export function clonePromptParts(prompt: Prompt): Prompt { + return prompt.map((part) => { + if (part.type === "text") return { ...part } + if (part.type === "image") return { ...part } + if (part.type === "agent") return { ...part } + return { + ...part, + selection: part.selection ? { ...part.selection } : undefined, + } + }) +} + +function cloneSelection(selection: SelectedLineRange): SelectedLineRange { + return { + start: selection.start, + end: selection.end, + ...(selection.side ? { side: selection.side } : {}), + ...(selection.endSide ? { endSide: selection.endSide } : {}), + } +} + +export function clonePromptHistoryComments(comments: PromptHistoryComment[]) { + return comments.map((comment) => ({ + ...comment, + selection: cloneSelection(comment.selection), + })) +} + +export function normalizePromptHistoryEntry(entry: PromptHistoryStoredEntry): PromptHistoryEntry { + if (Array.isArray(entry)) { + return { + prompt: clonePromptParts(entry), + comments: [], + } + } + return { + prompt: clonePromptParts(entry.prompt), + comments: clonePromptHistoryComments(entry.comments), + } +} + +export function promptLength(prompt: Prompt) { + return prompt.reduce((len, part) => len + ("content" in part ? part.content.length : 0), 0) +} + +export function prependHistoryEntry( + entries: PromptHistoryStoredEntry[], + prompt: Prompt, + comments: PromptHistoryComment[] = [], + max = MAX_HISTORY, +) { + const text = prompt + .map((part) => ("content" in part ? part.content : "")) + .join("") + .trim() + const hasImages = prompt.some((part) => part.type === "image") + const hasComments = comments.some((comment) => !!comment.comment.trim()) + if (!text && !hasImages && !hasComments) return entries + + const entry = { + prompt: clonePromptParts(prompt), + comments: clonePromptHistoryComments(comments), + } satisfies PromptHistoryEntry + const last = entries[0] + if (last && isPromptEqual(last, entry)) return entries + return [entry, ...entries].slice(0, max) +} + +function isCommentEqual(commentA: PromptHistoryComment, commentB: PromptHistoryComment) { + return ( + commentA.path === commentB.path && + commentA.comment === commentB.comment && + commentA.origin === commentB.origin && + commentA.preview === commentB.preview && + commentA.selection.start === commentB.selection.start && + commentA.selection.end === commentB.selection.end && + commentA.selection.side === commentB.selection.side && + commentA.selection.endSide === commentB.selection.endSide + ) +} + +function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistoryStoredEntry) { + const entryA = normalizePromptHistoryEntry(promptA) + const entryB = normalizePromptHistoryEntry(promptB) + if (entryA.prompt.length !== entryB.prompt.length) return false + for (let i = 0; i < entryA.prompt.length; i++) { + const partA = entryA.prompt[i] + const partB = entryB.prompt[i] + if (partA.type !== partB.type) return false + if (partA.type === "text" && partA.content !== (partB.type === "text" ? partB.content : "")) return false + if (partA.type === "file") { + if (partA.path !== (partB.type === "file" ? partB.path : "")) return false + const a = partA.selection + const b = partB.type === "file" ? partB.selection : undefined + const sameSelection = + (!a && !b) || + (!!a && + !!b && + a.startLine === b.startLine && + a.startChar === b.startChar && + a.endLine === b.endLine && + a.endChar === b.endChar) + if (!sameSelection) return false + } + if (partA.type === "agent" && partA.name !== (partB.type === "agent" ? partB.name : "")) return false + if (partA.type === "image" && partA.id !== (partB.type === "image" ? partB.id : "")) return false + } + if (entryA.comments.length !== entryB.comments.length) return false + for (let i = 0; i < entryA.comments.length; i++) { + const commentA = entryA.comments[i] + const commentB = entryB.comments[i] + if (!commentA || !commentB || !isCommentEqual(commentA, commentB)) return false + } + return true +} + +type HistoryNavInput = { + direction: "up" | "down" + entries: PromptHistoryStoredEntry[] + historyIndex: number + currentPrompt: Prompt + currentComments: PromptHistoryComment[] + savedPrompt: PromptHistoryEntry | null +} + +type HistoryNavResult = + | { + handled: false + historyIndex: number + savedPrompt: PromptHistoryEntry | null + } + | { + handled: true + historyIndex: number + savedPrompt: PromptHistoryEntry | null + entry: PromptHistoryEntry + cursor: "start" | "end" + } + +export function navigatePromptHistory(input: HistoryNavInput): HistoryNavResult { + if (input.direction === "up") { + if (input.entries.length === 0) { + return { + handled: false, + historyIndex: input.historyIndex, + savedPrompt: input.savedPrompt, + } + } + + if (input.historyIndex === -1) { + const entry = normalizePromptHistoryEntry(input.entries[0]) + return { + handled: true, + historyIndex: 0, + savedPrompt: { + prompt: clonePromptParts(input.currentPrompt), + comments: clonePromptHistoryComments(input.currentComments), + }, + entry, + cursor: "start", + } + } + + if (input.historyIndex < input.entries.length - 1) { + const next = input.historyIndex + 1 + const entry = normalizePromptHistoryEntry(input.entries[next]) + return { + handled: true, + historyIndex: next, + savedPrompt: input.savedPrompt, + entry, + cursor: "start", + } + } + + return { + handled: false, + historyIndex: input.historyIndex, + savedPrompt: input.savedPrompt, + } + } + + if (input.historyIndex > 0) { + const next = input.historyIndex - 1 + const entry = normalizePromptHistoryEntry(input.entries[next]) + return { + handled: true, + historyIndex: next, + savedPrompt: input.savedPrompt, + entry, + cursor: "end", + } + } + + if (input.historyIndex === 0) { + if (input.savedPrompt) { + return { + handled: true, + historyIndex: -1, + savedPrompt: null, + entry: input.savedPrompt, + cursor: "end", + } + } + + return { + handled: true, + historyIndex: -1, + savedPrompt: null, + entry: { + prompt: DEFAULT_PROMPT, + comments: [], + }, + cursor: "end", + } + } + + return { + handled: false, + historyIndex: input.historyIndex, + savedPrompt: input.savedPrompt, + } +} diff --git a/packages/app/src/components/prompt-input/image-attachments.css b/packages/app/src/components/prompt-input/image-attachments.css new file mode 100644 index 0000000000000000000000000000000000000000..2aaf494ebfc47dc7cd17d5358e895e09625997b6 --- /dev/null +++ b/packages/app/src/components/prompt-input/image-attachments.css @@ -0,0 +1,43 @@ +@keyframes prompt-attachments-fade-left { + from { + visibility: hidden; + } + to { + visibility: visible; + } +} + +@keyframes prompt-attachments-fade-right { + from { + visibility: visible; + } + to { + visibility: hidden; + } +} + +[data-slot="prompt-attachments"] { + timeline-scope: --prompt-attachments-scroll; + + [data-slot^="prompt-attachments-fade-"] { + visibility: hidden; + } +} + +@supports (animation-timeline: --prompt-attachments-scroll) and (timeline-scope: --prompt-attachments-scroll) { + [data-slot="prompt-attachments-scroll"] { + scroll-timeline: --prompt-attachments-scroll x; + } + + [data-slot="prompt-attachments-fade-left"] { + animation: prompt-attachments-fade-left linear both; + animation-timeline: --prompt-attachments-scroll; + animation-range: 0 0.1px; + } + + [data-slot="prompt-attachments-fade-right"] { + animation: prompt-attachments-fade-right linear both; + animation-timeline: --prompt-attachments-scroll; + animation-range: calc(100% - 1.1px) calc(100% - 1px); + } +} diff --git a/packages/app/src/components/prompt-input/image-attachments.tsx b/packages/app/src/components/prompt-input/image-attachments.tsx new file mode 100644 index 0000000000000000000000000000000000000000..96b9147bed82c4be87f0ccfc2a6b00bffe662714 --- /dev/null +++ b/packages/app/src/components/prompt-input/image-attachments.tsx @@ -0,0 +1,169 @@ +import { Component, For, Show } from "solid-js" +import { Icon } from "@opencode-ai/ui/icon" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { AttachmentCardV2 } from "@opencode-ai/session-ui/v2/attachment-card-v2" +import { CommentCardV2 } from "@opencode-ai/session-ui/v2/comment-card-v2" +import { typeLabel } from "@opencode-ai/session-ui/message-file" +import type { ContextItem, ImageAttachmentPart } from "@/context/prompt" +import "./image-attachments.css" + +type PromptCommentItem = ContextItem & { key: string } + +type PromptImageAttachmentsProps = { + attachments: ImageAttachmentPart[] + onOpen: (attachment: ImageAttachmentPart) => void + onRemove: (id: string) => void + removeLabel: string + fileLabel: string + newLayoutDesigns: boolean + comments?: PromptCommentItem[] + commentActive?: (item: PromptCommentItem) => boolean + onOpenComment?: (item: PromptCommentItem) => void + onRemoveComment?: (item: PromptCommentItem) => void +} + +const fallbackClass = "size-16 rounded-md bg-surface-base flex items-center justify-center border border-border-base" +const imageClass = + "size-16 rounded-md object-cover border border-border-base hover:border-border-strong-base transition-colors" +const imageClassV2 = "w-[58px] h-[46px] rounded-[6px] object-cover" +// inset box-shadows do not paint over content, so the hairline is a separate overlay +const imageHairlineClassV2 = + "absolute inset-0 rounded-[6px] shadow-[inset_0_0_0_0.5px_var(--v2-border-border-base)] pointer-events-none" +const removeClass = + "absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover" +const removeClassV2 = + "absolute -top-1 -right-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity" +const nameClass = "absolute bottom-0 left-0 right-0 px-1 py-0.5 bg-black/50 rounded-b-md" + +export const PromptImageAttachments: Component = (props) => { + return ( + 0 || (props.newLayoutDesigns && (props.comments?.length ?? 0) > 0)}> +
+
+ + + {(item) => ( +
+ + props.onOpenComment?.(item)} + /> + + +
+ )} +
+
+ + {(attachment) => { + const image = attachment.mime.startsWith("image/") + const media = () => ( + + +
+ } + > + + {typeLabel(attachment.filename, attachment.mime, props.fileLabel)} + + + } + > + {attachment.filename} props.onOpen(attachment)} + /> + + ) + const name = () => ( +
+ {attachment.filename} +
+ ) + const remove = () => ( + + ) + // v2 keeps the remove button outside the tooltip trigger so hovering it dismisses the tooltip + return ( + +
+ {media()} + {name()} + {remove()} +
+ + } + > +
+ + {media()} + +
+ + + {remove()} +
+
+ ) + }} + +
+ +
+
+ +
+ + ) +} diff --git a/packages/app/src/components/prompt-input/paste.ts b/packages/app/src/components/prompt-input/paste.ts new file mode 100644 index 0000000000000000000000000000000000000000..6787d5030907d702a4c84c321244f2c27a691dda --- /dev/null +++ b/packages/app/src/components/prompt-input/paste.ts @@ -0,0 +1,24 @@ +const LARGE_PASTE_CHARS = 8000 +const LARGE_PASTE_BREAKS = 120 + +function largePaste(text: string) { + if (text.length >= LARGE_PASTE_CHARS) return true + let breaks = 0 + for (const char of text) { + if (char !== "\n") continue + breaks += 1 + if (breaks >= LARGE_PASTE_BREAKS) return true + } + return false +} + +export function normalizePaste(text: string) { + if (!text.includes("\r")) return text + return text.replace(/\r\n?/g, "\n") +} + +export function pasteMode(text: string) { + if (largePaste(text)) return "manual" + if (text.includes("\n") || text.includes("\r")) return "manual" + return "native" +} diff --git a/packages/app/src/components/prompt-input/placeholder.test.ts b/packages/app/src/components/prompt-input/placeholder.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5852e25f412e78467d80ffdefb269dc881862cdb --- /dev/null +++ b/packages/app/src/components/prompt-input/placeholder.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, test } from "bun:test" +import { promptDesignPlaceholder, promptPlaceholder } from "./placeholder" + +describe("promptPlaceholder", () => { + const t = (key: string, params?: Record) => `${key}${params?.example ? `:${params.example}` : ""}` + + test("returns shell placeholder in shell mode", () => { + const value = promptPlaceholder({ + mode: "shell", + commentCount: 0, + example: "example", + suggest: true, + t, + }) + expect(value).toBe("prompt.placeholder.shell:example") + }) + + test("returns summarize placeholders for comment context", () => { + expect(promptPlaceholder({ mode: "normal", commentCount: 1, example: "example", suggest: true, t })).toBe( + "prompt.placeholder.summarizeComment", + ) + expect(promptPlaceholder({ mode: "normal", commentCount: 2, example: "example", suggest: true, t })).toBe( + "prompt.placeholder.summarizeComments", + ) + }) + + test("returns default placeholder with example when suggestions enabled", () => { + const value = promptPlaceholder({ + mode: "normal", + commentCount: 0, + example: "translated-example", + suggest: true, + t, + }) + expect(value).toBe("prompt.placeholder.normal:translated-example") + }) + + test("returns simple placeholder when suggestions disabled", () => { + const value = promptPlaceholder({ + mode: "normal", + commentCount: 0, + example: "translated-example", + suggest: false, + t, + }) + expect(value).toBe("prompt.placeholder.simple") + }) +}) + +describe("promptDesignPlaceholder", () => { + const t = (key: string, params?: Record) => { + if (key !== "ui.promptInput.placeholder.normal") return key + return `Ask anything, ${params?.slash} for commands, ${params?.at} for context...` + } + + test("composes the design placeholder from localized fragments", () => { + expect(promptDesignPlaceholder("normal", "fallback", t)).toBe("Ask anything, / for commands, @ for context...") + }) + + test("preserves the shell placeholder", () => { + expect(promptDesignPlaceholder("shell", "Enter shell command...", t)).toBe("Enter shell command...") + }) +}) diff --git a/packages/app/src/components/prompt-input/placeholder.ts b/packages/app/src/components/prompt-input/placeholder.ts new file mode 100644 index 0000000000000000000000000000000000000000..9bd4417f49824f249bc519962e2af795bdc8896f --- /dev/null +++ b/packages/app/src/components/prompt-input/placeholder.ts @@ -0,0 +1,24 @@ +type PromptPlaceholderInput = { + mode: "normal" | "shell" + commentCount: number + example: string + suggest: boolean + t: (key: string, params?: Record) => string +} + +export function promptPlaceholder(input: PromptPlaceholderInput) { + if (input.mode === "shell") return input.t("prompt.placeholder.shell", { example: input.example }) + if (input.commentCount > 1) return input.t("prompt.placeholder.summarizeComments") + if (input.commentCount === 1) return input.t("prompt.placeholder.summarizeComment") + if (!input.suggest) return input.t("prompt.placeholder.simple") + return input.t("prompt.placeholder.normal", { example: input.example }) +} + +export function promptDesignPlaceholder( + mode: PromptPlaceholderInput["mode"], + placeholder: string, + t: PromptPlaceholderInput["t"], +) { + if (mode === "shell") return placeholder + return t("ui.promptInput.placeholder.normal", { slash: "/", at: "@" }) +} diff --git a/packages/app/src/components/prompt-input/slash-popover.tsx b/packages/app/src/components/prompt-input/slash-popover.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b9a2dc7afaf31cbd05046ca86b620f1d8ca8034e --- /dev/null +++ b/packages/app/src/components/prompt-input/slash-popover.tsx @@ -0,0 +1,376 @@ +import { Component, For, Match, Show, Switch } from "solid-js" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { Icon } from "@opencode-ai/ui/icon" +import { Tag } from "@opencode-ai/ui/v2/badge-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" + +export type AtOption = + | { type: "agent"; name: string; display: string } + | { + type: "resource" + name: string + uri: string + client: string + display: string + description?: string + mime?: string + } + | { type: "reference"; name: string; path: string; display: string; description: string } + | { type: "file"; path: string; display: string; recent?: boolean } + +export interface SlashCommand { + id: string + trigger: string + title: string + description?: string + keybind?: string + type: "builtin" | "custom" + source?: "command" | "mcp" | "skill" +} + +type PromptPopoverProps = { + popover: "at" | "slash" | null + setSlashPopoverRef: (el: HTMLDivElement) => void + atFlat: AtOption[] + atActive?: string + atKey: (item: AtOption) => string + setAtActive: (id: string) => void + onAtSelect: (item: AtOption) => void + slashFlat: SlashCommand[] + slashActive?: string + setSlashActive: (id: string) => void + onSlashSelect: (item: SlashCommand) => void + slashMenu: boolean + slashMenuQuery: string + onSlashMenuInput: (value: string) => void + onSlashMenuKeyDown: (event: KeyboardEvent) => void + commandKeybind: (id: string) => string | undefined + commandKeybindParts: (id: string) => string[] + newLayoutDesigns: boolean + t: (key: string) => string +} + +export const PromptPopover: Component = (props) => { + return ( + +
{ + if (props.popover === "slash") props.setSlashPopoverRef(el) + }} + class="absolute inset-x-0 -top-2 -translate-y-full origin-bottom-left max-h-80 min-h-10 + overflow-auto no-scrollbar flex flex-col p-2" + classList={{ + "z-[70] rounded-[10px] bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]": props.newLayoutDesigns, + "rounded-[12px] bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-lg-border-base)]": + !props.newLayoutDesigns, + }} + onMouseDown={(e) => e.preventDefault()} + > + + + 0} + fallback={ +
+ {props.t("prompt.popover.emptyResults")} +
+ } + > + + {(item) => { + const key = props.atKey(item) + + if (item.type === "agent") { + return ( + + ) + } + + if (item.type === "resource") { + return ( + + ) + } + + if (item.type === "reference") { + return ( + + ) + } + + const isDirectory = item.path.endsWith("/") + const directory = isDirectory ? item.path : getDirectory(item.path) + const filename = isDirectory ? "" : getFilename(item.path) + + return ( + + ) + }} + +
+
+ + +
+ requestAnimationFrame(() => el.focus())} + value={props.slashMenuQuery} + onInput={(event) => props.onSlashMenuInput(event.currentTarget.value)} + onKeyDown={props.onSlashMenuKeyDown} + onMouseDown={(event) => event.stopPropagation()} + aria-label={props.t("prompt.menu.commands")} + placeholder="/" + class="w-full bg-transparent outline-none text-[13px] leading-5 text-v2-text-text-base placeholder:text-v2-text-text-faint" + /> +
+
+ 0} + fallback={ +
+ {props.t("prompt.popover.emptyCommands")} +
+ } + > + + {(cmd) => { + const keybind = () => props.commandKeybind(cmd.id) + const keybindParts = () => props.commandKeybindParts(cmd.id) + return ( + + ) + }} + +
+
+
+
+
+ ) +} diff --git a/packages/app/src/components/prompt-input/submission-state.ts b/packages/app/src/components/prompt-input/submission-state.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9ca735609e5da4a8b33f55fe664c5d690d2d188 --- /dev/null +++ b/packages/app/src/components/prompt-input/submission-state.ts @@ -0,0 +1,33 @@ +import { type ContextItem, type Prompt, type usePrompt } from "@/context/prompt" + +type PromptTarget = ReturnType["capture"]> + +export function createPromptSubmissionState(input: { + target: PromptTarget + prompt: Prompt + context: (ContextItem & { key: string })[] +}) { + const initial = input.target + let target = input.target + let cleared: Prompt | undefined + + return { + prompt: input.prompt, + context: input.context, + target: () => target, + clear() { + if (initial !== target) initial.reset() + target.reset() + cleared = target.current() + }, + retarget(next: PromptTarget) { + input.context.forEach(next.context.add) + target = next + }, + current: (value: PromptTarget) => target === value, + restore() { + if (cleared !== undefined && target.current() !== cleared) return + return { target, prompt: input.prompt, context: input.context } + }, + } +} diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b3201b3ef68a0fa3b94525bd67fa179b5ea2230c --- /dev/null +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -0,0 +1,598 @@ +import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test" +import { createStore } from "solid-js/store" +import type { Prompt, PromptStore } from "@/context/prompt" +import type { ModelSelection } from "@/context/local" + +let createPromptSubmit: typeof import("./submit").createPromptSubmit + +const createdClients: string[] = [] +const createdSessions: string[] = [] +const sessionCreateInputs: Array<{ + agent?: string + model?: { id: string; providerID: string; variant?: string } + location?: { directory: string } +}> = [] +const enabledAutoAccept: Array<{ server: string; sessionID: string; directory: string }> = [] +const optimistic: Array<{ + directory?: string + sessionID?: string + message: { + agent: string + model: { providerID: string; modelID: string } + variant?: string + } +}> = [] +const optimisticSeeded: boolean[] = [] +const storedSessions: Record> = {} +const promoted: Array<{ directory: string; sessionID: string }> = [] +const sentShell: Array<{ sessionID: string; id?: string; command: string }> = [] +const syncedDirectories: string[] = [] +const promotedDrafts: Array<{ draftID: string; server: string; sessionId: string }> = [] +const sentPrompts: string[] = [] +const promptInputs: unknown[] = [] +const sentCommands: unknown[] = [] +const commands: Array<{ name: string }> = [] +let serverSessionSyncs = 0 + +let params: { id?: string } = {} +let search: { draftId?: string } = {} +let selected = "/repo/worktree-a" +let variant: string | undefined +let permissionServer = "server-a" +let createSessionGate: Promise | undefined + +let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] +const [promptStore, setPromptStore] = createStore({ + prompt: promptValue, + cursor: 0, + context: { items: [] }, +}) +const prompt = { + store: [() => promptStore, setPromptStore] as [() => PromptStore, typeof setPromptStore], + ready: Object.assign(() => true, { promise: Promise.resolve(true) }), + current: () => promptValue, + cursor: () => 0, + dirty: () => true, + model: { + current: () => undefined, + set: () => undefined, + }, + reset: () => undefined, + set: () => undefined, + context: { + add: () => undefined, + remove: () => undefined, + removeComment: () => undefined, + updateComment: () => undefined, + replaceComments: () => undefined, + items: () => [], + }, + capture: () => prompt, +} + +const clientFor = (directory: string) => { + createdClients.push(directory) + return { + api: { + session: { + create: async (input: (typeof sessionCreateInputs)[number]) => { + await createSessionGate + const location = input.location?.directory ?? directory + createdSessions.push(location) + sessionCreateInputs.push(input) + return { + id: `session-${createdSessions.length}`, + projectID: "project", + agent: input.agent, + model: input.model, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, updated: 1 }, + title: `New session ${createdSessions.length}`, + location: { directory: location }, + } + }, + prompt: async (input: unknown) => { + sentPrompts.push(directory) + promptInputs.push(input) + return { data: undefined } + }, + command: async (input: unknown) => { + sentCommands.push(input) + }, + shell: async (input: { sessionID: string; id?: string; command: string }) => { + sentShell.push(input) + }, + }, + }, + session: { + command: async () => ({ data: undefined }), + abort: async () => ({ data: undefined }), + }, + worktree: { + create: async () => ({ data: { directory: `${directory}/new` } }), + }, + } +} + +beforeAll(async () => { + const rootClient = clientFor("/repo/main") + + mock.module("@solidjs/router", () => ({ + useNavigate: () => () => undefined, + useParams: () => params, + useLocation: () => ({}), + useSearchParams: () => [search, () => undefined], + })) + + mock.module("@opencode-ai/sdk/v2/client", () => ({ + createOpencodeClient: (input: { directory: string }) => { + createdClients.push(input.directory) + return clientFor(input.directory) + }, + })) + + mock.module("@opencode-ai/ui/toast", () => ({ + Toast: { Region: () => null }, + showToast: () => 0, + })) + + mock.module("@opencode-ai/core/util/encode", () => ({ + base64Encode: (value: string) => value, + })) + + mock.module("@/context/local", () => ({ + useLocal: () => ({ + model: { + current: () => ({ id: "model", provider: { id: "provider" } }), + variant: { current: () => variant }, + }, + agent: { + current: () => ({ name: "agent" }), + }, + session: { + promote(directory: string, sessionID: string) { + promoted.push({ directory, sessionID }) + }, + }, + }), + })) + + mock.module("@/context/permission", () => { + const state = (server: string) => ({ + enableAutoAccept(sessionID: string, directory: string) { + enabledAutoAccept.push({ server, sessionID, directory }) + }, + }) + return { usePermission: () => ({ currentServerState: () => state(permissionServer) }) } + }) + + mock.module("@/context/server", () => ({ + useServer: () => ({ key: "server-key" }), + })) + + mock.module("@/context/tabs", () => ({ + useTabs: () => ({ + draft: () => ({ server: "project-server" }), + promoteDraft: (draftID: string, session: { server: string; sessionId: string }) => { + promotedDrafts.push({ draftID, ...session }) + }, + }), + })) + + mock.module("@/context/prompt", () => ({ + usePrompt: () => prompt, + })) + + mock.module("@/context/layout", () => ({ + useLayout: () => ({ + handoff: { + setTabs: () => undefined, + }, + }), + })) + + mock.module("@/context/sdk", () => ({ + useSDK: () => { + const sdk = { + scope: "local", + directory: "/repo/main", + client: rootClient, + api: rootClient.api, + url: "http://localhost:4096", + createClient(opts: any) { + return clientFor(opts.directory) + }, + } + return () => sdk + }, + })) + + mock.module("@/context/sync", () => ({ + useSync: () => () => ({ + data: { command: commands }, + session: { + optimistic: { + add: (value: { + directory?: string + sessionID?: string + message: { agent: string; model: { providerID: string; modelID: string; variant?: string } } + }) => { + optimistic.push(value) + optimisticSeeded.push( + !!value.directory && + !!value.sessionID && + !!storedSessions[value.directory]?.find((item) => item.id === value.sessionID)?.title, + ) + }, + remove: () => undefined, + }, + }, + set: () => undefined, + }), + })) + + mock.module("@/context/server-sync", () => ({ + useServerSync: () => () => ({ + session: { + remember: () => undefined, + set: () => undefined, + sync: async () => { + serverSessionSyncs++ + }, + }, + child: (directory: string) => { + syncedDirectories.push(directory) + storedSessions[directory] ??= [] + return [ + { session: storedSessions[directory] }, + (...args: unknown[]) => { + if (args[0] !== "session") return + const next = args[1] + if (typeof next === "function") { + storedSessions[directory] = next(storedSessions[directory]) as Array<{ id: string; title?: string }> + return + } + if (Array.isArray(next)) { + storedSessions[directory] = next as Array<{ id: string; title?: string }> + } + }, + ] + }, + }), + })) + + mock.module("@/context/platform", () => ({ + usePlatform: () => ({ + fetch: fetch, + }), + })) + + mock.module("@/context/language", () => ({ + useLanguage: () => ({ + t: (key: string) => key, + }), + })) + + const mod = await import("./submit") + createPromptSubmit = mod.createPromptSubmit +}) + +beforeEach(() => { + createdClients.length = 0 + createdSessions.length = 0 + sessionCreateInputs.length = 0 + enabledAutoAccept.length = 0 + optimistic.length = 0 + optimisticSeeded.length = 0 + promoted.length = 0 + promotedDrafts.length = 0 + sentPrompts.length = 0 + promptInputs.length = 0 + sentCommands.length = 0 + commands.length = 0 + promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }] + params = {} + search = {} + sentShell.length = 0 + syncedDirectories.length = 0 + selected = "/repo/worktree-a" + variant = undefined + permissionServer = "server-a" + createSessionGate = undefined + serverSessionSyncs = 0 + for (const key of Object.keys(storedSessions)) delete storedSessions[key] +}) + +describe("prompt submit worktree selection", () => { + test("reads the latest worktree accessor value per submit", async () => { + const submit = createPromptSubmit({ + prompt, + info: () => undefined, + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "shell", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + newSessionWorktree: () => selected, + onNewSessionWorktreeReset: () => undefined, + onSubmit: () => undefined, + }) + + const event = { preventDefault: () => undefined } as unknown as Event + + await submit.handleSubmit(event) + selected = "/repo/worktree-b" + await submit.handleSubmit(event) + + expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) + expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) + expect(sessionCreateInputs).toEqual([ + { + agent: "agent", + model: { id: "model", providerID: "provider", variant: undefined }, + location: { directory: "/repo/worktree-a" }, + }, + { + agent: "agent", + model: { id: "model", providerID: "provider", variant: undefined }, + location: { directory: "/repo/worktree-b" }, + }, + ]) + expect(sentShell).toEqual([ + expect.objectContaining({ sessionID: "session-1", id: expect.stringMatching(/^evt_/), command: "ls" }), + expect.objectContaining({ sessionID: "session-2", id: expect.stringMatching(/^evt_/), command: "ls" }), + ]) + expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) + expect(serverSessionSyncs).toBe(0) + expect(promoted).toEqual([ + { directory: "/repo/worktree-a", sessionID: "session-1" }, + { directory: "/repo/worktree-b", sessionID: "session-2" }, + ]) + expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) + }) + + test("applies auto-accept to newly created sessions", async () => { + const submit = createPromptSubmit({ + prompt, + info: () => undefined, + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => true, + mode: () => "shell", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + newSessionWorktree: () => selected, + onNewSessionWorktreeReset: () => undefined, + onSubmit: () => undefined, + }) + + const event = { preventDefault: () => undefined } as unknown as Event + + await submit.handleSubmit(event) + + expect(enabledAutoAccept).toEqual([{ server: "server-a", sessionID: "session-1", directory: "/repo/worktree-a" }]) + }) + + test("keeps auto-accept bound to the submission server", async () => { + let release = () => {} + createSessionGate = new Promise((resolve) => { + release = resolve + }) + const submit = createPromptSubmit({ + prompt, + info: () => undefined, + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => true, + mode: () => "shell", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + newSessionWorktree: () => selected, + onNewSessionWorktreeReset: () => undefined, + onSubmit: () => undefined, + }) + + const result = submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) + permissionServer = "server-b" + release() + await result + + expect(enabledAutoAccept).toEqual([{ server: "server-a", sessionID: "session-1", directory: "/repo/worktree-a" }]) + }) + + test("promotes drafts using the selected project's server", async () => { + search = { draftId: "draft-1" } + const submit = createPromptSubmit({ + prompt, + info: () => undefined, + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + newSessionWorktree: () => selected, + onNewSessionWorktreeReset: () => undefined, + onSubmit: () => undefined, + }) + + await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) + + expect(promotedDrafts).toEqual([{ draftID: "draft-1", server: "project-server", sessionId: "session-1" }]) + }) + + test("includes the selected variant on optimistic prompts", async () => { + params = { id: "session-1" } + variant = "high" + + const submit = createPromptSubmit({ + prompt, + info: () => ({ id: "session-1" }), + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + onSubmit: () => undefined, + }) + + const event = { preventDefault: () => undefined } as unknown as Event + + await submit.handleSubmit(event) + await Bun.sleep(0) + + expect(optimistic).toHaveLength(1) + expect(optimistic[0]).toMatchObject({ + message: { + agent: "agent", + model: { providerID: "provider", modelID: "model", variant: "high" }, + }, + }) + expect(sentPrompts).toEqual(["/repo/main"]) + expect(promptInputs[0]).toMatchObject({ + sessionID: "session-1", + text: "ls", + files: [], + agents: [], + }) + expect((promptInputs[0] as { id?: string }).id).toStartWith("msg_") + expect((promptInputs[0] as { legacyParts?: { id: string; type: string; text?: string }[] }).legacyParts).toEqual([ + { id: expect.stringMatching(/^prt_/), type: "text", text: "ls" }, + ]) + }) + + test("submits slash commands through the current session API", async () => { + params = { id: "session-1" } + variant = "high" + commands.push({ name: "review" }) + promptValue = [{ type: "text", content: "/review staged changes", start: 0, end: 22 }] + + const submit = createPromptSubmit({ + prompt, + info: () => ({ id: "session-1" }), + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + }) + + await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) + + expect(sentCommands).toEqual([ + { + sessionID: "session-1", + id: expect.stringMatching(/^msg_/), + command: "review", + arguments: "staged changes", + agent: "agent", + model: { id: "model", providerID: "provider", variant: "high" }, + files: [], + }, + ]) + expect(serverSessionSyncs).toBe(0) + }) + + test("uses an injected model selection", async () => { + params = { id: "session-1" } + const model = { + current: () => ({ id: "draft-model", provider: { id: "draft-provider" } }), + variant: { current: () => "draft-variant" }, + } as unknown as ModelSelection + const submit = createPromptSubmit({ + prompt, + info: () => ({ id: "session-1" }), + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + model, + }) + + await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) + + expect(optimistic[0]).toMatchObject({ + message: { + model: { providerID: "draft-provider", modelID: "draft-model", variant: "draft-variant" }, + }, + }) + }) + + test("seeds new sessions before optimistic prompts are added", async () => { + const submit = createPromptSubmit({ + prompt, + info: () => undefined, + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + newSessionWorktree: () => selected, + onNewSessionWorktreeReset: () => undefined, + onSubmit: () => undefined, + }) + + const event = { preventDefault: () => undefined } as unknown as Event + + await submit.handleSubmit(event) + + expect(storedSessions["/repo/worktree-a"]).toHaveLength(1) + expect(storedSessions["/repo/worktree-a"]?.[0]).toMatchObject({ id: "session-1", title: "New session 1" }) + expect(optimisticSeeded).toEqual([true]) + }) +}) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts new file mode 100644 index 0000000000000000000000000000000000000000..c82c3a439939e3dc18399c53cbe1e4d238175424 --- /dev/null +++ b/packages/app/src/components/prompt-input/submit.ts @@ -0,0 +1,645 @@ +import type { Message, Session } from "@opencode-ai/sdk/v2/client" +import { showToast } from "@/utils/toast" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { Binary } from "@opencode-ai/core/util/binary" +import { useNavigate, useParams, useSearchParams } from "@solidjs/router" +import { batch, startTransition, type Accessor } from "solid-js" +import { useTabs } from "@/context/tabs" +import { useServerSync, type ServerSync } from "@/context/server-sync" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useLocal, type ModelSelection } from "@/context/local" +import { usePermission } from "@/context/permission" +import { type ContextItem, type ImageAttachmentPart, type Prompt, type usePrompt } from "@/context/prompt" +import { useSDK, type DirectorySDK } from "@/context/sdk" +import { useSync, type DirectorySync } from "@/context/sync" +import { Identifier } from "@/utils/id" +import { Worktree as WorktreeState } from "@/utils/worktree" +import { buildRequestParts } from "./build-request-parts" +import { setCursorPosition } from "./editor-dom" +import { formatServerError } from "@/utils/server-errors" +import { ScopedKey } from "@/utils/server-scope" +import { createPromptSubmissionState } from "./submission-state" +import { normalizeSessionInfo } from "@/utils/session" +import { Event } from "@opencode-ai/schema/event" +import { blobDataUrl } from "@/utils/draft-store" + +type PendingPrompt = { + abort: AbortController + cleanup: VoidFunction +} + +const pending = new Map() + +export type FollowupDraft = { + sessionID: string + sessionDirectory: string + prompt: Prompt + context: (ContextItem & { key: string })[] + agent: string + model: { providerID: string; modelID: string } + variant?: string +} + +type FollowupSendInput = { + api: DirectorySDK["api"]["session"] + serverSync: ServerSync + sync: DirectorySync + draft: FollowupDraft + messageID?: string + optimisticBusy?: boolean + before?: () => Promise | boolean +} + +const draftText = (prompt: Prompt) => prompt.map((part) => ("content" in part ? part.content : "")).join("") + +const draftImages = (prompt: Prompt) => prompt.filter((part): part is ImageAttachmentPart => part.type === "image") + +export async function sendFollowupDraft(input: FollowupSendInput) { + const text = draftText(input.draft.prompt) + const images = draftImages(input.draft.prompt) + const setBusy = () => { + if (!input.optimisticBusy) return + input.serverSync.session.set("session_status", input.draft.sessionID, { type: "busy" }) + } + + const setIdle = () => { + if (!input.optimisticBusy) return + input.serverSync.session.set("session_status", input.draft.sessionID, { type: "idle" }) + } + + const wait = async () => { + const ok = await input.before?.() + if (ok === false) return false + return true + } + + const [head, ...tail] = text.split(" ") + const cmd = head?.startsWith("/") ? head.slice(1) : undefined + if (cmd && input.sync.data.command.find((item) => item.name === cmd)) { + setBusy() + try { + if (!(await wait())) { + setIdle() + return false + } + + const messageID = Identifier.ascending("message") + await input.api.command({ + sessionID: input.draft.sessionID, + id: messageID, + command: cmd, + arguments: tail.join(" "), + agent: input.draft.agent, + model: { + id: input.draft.model.modelID, + providerID: input.draft.model.providerID, + variant: input.draft.variant, + }, + files: await Promise.all( + images.map(async (attachment) => ({ + uri: await blobDataUrl(attachment.blob, attachment.mime), + name: attachment.filename, + })), + ), + }) + return true + } catch (err) { + setIdle() + throw err + } + } + + const messageID = input.messageID ?? Identifier.ascending("message") + const encodedImages = await Promise.all( + images.map(async (attachment) => ({ + ...attachment, + dataUrl: await blobDataUrl(attachment.blob, attachment.mime), + })), + ) + const { requestParts, optimisticParts } = buildRequestParts({ + prompt: input.draft.prompt, + context: input.draft.context, + images: encodedImages, + text, + sessionID: input.draft.sessionID, + messageID, + sessionDirectory: input.draft.sessionDirectory, + }) + + const message: Message = { + id: messageID, + sessionID: input.draft.sessionID, + role: "user", + time: { created: Date.now() }, + agent: input.draft.agent, + model: { ...input.draft.model, variant: input.draft.variant }, + } + + const add = () => + input.sync.session.optimistic.add({ + directory: input.draft.sessionDirectory, + sessionID: input.draft.sessionID, + message, + parts: optimisticParts, + }) + + const remove = () => + input.sync.session.optimistic.remove({ + directory: input.draft.sessionDirectory, + sessionID: input.draft.sessionID, + messageID, + }) + + batch(() => { + setBusy() + add() + }) + + try { + if (!(await wait())) { + batch(() => { + setIdle() + remove() + }) + return false + } + + await input.api.prompt({ + sessionID: input.draft.sessionID, + id: messageID, + agent: input.draft.agent, + model: input.draft.model, + variant: input.draft.variant, + legacyParts: requestParts, + text: requestParts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"), + files: requestParts.flatMap((part) => { + if (part.type !== "file") return [] + const text = part.source?.text + return [ + { + uri: part.url, + name: part.filename, + mention: text ? { start: text.start, end: text.end, text: text.value } : undefined, + }, + ] + }), + agents: requestParts.flatMap((part) => + part.type === "agent" + ? [ + { + name: part.name, + mention: part.source + ? { start: part.source.start, end: part.source.end, text: part.source.value } + : undefined, + }, + ] + : [], + ), + }) + return true + } catch (err) { + batch(() => { + setIdle() + remove() + }) + throw err + } +} + +type PromptSubmitInput = { + prompt: ReturnType + info: Accessor<{ id: string } | undefined> + imageAttachments: Accessor + commentCount: Accessor + autoAccept: Accessor + mode: Accessor<"normal" | "shell"> + working: Accessor + editor: () => HTMLDivElement | undefined + queueScroll: () => void + promptLength: (prompt: Prompt) => number + addToHistory: (prompt: Prompt, mode: "normal" | "shell") => void + resetHistoryNavigation: () => void + setMode: (mode: "normal" | "shell") => void + setPopover: (popover: "at" | "slash" | null) => void + newSessionWorktree?: Accessor + onNewSessionWorktreeReset?: () => void + shouldQueue?: Accessor + onQueue?: (draft: FollowupDraft) => void + onAbort?: () => void + onSubmit?: () => void + model?: ModelSelection +} + +export function createPromptSubmit(input: PromptSubmitInput) { + const navigate = useNavigate() + const sdk = useSDK() + const sync = useSync() + const serverSync = useServerSync() + const local = useLocal() + const permission = usePermission() + const prompt = input.prompt + const layout = useLayout() + const language = useLanguage() + const params = useParams() + const [search] = useSearchParams<{ draftId?: string }>() + const tabs = useTabs() + const pendingKey = (sessionID: string) => ScopedKey.from(sdk().scope, sessionID) + + const errorMessage = (err: unknown) => { + if (err && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message + if (err && typeof err === "object" && "data" in err) { + const data = (err as { data?: { message?: string } }).data + if (data?.message) return data.message + } + if (err instanceof Error) return err.message + return language.t("common.requestFailed") + } + + const abort = async () => { + const sessionID = params.id + if (!sessionID) return Promise.resolve() + + serverSync().session.set("todo", sessionID, []) + + input.onAbort?.() + + const key = pendingKey(sessionID) + const queued = pending.get(key) + if (queued) { + queued.abort.abort() + queued.cleanup() + pending.delete(key) + return Promise.resolve() + } + return sdk() + .api.session.interrupt({ sessionID }) + .catch(() => {}) + } + + const restoreCommentItems = ( + target: ReturnType["capture"]>, + items: (ContextItem & { key: string })[], + ) => { + for (const item of items) { + target.context.add({ + type: "file", + path: item.path, + selection: item.selection, + comment: item.comment, + commentID: item.commentID, + commentOrigin: item.commentOrigin, + preview: item.preview, + }) + } + } + + const clearContext = (target: ReturnType["capture"]>) => { + for (const item of target.context.items()) { + target.context.remove(item.key) + } + } + + const seed = (dir: string, info: Session) => { + serverSync().session.remember(info) + const [, setStore] = serverSync().child(dir) + setStore("session", (list: Session[]) => { + const result = Binary.search(list, info.id, (item) => item.id) + const next = [...list] + if (result.found) { + next[result.index] = info + return next + } + next.splice(result.index, 0, info) + return next + }) + } + + const handleSubmit = async (event: Event) => { + event.preventDefault() + + const target = prompt.capture() + const submission = createPromptSubmissionState({ + target, + prompt: target.current(), + context: target.context.items().slice(), + }) + const currentPrompt = submission.prompt + const context = submission.context + const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("") + const images = input.imageAttachments().slice() + const mode = input.mode() + + if (text.trim().length === 0 && images.length === 0 && input.commentCount() === 0) { + if (input.working()) void abort() + return + } + + const modelSelection = input.model ?? local.model + const currentModel = modelSelection.current() + const currentAgent = local.agent.current() + const variant = modelSelection.variant.current() + if (!currentModel || !currentAgent) { + showToast({ + title: language.t("prompt.toast.modelAgentRequired.title"), + description: language.t("prompt.toast.modelAgentRequired.description"), + }) + return + } + + input.addToHistory(currentPrompt, mode) + input.resetHistoryNavigation() + + const projectDirectory = sdk().directory + const permissionState = permission.currentServerState() + const isNewSession = !params.id + const shouldAutoAccept = isNewSession && input.autoAccept() + const worktreeSelection = input.newSessionWorktree?.() || "main" + + let sessionDirectory = projectDirectory + let client = sdk().client + + if (isNewSession) { + if (worktreeSelection === "create") { + const createdWorktree = await client.worktree + .create({ directory: projectDirectory }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("prompt.toast.worktreeCreateFailed.title"), + description: errorMessage(err), + }) + return undefined + }) + + if (!createdWorktree?.directory) { + showToast({ + title: language.t("prompt.toast.worktreeCreateFailed.title"), + description: language.t("common.requestFailed"), + }) + return + } + WorktreeState.pending(sdk().scope, createdWorktree.directory) + sessionDirectory = createdWorktree.directory + } + + if (worktreeSelection !== "main" && worktreeSelection !== "create") { + sessionDirectory = worktreeSelection + } + + if (sessionDirectory !== projectDirectory) { + client = sdk().createClient({ + directory: sessionDirectory, + throwOnError: true, + }) + serverSync().child(sessionDirectory) + } + + input.onNewSessionWorktreeReset?.() + } + + let session = input.info() + if (!session && isNewSession) { + const created = await sdk() + .api.session.create({ + agent: currentAgent.name, + model: { id: currentModel.id, providerID: currentModel.provider.id, variant }, + location: { directory: sessionDirectory }, + }) + .then(normalizeSessionInfo) + .catch((err) => { + showToast({ + title: language.t("prompt.toast.sessionCreateFailed.title"), + description: errorMessage(err), + }) + return undefined + }) + if (created) { + seed(sessionDirectory, created) + session = created + await startTransition(() => { + if (!session) return + if (shouldAutoAccept) permissionState.enableAutoAccept(session.id, sessionDirectory) + local.session.promote(sessionDirectory, session.id, { + agent: currentAgent.name, + model: { providerID: currentModel.provider.id, modelID: currentModel.id }, + variant: variant ?? null, + }) + layout.handoff.setTabs(base64Encode(sessionDirectory), session.id) + const draftID = search.draftId + if (draftID) tabs.promoteDraft(draftID, { server: tabs.draft(draftID).server, sessionId: session.id }) + else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) + submission.retarget(prompt.capture({ dir: base64Encode(sessionDirectory), id: session.id })) + }) + } + } + if (!session) { + showToast({ + title: language.t("prompt.toast.promptSendFailed.title"), + description: language.t("prompt.toast.promptSendFailed.description"), + }) + return + } + + const model = { + modelID: currentModel.id, + providerID: currentModel.provider.id, + } + const agent = currentAgent.name + const draft: FollowupDraft = { + sessionID: session.id, + sessionDirectory, + prompt: currentPrompt, + context, + agent, + model, + variant, + } + + const clearInput = () => { + submission.clear() + input.setMode("normal") + input.setPopover(null) + } + + const restoreInput = () => { + const restored = submission.restore() + if (!restored) return false + restored.target.set(restored.prompt, input.promptLength(restored.prompt)) + if (!submission.current(prompt.capture())) return true + input.setMode(mode) + input.setPopover(null) + requestAnimationFrame(() => { + const editor = input.editor() + if (!editor) return + editor.focus() + setCursorPosition(editor, input.promptLength(currentPrompt)) + input.queueScroll() + }) + return true + } + + if (!isNewSession && mode === "normal" && input.shouldQueue?.()) { + input.onQueue?.(draft) + clearContext(submission.target()) + clearInput() + return + } + + input.onSubmit?.() + + if (mode === "shell") { + clearInput() + const eventID = Event.ID.create() + sdk() + .api.session.shell({ + sessionID: session.id, + id: eventID, + command: text, + agent, + model, + }) + .catch((err) => { + showToast({ + title: language.t("prompt.toast.shellSendFailed.title"), + description: errorMessage(err), + }) + restoreInput() + }) + return + } + + if (text.startsWith("/")) { + const [cmdName, ...args] = text.split(" ") + const commandName = cmdName.slice(1) + const customCommand = sync().data.command.find((c) => c.name === commandName) + if (customCommand) { + clearInput() + const messageID = Identifier.ascending("message") + serverSync().session.set("session_status", session.id, { type: "busy" }) + sdk() + .api.session.command({ + sessionID: session.id, + id: messageID, + command: commandName, + arguments: args.join(" "), + agent, + model: { id: model.modelID, providerID: model.providerID, variant }, + files: await Promise.all( + images.map(async (attachment) => ({ + uri: await blobDataUrl(attachment.blob, attachment.mime), + name: attachment.filename, + })), + ), + }) + .catch((err) => { + serverSync().session.set("session_status", session.id, { type: "idle" }) + showToast({ + title: language.t("prompt.toast.commandSendFailed.title"), + description: formatServerError(err, language.t, language.t("common.requestFailed")), + }) + restoreInput() + }) + return + } + } + + const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim()) + const messageID = Identifier.ascending("message") + + const removeOptimisticMessage = () => { + sync().session.optimistic.remove({ + directory: sessionDirectory, + sessionID: session.id, + messageID, + }) + } + + for (const item of commentItems) submission.target().context.remove(item.key) + clearInput() + + const waitForWorktree = async () => { + const worktree = WorktreeState.get(sdk().scope, sessionDirectory) + if (!worktree || worktree.status !== "pending") return true + + if (sessionDirectory === projectDirectory) { + sync().set("session_status", session.id, { type: "busy" }) + } + + const controller = new AbortController() + const cleanup = () => { + if (sessionDirectory === projectDirectory) { + sync().set("session_status", session.id, { type: "idle" }) + } + removeOptimisticMessage() + if (restoreInput()) restoreCommentItems(submission.target(), commentItems) + } + + pending.set(pendingKey(session.id), { abort: controller, cleanup }) + + const abortWait = new Promise>>((resolve) => { + if (controller.signal.aborted) { + resolve({ status: "failed", message: "aborted" }) + return + } + controller.signal.addEventListener( + "abort", + () => { + resolve({ status: "failed", message: "aborted" }) + }, + { once: true }, + ) + }) + + const timeoutMs = 5 * 60 * 1000 + const timer = { id: undefined as number | undefined } + const timeout = new Promise>>((resolve) => { + timer.id = window.setTimeout(() => { + resolve({ + status: "failed", + message: language.t("workspace.error.stillPreparing"), + }) + }, timeoutMs) + }) + + const result = await Promise.race([ + WorktreeState.wait(sdk().scope, sessionDirectory), + abortWait, + timeout, + ]).finally(() => { + if (timer.id === undefined) return + clearTimeout(timer.id) + }) + pending.delete(pendingKey(session.id)) + if (controller.signal.aborted) return false + if (result.status === "failed") throw new Error(result.message) + return true + } + + void sendFollowupDraft({ + api: sdk().api.session, + sync: sync(), + serverSync: serverSync(), + draft, + messageID, + optimisticBusy: sessionDirectory === projectDirectory, + before: waitForWorktree, + }).catch((err) => { + pending.delete(pendingKey(session.id)) + if (sessionDirectory === projectDirectory) { + sync().set("session_status", session.id, { type: "idle" }) + } + showToast({ + title: language.t("prompt.toast.promptSendFailed.title"), + description: errorMessage(err), + }) + removeOptimisticMessage() + if (restoreInput()) restoreCommentItems(submission.target(), commentItems) + }) + } + + return { + abort, + handleSubmit, + } +} diff --git a/packages/app/src/components/prompt-input/transient-state.ts b/packages/app/src/components/prompt-input/transient-state.ts new file mode 100644 index 0000000000000000000000000000000000000000..29f2f182b34235180ac3457c431a310feedea426 --- /dev/null +++ b/packages/app/src/components/prompt-input/transient-state.ts @@ -0,0 +1,46 @@ +import { createComputed, on, type Accessor } from "solid-js" +import { createStore, type SetStoreFunction } from "solid-js/store" +import type { PromptHistoryEntry } from "./history" + +export type PromptInputTransientState = { + popover: "at" | "slash" | null + slashMenu: boolean + slashMenuQuery: string + historyIndex: number + savedPrompt: PromptHistoryEntry | null + placeholder: number + draggingType: "image" | "@mention" | null + mode: "normal" | "shell" + applyingHistory: boolean +} + +function resetPromptInputTransientState(setStore: SetStoreFunction) { + setStore({ + popover: null, + slashMenu: false, + slashMenuQuery: "", + historyIndex: -1, + savedPrompt: null, + draggingType: null, + mode: "normal", + applyingHistory: false, + }) +} + +export function createPromptInputTransientState(identity: Accessor, placeholder: number) { + const [store, setStore] = createStore({ + popover: null, + slashMenu: false, + slashMenuQuery: "", + historyIndex: -1, + savedPrompt: null, + placeholder, + draggingType: null, + mode: "normal", + applyingHistory: false, + }) + + createComputed(on(identity, () => resetPromptInputTransientState(setStore), { defer: true })) + + return [store, setStore] as const +} diff --git a/packages/app/src/components/server/server-row-menu.tsx b/packages/app/src/components/server/server-row-menu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a2920dec7f4c96501f719d984a997ebbb1c887c --- /dev/null +++ b/packages/app/src/components/server/server-row-menu.tsx @@ -0,0 +1,96 @@ +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { type Component, Show } from "solid-js" +import { useServerManagementController } from "@/components/dialog-select-server" +import { useLanguage } from "@/context/language" +import { ServerConnection } from "@/context/server" + +export const ServerRowMenu: Component<{ + server: ServerConnection.Any + controller: ReturnType + onEdit: (server: ServerConnection.Http) => void + open?: boolean + onOpenChange?: (open: boolean) => void +}> = (props) => { + const language = useLanguage() + const key = ServerConnection.key(props.server) + return ( + props.controller.setDefault(key)} + onRemoveDefault={() => props.controller.setDefault(null)} + onRemove={() => props.controller.handleRemove(key)} + open={props.open} + onOpenChange={props.onOpenChange} + /> + ) +} + +export function serverMenuLabels(language: ReturnType) { + return { + more: language.t("common.moreOptions"), + server: language.t("settings.section.server"), + edit: language.t("dialog.server.menu.edit"), + default: language.t("dialog.server.menu.default"), + defaultRemove: language.t("dialog.server.menu.defaultRemove"), + delete: language.t("dialog.server.menu.delete"), + } +} + +export const ServerRowMenuView: Component<{ + server: ServerConnection.Any + labels: ReturnType + canDefault: boolean + isDefault: boolean + onEdit: (server: ServerConnection.Http) => void + onSetDefault: () => void + onRemoveDefault: () => void + onRemove: () => void + open?: boolean + onOpenChange?: (open: boolean) => void +}> = (props) => { + const builtin = () => ServerConnection.builtin(props.server) + const httpServer = () => (props.server.type === "http" ? props.server : undefined) + return ( + + } + aria-label={props.labels.more} + /> + + + + {props.labels.server} + { + const server = httpServer() + if (server) props.onEdit(server) + }} + > + {props.labels.edit} + + + {props.labels.default} + + + {props.labels.defaultRemove} + + + + {props.labels.delete} + + + + + + ) +} diff --git a/packages/app/src/components/server/server-row.tsx b/packages/app/src/components/server/server-row.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8060b0a7087a69cadb4adbc00bc83b1a58278135 --- /dev/null +++ b/packages/app/src/components/server/server-row.tsx @@ -0,0 +1,127 @@ +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { createResizeObserver } from "@solid-primitives/resize-observer" +import { + children, + createEffect, + createMemo, + createSignal, + type JSXElement, + onMount, + type ParentProps, + Show, +} from "solid-js" +import { useLanguage } from "@/context/language" +import { type ServerConnection, serverName } from "@/context/server" +import type { ServerHealth } from "@/utils/server-health" + +interface ServerRowProps extends ParentProps { + conn: ServerConnection.Any + status?: ServerHealth + class?: string + nameClass?: string + versionClass?: string + dimmed?: boolean + badge?: JSXElement + showCredentials?: boolean +} + +export function ServerRow(props: ServerRowProps) { + const language = useLanguage() + const [truncated, setTruncated] = createSignal(false) + let nameRef: HTMLSpanElement | undefined + let versionRef: HTMLSpanElement | undefined + const name = createMemo(() => serverName(props.conn)) + + const check = () => { + const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false + const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false + setTruncated(nameTruncated || versionTruncated) + } + + createEffect(() => { + name() + props.conn.http.url + props.status?.version + queueMicrotask(check) + }) + + onMount(() => { + if (typeof ResizeObserver !== "function") return + createResizeObserver([nameRef, versionRef], check) + check() + }) + + const tooltipValue = () => ( + + {serverName(props.conn, true)} + + v{props.status?.version} + + + ) + + const badge = children(() => props.badge) + + return ( + +
+
+
+ + {name()} + + + + v{props.status?.version} + + + } + > + {(badge) => badge()} + +
+ + {(conn) => ( +
+ + {conn().http.username ? ( + {conn().http.username} + ) : ( + {language.t("server.row.noUsername")} + )} + + {conn().http.password && ••••••••} +
+ )} +
+
+ {props.children} +
+
+ ) +} + +export function ServerHealthIndicator(props: { health?: ServerHealth }) { + return ( +
+ ) +} diff --git a/packages/app/src/components/session/open-in-app-v2.tsx b/packages/app/src/components/session/open-in-app-v2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b5291dcc32cd0270fa9fc65f77406358508afc78 --- /dev/null +++ b/packages/app/src/components/session/open-in-app-v2.tsx @@ -0,0 +1,101 @@ +import { For, Show } from "solid-js" +import { AppIcon } from "@opencode-ai/ui/app-icon" +import { Icon } from "@opencode-ai/ui/icon" +import { Spinner } from "@opencode-ai/ui/spinner" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { SplitButtonV2, SplitButtonV2Action, SplitButtonV2MenuTrigger } from "@opencode-ai/ui/v2/split-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { useLanguage } from "@/context/language" +import { type OpenApp, useOpenInApp } from "@/components/session/open-in-app" + +export function OpenInAppV2(props: { directory: () => string }) { + const language = useLanguage() + const state = useOpenInApp(props) + + return ( + + event.stopPropagation()}> + + event.stopPropagation()} + onClick={(event) => { + event.stopPropagation() + if (state.opening()) return + state.openDir(state.current().id) + }} + disabled={state.opening()} + aria-label={language.t("session.header.open.ariaLabel", { app: state.current().label })} + > + } + > + + + + + state.setMenu("open", open)} + > + event.stopPropagation()} + > + + + + + + {language.t("session.header.openIn")} + { + state.selectApp(value as OpenApp) + }} + > + + {(option) => ( + { + state.selectApp(option.id) + state.setMenu("open", false) + state.openDir(option.id) + }} + > + + {option.label} + + )} + + + + + { + state.setMenu("open", false) + state.copyPath() + }} + > + + {language.t("session.header.open.copyPath")} + + + + + + + ) +} diff --git a/packages/app/src/components/session/session-context-metrics.test.ts b/packages/app/src/components/session/session-context-metrics.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..37cc0d5b5d2e372f54a8f41f4137091272521b30 --- /dev/null +++ b/packages/app/src/components/session/session-context-metrics.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, test } from "bun:test" +import type { Message } from "@opencode-ai/sdk/v2/client" +import { getSessionContext } from "./session-context-metrics" + +const assistant = ( + id: string, + tokens: { input: number; output: number; reasoning: number; read: number; write: number }, + cost: number, + providerID = "openai", + modelID = "gpt-4.1", +) => { + return { + id, + role: "assistant", + providerID, + modelID, + cost, + tokens: { + input: tokens.input, + output: tokens.output, + reasoning: tokens.reasoning, + cache: { + read: tokens.read, + write: tokens.write, + }, + }, + time: { created: 1 }, + } as unknown as Message +} + +const user = (id: string) => { + return { + id, + role: "user", + cost: 0, + time: { created: 1 }, + } as unknown as Message +} + +describe("getSessionContext", () => { + test("computes token totals and usage from latest assistant with tokens", () => { + const messages = [ + user("u1"), + assistant("a1", { input: 600, output: 200, reasoning: 100, read: 50, write: 50 }, 0.5), + assistant("a2", { input: 300, output: 100, reasoning: 50, read: 25, write: 25 }, 1.25), + ] + const providers = [ + { + id: "openai", + name: "OpenAI", + models: { + "gpt-4.1": { + name: "GPT-4.1", + limit: { context: 1000 }, + }, + }, + }, + ] + + const ctx = getSessionContext(messages, providers) + + expect(ctx?.message.id).toBe("a2") + expect(ctx?.total).toBe(500) + expect(ctx?.input).toBe(300) + expect(ctx?.usage).toBe(50) + expect(ctx?.providerLabel).toBe("OpenAI") + expect(ctx?.modelLabel).toBe("GPT-4.1") + }) + + test("preserves fallback labels and null usage when model metadata is missing", () => { + const messages = [assistant("a1", { input: 40, output: 10, reasoning: 0, read: 0, write: 0 }, 0.1, "p-1", "m-1")] + const providers = [{ id: "p-1", models: {} }] + + const ctx = getSessionContext(messages, providers) + + expect(ctx?.providerLabel).toBe("p-1") + expect(ctx?.modelLabel).toBe("m-1") + expect(ctx?.limit).toBeUndefined() + expect(ctx?.usage).toBeNull() + }) + + test("recomputes when message array is mutated in place", () => { + const messages = [assistant("a1", { input: 10, output: 10, reasoning: 10, read: 10, write: 10 }, 0.25)] + const providers = [{ id: "openai", models: {} }] + + const one = getSessionContext(messages, providers) + messages.push(assistant("a2", { input: 100, output: 20, reasoning: 0, read: 0, write: 0 }, 0.75)) + const two = getSessionContext(messages, providers) + + expect(one?.message.id).toBe("a1") + expect(two?.message.id).toBe("a2") + }) + + test("returns undefined when inputs are undefined", () => { + const ctx = getSessionContext(undefined, undefined) + + expect(ctx).toBeUndefined() + }) +}) diff --git a/packages/app/src/components/settings-v2/dialog-server-v2.tsx b/packages/app/src/components/settings-v2/dialog-server-v2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7113cdcf0a0aa4557a8a6df5055436eba8d06f66 --- /dev/null +++ b/packages/app/src/components/settings-v2/dialog-server-v2.tsx @@ -0,0 +1,134 @@ +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2" +import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { type Component, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js" +import { useLanguage } from "@/context/language" +import { type ServerConnection } from "@/context/server" +import { useServerManagementController } from "../dialog-select-server" +import "./settings-v2.css" + +export const DialogServerV2: Component<{ + mode: "add" | "edit" + server?: ServerConnection.Http +}> = (props) => { + const dialog = useDialog() + const language = useLanguage() + const controller = useServerManagementController({ + onSelect: () => dialog.close(), + navigateOnAdd: false, + }) + const [opened, setOpened] = createSignal(false) + + onMount(() => { + if (props.mode === "add") controller.startAdd() + if (props.mode === "edit" && props.server) controller.startEdit(props.server) + setOpened(true) + }) + + onCleanup(() => { + controller.resetForm() + }) + + createEffect(() => { + if (!opened()) return + if (controller.isFormMode()) return + dialog.close() + }) + + const keyDown = (event: KeyboardEvent) => { + if (event.key !== "Enter" || event.isComposing) return + event.preventDefault() + controller.submitForm() + } + + const title = () => + props.mode === "add" ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title") + + const submitLabel = () => { + if (controller.formBusy()) return language.t("dialog.server.add.checking") + if (props.mode === "add") return language.t("dialog.server.add.button") + return language.t("common.save") + } + + return ( + + + {title()} + + + +
+
+ + controller.handleFormChange()(event.currentTarget.value)} + onKeyDown={keyDown} + /> + + {controller.formError()} + +
+
+ + controller.handleFormNameChange()(event.currentTarget.value)} + onKeyDown={keyDown} + /> +
+
+
+ + controller.handleFormUsernameChange()(event.currentTarget.value)} + onKeyDown={keyDown} + /> +
+
+ + controller.handleFormPasswordChange()(event.currentTarget.value)} + onKeyDown={keyDown} + /> +
+
+
+
+ + dialog.close()}> + {language.t("common.cancel")} + + + {submitLabel()} + + +
+ ) +} diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4116f4a6208c578121dfb1468433992521f7c199 --- /dev/null +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -0,0 +1,114 @@ +import { Component, createMemo, createSignal, startTransition } from "solid-js" +import { Dialog } from "@opencode-ai/ui/v2/dialog-v2" +import { TabsV2 } from "@opencode-ai/ui/v2/tabs-v2" +import { Icon } from "@opencode-ai/ui/icon" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { SettingsGeneralV2 } from "./general" +import { SettingsKeybinds } from "../settings-keybinds" +import { SettingsProvidersV2 } from "./providers" +import { SettingsModelsV2 } from "./models" +import "./settings-v2.css" +import { SettingsServersV2 } from "./servers" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLayout } from "@/context/layout" +import { useTabs } from "@/context/tabs" +import { useServerSync } from "@/context/server-sync" + +export const DialogSettings: Component<{ + sessionID?: string + defaultValue?: string +}> = (props) => { + const language = useLanguage() + const platform = usePlatform() + const dialog = useDialog() + const layout = useLayout() + const tabs = useTabs() + const serverSync = useServerSync() + const [tab, setTab] = createSignal(props.defaultValue ?? "general") + const directory = createMemo(() => { + const route = layout.route() + if (route.type === "dir-new-sesssion") return route.dir + if (route.type === "draft") { + const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID) + return draft?.type === "draft" ? draft.directory : undefined + } + if (route.type === "session") return serverSync().session.get(route.sessionId)?.directory + return undefined + }) + + const showProviders = () => { + void dialog.show(() => ) + } + + return ( + + void startTransition(() => setTab(value))} + class="settings-v2" + > + +
+
+
+
+ {language.t("settings.section.desktop")} +
+ + + {language.t("settings.tab.general")} + + + + {language.t("settings.tab.shortcuts")} + +
+
+ +
+ {language.t("settings.section.server")} +
+ + + {language.t("status.popover.tab.servers")} + + + + {language.t("settings.providers.title")} + + + + {language.t("settings.models.title")} + +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + +
+
+ ) +} diff --git a/packages/app/src/components/settings-v2/general-controller-behavior.ts b/packages/app/src/components/settings-v2/general-controller-behavior.ts new file mode 100644 index 0000000000000000000000000000000000000000..64bd4c118755ba22e6d7393aaaaef5573becd85a --- /dev/null +++ b/packages/app/src/components/settings-v2/general-controller-behavior.ts @@ -0,0 +1,70 @@ +import { onCleanup } from "solid-js" + +export type ShellOption = { + path: string + name: string + acceptable: boolean +} + +export type ShellSelectOption = { + id: string + value: string + name: string + terminalOnly: boolean +} + +export function createShellOptions(input: { shells: ShellOption[]; current: string | undefined }) { + const counts = input.shells.reduce((result, shell) => { + result.set(shell.name, (result.get(shell.name) ?? 0) + 1) + return result + }, new Map()) + const options: ShellSelectOption[] = [ + { id: "auto", value: "", name: "", terminalOnly: false }, + ...input.shells.map((shell) => { + const ambiguous = (counts.get(shell.name) ?? 0) > 1 + const name = ambiguous ? shell.path : shell.name + return { + id: shell.path, + value: ambiguous ? shell.path : shell.name, + name, + terminalOnly: !shell.acceptable, + } + }), + ] + if (input.current && !options.some((option) => option.value === input.current)) { + options.push({ id: input.current, value: input.current, name: input.current, terminalOnly: false }) + } + return options +} + +export function createSoundPreviewController(player: (id: string | undefined) => Promise<(() => void) | undefined>) { + let cleanup: (() => void) | undefined + let timeout: ReturnType | undefined + let run = 0 + + const stop = () => { + run += 1 + cleanup?.() + clearTimeout(timeout) + cleanup = undefined + timeout = undefined + } + const play = (id: string | undefined) => { + stop() + if (!id) return + const current = ++run + timeout = setTimeout(() => { + timeout = undefined + void player(id).then((next) => { + if (run === current) { + cleanup = next + return + } + next?.() + }) + }, 100) + } + + onCleanup(stop) + return { play, stop } +} diff --git a/packages/app/src/components/settings-v2/general-controllers.test.ts b/packages/app/src/components/settings-v2/general-controllers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..09c8d21bd538236404dc903b5424face5bd6b499 --- /dev/null +++ b/packages/app/src/components/settings-v2/general-controllers.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test, vi } from "bun:test" +import { createRoot } from "solid-js" +import { createShellOptions, createSoundPreviewController } from "./general-controller-behavior" + +describe("settings v2 controllers", () => { + test("normalizes shell names and preserves an unavailable configured shell", () => { + expect( + createShellOptions({ + shells: [ + { path: "/bin/bash", name: "bash", acceptable: true }, + { path: "/opt/bash", name: "bash", acceptable: false }, + { path: "/bin/zsh", name: "zsh", acceptable: true }, + ], + current: "fish", + }), + ).toEqual([ + { id: "auto", value: "", name: "", terminalOnly: false }, + { id: "/bin/bash", value: "/bin/bash", name: "/bin/bash", terminalOnly: false }, + { id: "/opt/bash", value: "/opt/bash", name: "/opt/bash", terminalOnly: true }, + { id: "/bin/zsh", value: "zsh", name: "zsh", terminalOnly: false }, + { id: "fish", value: "fish", name: "fish", terminalOnly: false }, + ]) + }) + + test("debounces previews and stops owned audio on disposal", async () => { + vi.useFakeTimers() + try { + const played: string[] = [] + const stopped: string[] = [] + const owned = createRoot((dispose) => ({ + dispose, + preview: createSoundPreviewController(async (id) => { + played.push(id ?? "") + return () => stopped.push(id ?? "") + }), + })) + + owned.preview.play("first") + vi.advanceTimersByTime(99) + expect(played).toEqual([]) + + owned.preview.play("second") + vi.advanceTimersByTime(100) + await Promise.resolve() + expect(played).toEqual(["second"]) + + owned.dispose() + expect(stopped).toEqual(["second"]) + } finally { + vi.useRealTimers() + } + }) +}) diff --git a/packages/app/src/components/settings-v2/general-controllers.ts b/packages/app/src/components/settings-v2/general-controllers.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae77fa332f22c14f96703507dfd26ee8850f4eec --- /dev/null +++ b/packages/app/src/components/settings-v2/general-controllers.ts @@ -0,0 +1,173 @@ +import { createMemo, createResource, onMount, type Accessor } from "solid-js" +import type { ColorScheme } from "@opencode-ai/ui/theme/context" +import { useTheme } from "@opencode-ai/ui/theme/context" +import { usePermission } from "@/context/permission" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" +import { + monoDefault, + monoFontFamily, + monoInput, + sansDefault, + sansFontFamily, + sansInput, + terminalDefault, + terminalFontFamily, + terminalInput, + useSettings, +} from "@/context/settings" +import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" +import { createSoundPreviewController, type ShellOption } from "./general-controller-behavior" + +export { createShellOptions, createSoundPreviewController } from "./general-controller-behavior" +export type { ShellOption, ShellSelectOption } from "./general-controller-behavior" + +export function createPermissionScopeController(sessionID: Accessor) { + const permission = usePermission() + const serverSync = useServerSync() + const directory = createMemo(() => { + const id = sessionID() + if (!id) return undefined + return serverSync().session.lineage.peek(id)?.session.directory + }) + + return { + accepting: createMemo(() => { + const id = sessionID() + const dir = directory() + if (!id || !dir) return false + return permission.isAutoAccepting(id, dir) + }), + enabled: createMemo(() => !!directory()), + set: (checked: boolean) => { + const id = sessionID() + const dir = directory() + if (!id || !dir) return + if (checked) return permission.enableAutoAccept(id, dir) + permission.disableAutoAccept(id, dir) + }, + } +} + +export function createShellSettingsController() { + const serverSdk = useServerSDK() + const serverSync = useServerSync() + const [shells] = createResource( + async () => { + const sdk = serverSdk() + if ((await sdk.protocol) === "v1") return (await sdk.client.pty.shells()).data ?? [] + return [] as ShellOption[] + }, + { initialValue: [] as ShellOption[] }, + ) + const current = createMemo(() => serverSync().data.config.shell ?? "") + + return { + shells: () => shells.latest, + current, + select: (value: string) => { + if (value === current()) return + void serverSync().updateConfig({ shell: value }) + }, + } +} + +export function createAppearanceSettingsController() { + const settings = useSettings() + const theme = useTheme() + const themes = createMemo(() => theme.ids().map((id) => ({ id, name: theme.name(id) }))) + + onMount(() => void theme.loadThemes()) + + return { + scheme: { + current: theme.colorScheme, + select: (value: ColorScheme) => theme.setColorScheme(value), + }, + theme: { + options: themes, + current: createMemo(() => themes().find((option) => option.id === theme.themeId())), + select: (option: { id: string } | null) => option && theme.setTheme(option.id), + }, + fonts: { + ui: createMemo(() => ({ + value: sansInput(settings.appearance.uiFont()), + family: sansFontFamily(settings.appearance.uiFont()), + placeholder: sansDefault, + })), + code: createMemo(() => ({ + value: monoInput(settings.appearance.font()), + family: monoFontFamily(settings.appearance.font()), + placeholder: monoDefault, + })), + terminal: createMemo(() => ({ + value: terminalInput(settings.appearance.terminalFont()), + family: terminalFontFamily(settings.appearance.terminalFont()), + placeholder: terminalDefault, + })), + setUI: (value: string) => settings.appearance.setUIFont(value), + setCode: (value: string) => settings.appearance.setFont(value), + setTerminal: (value: string) => settings.appearance.setTerminalFont(value), + }, + } +} + +const noneSound = { id: "none", label: "sound.option.none" } as const +export const soundOptions = [noneSound, ...SOUND_OPTIONS] +export type SoundSelectOption = (typeof soundOptions)[number] + +export function createSoundSettingsController() { + const settings = useSettings() + const preview = createSoundPreviewController(playSoundById) + const channel = ( + enabled: Accessor, + current: Accessor, + setEnabled: (value: boolean) => void, + set: (id: string) => void, + ) => ({ + current: createMemo(() => + enabled() ? (soundOptions.find((option) => option.id === current()) ?? noneSound) : noneSound, + ), + highlight: (option: SoundSelectOption | undefined) => { + if (!option) return + preview.play(option.id === "none" ? undefined : option.id) + }, + select: (option: SoundSelectOption | null) => { + if (!option) return + if (option.id === "none") { + setEnabled(false) + preview.stop() + return + } + setEnabled(true) + set(option.id) + preview.play(option.id) + }, + }) + + return { + agent: channel( + settings.sounds.agentEnabled, + settings.sounds.agent, + (value) => settings.sounds.setAgentEnabled(value), + (id) => settings.sounds.setAgent(id), + ), + permissions: channel( + settings.sounds.permissionsEnabled, + settings.sounds.permissions, + (value) => settings.sounds.setPermissionsEnabled(value), + (id) => settings.sounds.setPermissions(id), + ), + errors: channel( + settings.sounds.errorsEnabled, + settings.sounds.errors, + (value) => settings.sounds.setErrorsEnabled(value), + (id) => settings.sounds.setErrors(id), + ), + } +} + +export type PermissionScopeController = ReturnType +export type ShellSettingsController = ReturnType +export type AppearanceSettingsController = ReturnType +export type SoundSettingsController = ReturnType diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b41da107ed6e92318c042c625f8b2756839eea2e --- /dev/null +++ b/packages/app/src/components/settings-v2/general.tsx @@ -0,0 +1,571 @@ +import { Component, Show, createMemo, createResource } from "solid-js" +import { createMediaQuery } from "@solid-primitives/media" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { SelectV2 } from "@opencode-ai/ui/v2/select-v2" +import { Switch } from "@opencode-ai/ui/v2/switch-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { useUpdaterAction } from "../updater-action" +import { useSettings } from "@/context/settings" +import { ExternalLink } from "../external-link" +import { SettingsListV2 } from "./parts/list" +import { SettingsRowV2 } from "./parts/row" +import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition" +import { + createAppearanceSettingsController, + createPermissionScopeController, + createShellOptions, + createShellSettingsController, + createSoundSettingsController, + soundOptions, + type AppearanceSettingsController, + type PermissionScopeController, + type ShellSettingsController, + type SoundSettingsController, +} from "./general-controllers" +import "./settings-v2.css" + +const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"] +const fontSettings = { + ui: { + action: "settings-ui-font", + title: "settings.general.row.uiFont.title", + description: "settings.general.row.uiFont.description", + font: "ui", + input: "setUI", + }, + code: { + action: "settings-code-font", + title: "settings.general.row.font.title", + description: "settings.general.row.font.description", + font: "code", + input: "setCode", + }, + terminal: { + action: "settings-terminal-font", + title: "settings.general.row.terminalFont.title", + description: "settings.general.row.terminalFont.description", + font: "terminal", + input: "setTerminal", + }, +} as const +const soundSettings = { + agent: { + action: "settings-sounds-agent", + title: "settings.general.sounds.agent.title", + description: "settings.general.sounds.agent.description", + }, + permissions: { + action: "settings-sounds-permissions", + title: "settings.general.sounds.permissions.title", + description: "settings.general.sounds.permissions.description", + }, + errors: { + action: "settings-sounds-errors", + title: "settings.general.sounds.errors.title", + description: "settings.general.sounds.errors.description", + }, +} as const + +const PermissionScopeSetting: Component<{ controller: PermissionScopeController }> = (props) => { + const language = useLanguage() + return ( + +
+ +
+
+ ) +} + +const ShellSetting: Component<{ controller: ShellSettingsController }> = (props) => { + const language = useLanguage() + const options = createMemo(() => + createShellOptions({ + shells: props.controller.shells(), + current: props.controller.current(), + }), + ) + return ( + + option.value === props.controller.current()) ?? options()[0]} + placement="bottom-end" + gutter={6} + value={(option) => option.id} + label={(option) => { + if (option.id === "auto") return language.t("settings.general.row.shell.autoDefault") + if (!option.terminalOnly) return option.name + return `${option.name} (${language.t("settings.general.row.shell.terminalOnly")})` + }} + onSelect={(option) => option && props.controller.select(option.value)} + /> + + ) +} + +const AppearanceSection: Component<{ controller: AppearanceSettingsController }> = (props) => { + const language = useLanguage() + return ( +
+

{language.t("settings.general.section.appearance")}

+ + + option === props.controller.scheme.current())} + placement="bottom-end" + gutter={6} + label={(option) => { + if (option === "system") return language.t("theme.scheme.system") + if (option === "light") return language.t("theme.scheme.light") + return language.t("theme.scheme.dark") + }} + onSelect={(option) => option && props.controller.scheme.select(option)} + /> + + + + {language.t("settings.general.row.theme.description")}{" "} + + {language.t("common.learnMore")} + + + } + > + option.id} + label={(option) => option.name} + onSelect={props.controller.theme.select} + /> + + + + + + +
+ ) +} + +const FontSetting: Component<{ + kind: "ui" | "code" | "terminal" + fonts: AppearanceSettingsController["fonts"] +}> = (props) => { + const language = useLanguage() + const config = () => fontSettings[props.kind] + return ( + +
+ props.fonts[config().input](event.currentTarget.value)} + placeholder={props.fonts[config().font]().placeholder} + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + aria-label={language.t(config().title)} + style={{ "font-family": props.fonts[config().font]().family }} + /> +
+
+ ) +} + +const SoundsSection: Component<{ controller: SoundSettingsController }> = (props) => { + const language = useLanguage() + return ( +
+

{language.t("settings.general.section.sounds")}

+ + + + + +
+ ) +} + +const SoundSetting: Component<{ + kind: "agent" | "permissions" | "errors" + channel: SoundSettingsController["agent"] +}> = (props) => { + const language = useLanguage() + const config = () => soundSettings[props.kind] + return ( + + option.id} + label={(option) => language.t(option.label)} + onHighlight={props.channel.highlight} + onSelect={props.channel.select} + placement="bottom-end" + gutter={6} + /> + + ) +} + +const LanguageSetting = () => { + const language = useLanguage() + const options = createMemo(() => + language.locales.map((locale) => ({ + value: locale, + label: language.label(locale), + })), + ) + return ( + + option.value === language.locale())} + value={(option) => option.value} + label={(option) => option.label} + onSelect={(option) => option && language.setLocale(option.value)} + /> + + ) +} + +export const SettingsGeneralV2: Component<{ + sessionID?: string +}> = (props) => { + const language = useLanguage() + const platform = usePlatform() + const dialog = useDialog() + const settings = useSettings() + const mobile = createMediaQuery("(max-width: 767px)") + const updater = useUpdaterAction() + const permissionScope = createPermissionScopeController(() => props.sessionID) + const shell = createShellSettingsController() + const appearance = createAppearanceSettingsController() + const sounds = createSoundSettingsController() + const desktop = createMemo(() => platform.platform === "desktop") + + const [pinchZoom, { mutate: setPinchZoom }] = createResource( + () => desktop() && "getPinchZoomEnabled" in platform, + () => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false), + { initialValue: false }, + ) + + const onPinchZoomChange = (checked: boolean) => { + setPinchZoom(checked) + const update = platform.setPinchZoomEnabled?.(checked) + if (!update) return + void update.catch(() => setPinchZoom(!checked)) + } + + const InterfaceSection = () => ( + { + settings.general.setNewLayoutDesigns(checked) + if (checked) return + void import("@/components/dialog-settings").then((module) => { + void dialog.show(() => ) + }) + }} + /> + ) + + const InterfaceNoticeSection = () => ( + settings.general.dismissNewInterfaceNotice()} + /> + ) + + const GeneralSection = () => ( +
+ + + + + + + + +
+ settings.general.setShowReasoningSummaries(checked)} + /> +
+
+ + +
+ settings.general.setShellToolPartsExpanded(checked)} + /> +
+
+ + +
+ settings.general.setEditToolPartsExpanded(checked)} + /> +
+
+ + + +
+ settings.general.setMobileTitlebarPosition(checked ? "bottom" : "top")} + /> +
+
+
+
+
+ ) + + const AdvancedSection = () => ( +
+

{language.t("settings.general.section.advanced")}

+ + + +
+ settings.general.setShowFileTree(checked)} + /> +
+
+ + +
+ settings.general.setShowSearch(checked)} + /> +
+
+ + +
+ settings.general.setShowStatus(checked)} + /> +
+
+ + +
+ settings.general.setShowCustomAgents(checked)} + /> +
+
+
+
+ ) + + const NotificationsSection = () => ( +
+

{language.t("settings.general.section.notifications")}

+ + + +
+ settings.notifications.setAgent(checked)} + /> +
+
+ + +
+ settings.notifications.setPermissions(checked)} + /> +
+
+ + +
+ settings.notifications.setErrors(checked)} + /> +
+
+
+
+ ) + + const UpdatesSection = () => ( +
+

{language.t("settings.general.section.updates")}

+ + + +
+ settings.general.setReleaseNotes(checked)} + /> +
+
+ + + updater.run()}> + {language.t(updater.action().label)} + + +
+
+ ) + + // We can probably remove this, right? + const DisplaySection = () => ( + +
+

{language.t("settings.general.section.display")}

+ + + +
+ +
+
+
+
+
+ ) + + return ( + <> +
+

{language.t("settings.tab.general")}

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + ) +} diff --git a/packages/app/src/components/settings-v2/index.tsx b/packages/app/src/components/settings-v2/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8ff5884379876abdb0f49feeef3dd3b58765de08 --- /dev/null +++ b/packages/app/src/components/settings-v2/index.tsx @@ -0,0 +1 @@ +export { DialogSettings } from "./dialog-settings-v2" diff --git a/packages/app/src/components/settings-v2/interface-transition.stories.tsx b/packages/app/src/components/settings-v2/interface-transition.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a48de957254c0efc0c5f04e770b02e7083ea23a --- /dev/null +++ b/packages/app/src/components/settings-v2/interface-transition.stories.tsx @@ -0,0 +1,76 @@ +// @ts-nocheck +import { Show } from "solid-js" +import { createStore } from "solid-js/store" +import { LayoutRetirementNotice, LayoutTransitionToggle } from "./interface-transition" + +const copy = { + title: "New layout", + badge: "New", + description: "Use the new tabs and home layout. Switch between layouts for a limited time.", + noticeTitle: "You're now using new layout", + noticeDescription: "The previous layout is no longer available", + dismiss: "Dismiss", +} + +function Frame(props) { + return
{props.children}
+} + +function ToggleExample(props) { + const [state, setState] = createStore({ checked: props.checked }) + return ( + + setState("checked", checked)} + /> + + ) +} + +function NoticeExample() { + const [state, setState] = createStore({ dismissed: false }) + return ( + + Notice dismissed}> + setState("dismissed", true)} + /> + + + ) +} + +export default { + title: "App/Settings/Layout transition", + id: "app-settings-layout-transition", + component: LayoutTransitionToggle, +} + +export const NewLayoutEnabled = { + render: () => , +} + +export const PreviousLayoutEnabled = { + render: () => , +} + +export const PreviousLayoutRetired = { + render: () => , +} + +export const AllStates = { + render: () => ( +
+ + + +
+ ), +} diff --git a/packages/app/src/components/settings-v2/interface-transition.tsx b/packages/app/src/components/settings-v2/interface-transition.tsx new file mode 100644 index 0000000000000000000000000000000000000000..53bd980a540107a99f018a54c71897828da81c2d --- /dev/null +++ b/packages/app/src/components/settings-v2/interface-transition.tsx @@ -0,0 +1,54 @@ +import { Tag } from "@opencode-ai/ui/v2/badge-v2" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Switch } from "@opencode-ai/ui/v2/switch-v2" +import { SettingsListV2 } from "./parts/list" +import { SettingsRowV2 } from "./parts/row" + +export function LayoutTransitionToggle(props: { + title: string + badge: string + description: string + checked: boolean + onChange: (checked: boolean) => void +}) { + return ( +
+
+ + + {props.title} + {props.badge} + + } + description={props.description} + > +
+ +
+
+
+
+
+ ) +} + +export function LayoutRetirementNotice(props: { + title: string + description: string + dismiss: string + onDismiss: () => void +}) { + return ( +
+ + + + {props.dismiss} + + + +
+ ) +} diff --git a/packages/app/src/components/settings-v2/models.tsx b/packages/app/src/components/settings-v2/models.tsx new file mode 100644 index 0000000000000000000000000000000000000000..482e8cbffbd4beb1dc2fcecbdc6751703cdef2d1 --- /dev/null +++ b/packages/app/src/components/settings-v2/models.tsx @@ -0,0 +1,187 @@ +import { useFilteredList } from "@opencode-ai/ui/hooks" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { Switch } from "@opencode-ai/ui/v2/switch-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { type Component, For, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { useLanguage } from "@/context/language" +import { useModels } from "@/context/models" +import { useServerSDK } from "@/context/server-sdk" +import { popularProviders } from "@/hooks/use-providers" +import { Persist, persisted } from "@/utils/persist" +import { SettingsListV2 } from "./parts/list" +import { SettingsRowV2 } from "./parts/row" +import "./settings-v2.css" + +type ModelItem = ReturnType["list"]>[number] + +const PROVIDER_ICON_SIZE = 16 + +export const SettingsModelsV2: Component = () => { + const language = useLanguage() + const models = useModels() + const serverSdk = useServerSDK() + const [store, setStore] = persisted( + Persist.serverGlobal(serverSdk().scope, "settings-v2.models.providers"), + createStore({ collapsed: {} as Record }), + ) + + const list = useFilteredList({ + items: (_filter) => models.list(), + key: (x) => `${x.provider.id}:${x.id}`, + filterKeys: ["provider.name", "name", "id"], + sortBy: (a, b) => a.name.localeCompare(b.name), + groupBy: (x) => x.provider.id, + sortGroupsBy: (a, b) => { + const aIndex = popularProviders.indexOf(a.category) + const bIndex = popularProviders.indexOf(b.category) + const aPopular = aIndex >= 0 + const bPopular = bIndex >= 0 + + if (aPopular && !bPopular) return -1 + if (!aPopular && bPopular) return 1 + if (aPopular && bPopular) return aIndex - bIndex + + const aName = a.items[0].provider.name + const bName = b.items[0].provider.name + return aName.localeCompare(bName) + }, + }) + + return ( + <> +
+

{language.t("settings.models.title")}

+ +
+ +
+ + {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+ } + > + 0} + fallback={ +
+ {language.t("dialog.model.empty")} + + "{list.filter()}" + +
+ } + > + + {(group) => { + const searching = () => list.filter().length > 0 + const expanded = () => searching() || !store.collapsed[group.category] + + return ( +
+

+ +

+ + + + {(item) => { + const key = { providerID: item.provider.id, modelID: item.id } + return ( + +
+ { + models.setVisibility(key, checked) + }} + hideLabel + > + {item.name} + +
+
+ ) + }} +
+
+
+
+ ) + }} +
+
+ +
+ + ) +} diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx new file mode 100644 index 0000000000000000000000000000000000000000..acd73ddbe54400bf6bc699405378f2f49020ea7c --- /dev/null +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -0,0 +1,267 @@ +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Tag } from "@opencode-ai/ui/v2/badge-v2" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { showToast } from "@/utils/toast" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { createMemo, type Accessor, type Component, For, Show } from "solid-js" +import { useLanguage } from "@/context/language" +import { useServerProtocol, useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" +import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider" +import { DialogCustomProvider } from "../dialog-custom-provider" +import { SettingsListV2 } from "./parts/list" +import "./settings-v2.css" + +type ProviderSource = "env" | "api" | "config" | "custom" +type ProviderItem = ReturnType["connected"]>[number] + +const PROVIDER_NOTES = [ + { match: (id: string) => id === "opencode", key: "dialog.provider.opencode.note" }, + { match: (id: string) => id === "opencode-go", key: "dialog.provider.opencodeGo.tagline" }, + { match: (id: string) => id === "anthropic", key: "dialog.provider.anthropic.note" }, + { match: (id: string) => id.startsWith("github-copilot"), key: "dialog.provider.copilot.note" }, + { match: (id: string) => id === "openai", key: "dialog.provider.openai.note" }, + { match: (id: string) => id === "google", key: "dialog.provider.google.note" }, + { match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" }, + { match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" }, +] as const + +const PROVIDER_ICON_SIZE = 16 + +export const SettingsProvidersV2: Component<{ + directory: Accessor + onBack?: () => void +}> = (props) => { + const dialog = useDialog() + const language = useLanguage() + const serverSdk = useServerSDK() + const protocol = useServerProtocol() + const serverSync = useServerSync() + const providers = useProviders(props.directory) + const providerConnect = useProviderConnectController({ onBack: props.onBack }) + + const connect = (provider?: string) => { + providerConnect.select(provider) + void dialog.show(() => ) + } + + const connected = createMemo(() => { + return providers + .connected() + .filter((p) => p.id !== "opencode" || Object.values(p.models).find((m) => m.cost?.input)) + }) + + const popular = createMemo(() => { + const connectedIDs = new Set(connected().map((p) => p.id)) + const items = providers + .popular() + .filter((p) => !connectedIDs.has(p.id)) + .slice() + items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)) + return items + }) + + const source = (item: ProviderItem): ProviderSource | undefined => { + if (!("source" in item)) return + const value = item.source + if (value === "env" || value === "api" || value === "config" || value === "custom") return value + return + } + + const type = (item: ProviderItem) => { + const current = source(item) + if (current === "env") return language.t("settings.providers.tag.environment") + if (current === "api") return language.t("provider.connect.method.apiKey") + if (current === "config") { + if (isConfigCustom(item.id)) return language.t("settings.providers.tag.custom") + return language.t("settings.providers.tag.config") + } + if (current === "custom") return language.t("settings.providers.tag.custom") + return language.t("settings.providers.tag.other") + } + + const canDisconnect = (item: ProviderItem) => + source(item) !== "env" && (protocol() === "v1" || !isConfigCustom(item.id)) + + const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key + + const isConfigCustom = (providerID: string) => { + const provider = serverSync().data.config.provider?.[providerID] + if (!provider) return false + if (provider.npm !== "@ai-sdk/openai-compatible") return false + if (!provider.models || Object.keys(provider.models).length === 0) return false + return true + } + + const disableProvider = async (providerID: string, name: string) => { + if (protocol() !== "v1") return + const before = serverSync().data.config.disabled_providers ?? [] + const next = before.includes(providerID) ? before : [...before, providerID] + serverSync().set("config", "disabled_providers", next) + + await serverSync() + .updateConfig({ disabled_providers: next }) + .then(() => { + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }), + description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }), + }) + }) + .catch((err: unknown) => { + serverSync().set("config", "disabled_providers", before) + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + } + + const disconnect = async (providerID: string, name: string) => { + if (isConfigCustom(providerID)) { + await serverSdk() + .client.auth.remove({ providerID }) + .catch(() => undefined) + await disableProvider(providerID, name) + return + } + await serverSdk() + .client.auth.remove({ providerID }) + .then(async () => { + await serverSdk().client.global.dispose() + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }), + description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }), + }) + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + } + + return ( + <> +
+

{language.t("settings.providers.title")}

+
+ +
+
+

{language.t("settings.providers.section.connected")}

+ + 0} + fallback={ +
{language.t("settings.providers.connected.empty")}
+ } + > + + {(item) => ( +
+
+ +
+ {item.name} + {type(item)} +
+
+ + {language.t("settings.providers.connected.environmentDescription")} + + } + > + void disconnect(item.id, item.name)}> + {language.t("common.disconnect")} + + +
+ )} +
+
+
+
+ +
+

{language.t("settings.providers.section.popular")}

+ + + {(item) => ( +
+
+ +
+
+ {item.name} + + {language.t("dialog.provider.tag.recommended")} + +
+ + {(key) =>

{language.t(key())}

} +
+
+
+ connect(item.id)}> + {language.t("common.connect")} + +
+ )} +
+ + +
+
+ +
+
+ {language.t("provider.custom.title")} + {language.t("settings.providers.tag.custom")} +
+

+ {language.t("settings.providers.custom.description")} +

+
+
+ { + dialog.show(() => ) + }} + > + {language.t("common.connect")} + +
+
+
+ + +
+
+ + ) +} diff --git a/packages/app/src/components/settings-v2/servers.tsx b/packages/app/src/components/settings-v2/servers.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b93f700d24cff026a4fbf3261eee68f5b47f0ea5 --- /dev/null +++ b/packages/app/src/components/settings-v2/servers.tsx @@ -0,0 +1,139 @@ +import { Tag } from "@opencode-ai/ui/v2/badge-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import fuzzysort from "fuzzysort" +import { type Component, For, Show, createMemo } from "solid-js" +import { createStore } from "solid-js/store" +import { ServerRowMenu } from "@/components/server/server-row-menu" +import { ServerHealthIndicator } from "@/components/server/server-row" +import { useLanguage } from "@/context/language" +import { ServerConnection, serverName } from "@/context/server" +import { useServerManagementController } from "../dialog-select-server" +import { DialogServerV2 } from "./dialog-server-v2" +import { SettingsListV2 } from "./parts/list" +import { AddServerMenu, isWslServer, useFilteredWslServers, WslServerSettings } from "@/wsl/settings" +import "./settings-v2.css" + +export const SettingsServersV2: Component = () => { + const dialog = useDialog() + const language = useLanguage() + const controller = useServerManagementController() + const [store, setStore] = createStore({ filter: "" }) + const wslServers = useFilteredWslServers(() => store.filter) + + const showSearch = createMemo( + () => controller.sortedItems().filter((item) => !isWslServer(item)).length + wslServers().length > 1, + ) + + const filtered = createMemo(() => { + const items = controller.sortedItems().filter((item) => !isWslServer(item)) + const query = store.filter.trim() + if (!query) return items + return fuzzysort + .go(query, items, { + keys: [(item) => serverName(item), (item) => item.http.url], + }) + .map((result) => result.obj) + }) + + const openAdd = () => { + dialog.push(() => ) + } + + const openEdit = (server: ServerConnection.Http) => { + dialog.push(() => ) + } + + return ( + <> +
+
+

{language.t("status.popover.tab.servers")}

+ +
+ + + +
+ +
+ 0 || wslServers().length > 0} + fallback={ +
+ {store.filter ? language.t("palette.empty") : language.t("dialog.server.empty")} + + "{store.filter}" + +
+ } + > + + + + {(item) => { + const key = ServerConnection.key(item) + const health = () => controller.status()[key] + const isDefault = () => controller.defaultKey() === key + return ( +
+
+ +
+ {serverName(item)} + + v{health()?.version} + • + {language.t("server.row.noUsername")}} + > + {item.http.username} + + +
+
+
+ + {language.t("dialog.server.status.default")} + + +
+
+ ) + }} +
+
+
+
+ + ) +} diff --git a/packages/app/src/components/settings-v2/settings-v2.css b/packages/app/src/components/settings-v2/settings-v2.css new file mode 100644 index 0000000000000000000000000000000000000000..b73269b496df427a835bdcc88bbc9d2ce286e0a1 --- /dev/null +++ b/packages/app/src/components/settings-v2/settings-v2.css @@ -0,0 +1,729 @@ +@import "@opencode-ai/ui/v2/text-input-v2.css"; +@import "@opencode-ai/ui/v2/button-v2.css"; + +[data-component="tabs-v2"][data-variant="settings"] { + height: 100%; +} + +[data-component="dialog-v2"][data-variant="settings"] [data-slot="dialog-container"] { + background: var(--v2-background-bg-base); +} + +[data-component="dialog-v2"][data-variant="settings"] [data-slot="dialog-body"] { + padding: 0; + overflow: hidden; +} + +.settings-v2-panel { + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; + scrollbar-width: none; + user-select: none; +} + +.settings-v2-panel :is(input, textarea, [contenteditable="true"]) { + user-select: text; +} + +.settings-v2-panel::-webkit-scrollbar { + display: none; +} + +.settings-v2-tab-header { + position: sticky; + top: 0; + z-index: 10; + padding: 40px 40px 32px; + background: linear-gradient(to bottom, var(--v2-background-bg-base) calc(100% - 24px), transparent); +} + +.settings-v2-tab-title { + font-size: 15px; + font-weight: 640; + line-height: 1; + color: var(--v2-text-text-base); +} + +.settings-v2-tab-body { + display: flex; + flex-direction: column; + gap: 36px; + width: 100%; + padding: 0 40px 40px; +} + +[data-slot="settings-v2-row-description"] a.settings-v2-link { + color: var(--v2-text-text-accent); + cursor: pointer; + text-decoration: none; +} + +[data-slot="settings-v2-row-description"] a.settings-v2-link:hover { + color: var(--v2-text-text-accent-hover); +} + +.settings-v2-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.settings-v2-section-title { + padding-bottom: 8px; + font-size: 15px; + font-weight: 640; + line-height: 1; + color: var(--v2-text-text-base); +} + +.settings-v2-section-title + [data-component="settings-v2-list"] { + margin-top: -4px; + margin-bottom: 0; +} + +[data-component="settings-v2-list"] { + border-radius: 8px; + background-color: var(--v2-background-bg-layer-01); + padding-inline: 20px; + box-shadow: inset 0 0 0 0.5px var(--v2-border-border-muted); +} + +.settings-v2-interface-feature [data-component="settings-v2-list"] { + background-color: var(--v2-background-bg-base); + box-shadow: var(--v2-elevation-raised); +} + +[data-component="settings-v2-row"] { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 16px; + padding-block: 20px; + border-bottom: 0.5px solid var(--v2-border-border-base); +} + +[data-component="settings-v2-row"]:last-child { + border-bottom: none; +} + +@media (min-width: 640px) { + [data-component="settings-v2-row"] { + flex-wrap: nowrap; + } +} + +[data-slot="settings-v2-row-copy"] { + display: flex; + min-width: 0; + flex: 1; + flex-direction: column; + gap: 8px; +} + +[data-slot="settings-v2-row-title"] { + font-style: normal; + font-size: 13px; + font-weight: 530; + line-height: 1; + letter-spacing: -0.04px; + color: var(--v2-text-text-base); + font-variation-settings: "slnt" 0; +} + +[data-slot="settings-v2-row-description"] { + margin-block: -3.5px; + font-size: 13px; + font-weight: 440; + line-height: 20px; + color: var(--v2-text-text-muted); +} + +[data-slot="settings-v2-row-control"] { + display: flex; + width: 100%; + justify-content: flex-end; +} + +[data-slot="settings-v2-row-control"] > div:has([data-component="switch"]), +[data-slot="settings-v2-row-control"] > [data-component="switch"] { + display: inline-flex; + align-items: center; + padding: 4px; +} + +@media (min-width: 640px) { + [data-slot="settings-v2-row-control"] { + width: auto; + flex-shrink: 0; + } +} + +[data-slot="settings-v2-row-control"] [data-component="text-input-v2"] { + width: 100%; +} + +[data-component="dialog-v2"][data-variant="settings"] [data-component="select-v2-root"] { + width: fit-content; + max-width: 100%; +} + +[data-component="dialog-v2"][data-variant="settings"] [data-component="button-v2"] { + width: fit-content; + max-width: 100%; +} + +[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] [data-slot="tabs-v2-list"] { + background-color: var(--v2-background-bg-layer-01); +} + +@media (max-width: 639px) { + .settings-v2[data-component="tabs-v2"][data-variant="settings"][data-orientation="vertical"] + [data-slot="tabs-v2-list"] { + width: 144px; + min-width: 144px; + padding-inline: 8px; + } +} + +.settings-v2-nav-footer { + display: flex; + flex-direction: column; + gap: 8px; + padding: 4px 0 4px 4px; + user-select: none; +} + +.settings-v2-nav-footer > span { + font-size: 11px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-faint); +} + +.settings-v2-legacy-panel { + height: 100%; + overflow: hidden; +} + +.settings-v2-legacy-panel [data-component="dialog"] { + display: contents; +} + +.settings-v2-provider-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 16px; + padding-block: 20px; + border-bottom: 0.5px solid var(--v2-border-border-base); +} + +.settings-v2-provider-row:last-child { + border-bottom: none; +} + +@media (min-width: 640px) { + .settings-v2-provider-row { + flex-wrap: nowrap; + } +} + +.settings-v2-providers [data-component="provider-icon"] { + color: var(--v2-icon-icon-base); +} + +.settings-v2-provider-lead { + display: flex; + min-width: 0; + flex: 1; + align-items: flex-start; + gap: 10px; +} + +.settings-v2-provider-lead:not(:has(.settings-v2-provider-copy)) { + align-items: center; +} + +.settings-v2-provider-copy { + display: flex; + min-width: 0; + flex: 1; + flex-direction: column; + gap: 6px; +} + +.settings-v2-provider-main { + display: flex; + min-width: 0; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.settings-v2-provider-name { + font-size: 13px; + font-weight: 530; + line-height: 16px; + color: var(--v2-text-text-base); +} + +.settings-v2-provider-description { + margin-block: -3.5px; + font-size: 13px; + font-weight: 440; + line-height: 20px; + color: var(--v2-text-text-muted); +} + +.settings-v2-provider-empty { + padding-block: 20px; + font-size: 13px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-muted); +} + +.settings-v2-provider-env-hint { + padding-inline-end: 12px; + font-size: 13px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-muted); + opacity: 0; + transition: opacity 200ms ease; + cursor: default; +} + +.group:hover .settings-v2-provider-env-hint { + opacity: 1; +} + +.settings-v2-providers-view-all { + margin-top: 20px; + padding: 0; + border: 0; + background: transparent; + font-size: 13px; + font-weight: 530; + line-height: 1; + color: var(--v2-text-text-accent); + cursor: pointer; + text-align: start; +} + +.settings-v2-providers-view-all:hover { + color: var(--v2-text-text-accent-hover); +} + +.settings-v2-tab-body.settings-v2-providers { + gap: 32px; +} + +.settings-v2-tab-header:has(+ .settings-v2-tab-body.settings-v2-providers) { + padding-bottom: 32px; +} + +.settings-v2-providers .settings-v2-section-title { + padding-bottom: 0; + font-size: 13px; + font-weight: 530; + line-height: 1; +} + +.settings-v2-providers .settings-v2-section-title + [data-component="settings-v2-list"] { + margin-top: 16px; +} + +.settings-v2-tab-header.settings-v2-tab-header--stacked { + display: flex; + flex-direction: column; + gap: 32px; + padding-bottom: 32px; +} + +.settings-v2-tab-header--stacked > .settings-v2-tab-header-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.settings-v2-tab-search { + position: relative; + width: 100%; +} + +.settings-v2-tab-search [data-component="text-input-v2"] { + width: 100%; +} + +.settings-v2-tab-search [data-slot="text-input-v2-input"] { + padding-inline-end: 28px; +} + +.settings-v2-tab-search-clear { + position: absolute; + top: 50%; + inset-inline-end: 6px; + z-index: 1; + transform: translateY(-50%); +} + +.settings-v2-models { + gap: 12px; +} + +.settings-v2-models .settings-v2-section { + gap: 12px; +} + +.settings-v2-models .settings-v2-section[data-expanded] { + padding-bottom: 12px; +} + +.settings-v2-models-group-header { + display: flex; + align-items: center; + height: 28px; +} + +.settings-v2-models-group-trigger { + display: flex; + align-items: center; + gap: 4px; + height: 28px; + padding: 4px 8px 4px 4px; + border: 0; + border-radius: 4px; + background: transparent; + color: var(--v2-text-text-base); +} + +.settings-v2-models-group-trigger:focus-visible { + outline: 2px solid var(--v2-border-border-focus); + outline-offset: 2px; +} + +@media (hover: hover) { + .settings-v2-models-group-trigger:not(:disabled):hover { + background-color: var(--v2-background-bg-layer-02); + } +} + +.settings-v2-models-group-trigger:disabled { + cursor: default; +} + +.settings-v2-models-group-chevron { + display: flex; + width: 20px; + height: 20px; + flex-shrink: 0; + align-items: center; + justify-content: center; + color: var(--v2-icon-icon-muted); +} + +.settings-v2-models-group-label { + display: flex; + align-items: center; + gap: 8px; +} + +.settings-v2-models .settings-v2-section-title { + padding-bottom: 0; + font-size: 13px; + font-weight: 530; + line-height: 16px; +} + +.settings-v2-models [data-component="provider-icon"] { + color: var(--v2-icon-icon-base); +} + +.settings-v2-models [data-component="settings-v2-list"] { + padding-inline: 16px; +} + +.settings-v2-models .settings-v2-section-title + [data-component="settings-v2-list"] { + margin-top: 0; +} + +.settings-v2-models [data-slot="settings-v2-row-description"]:empty { + display: none; +} + +.settings-v2-models [data-slot="settings-v2-row-copy"] { + gap: 0; +} + +.settings-v2-models [data-slot="settings-v2-row-title"] { + min-width: 0; + overflow: hidden; + font-size: 13px; + font-weight: 440; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.settings-v2-models-status { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding-block: 48px; + font-size: 13px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-muted); + text-align: center; +} + +.settings-v2-models-status-filter { + color: var(--v2-text-text-base); +} + +.settings-v2-shortcuts .settings-v2-section { + gap: 16px; +} + +.settings-v2-shortcuts .settings-v2-section-title { + padding-bottom: 0; + font-size: 13px; + font-weight: 530; + line-height: 1; +} + +.settings-v2-shortcuts [data-component="settings-v2-list"] { + display: flex; + flex-direction: column; + gap: 0; + padding: 20px; + border-radius: 6px; +} + +.settings-v2-shortcuts [data-component="settings-v2-list"] > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding-top: 0; + padding-bottom: 0; + border-bottom: none; +} + +.settings-v2-shortcuts [data-component="settings-v2-list"] > div:not(:last-child) { + padding-bottom: 16px; + margin-bottom: 16px; + border-bottom: 0.5px solid var(--v2-border-border-base); +} + +.settings-v2-shortcuts [data-component="settings-v2-list"] > div > span { + font-weight: 440; + font-size: 13px; + line-height: 1; + letter-spacing: -0.04px; + color: var(--v2-text-text-base); + font-variation-settings: "slnt" 0; +} + +.settings-v2-keybind-button { + box-sizing: border-box; + flex-shrink: 0; + padding: 6px 8px; + margin: -6px -8px; + border: 0; + border-radius: 2px; + background: transparent; + cursor: pointer; + font-style: normal; + font-weight: 530; + font-size: 11px; + line-height: 1; + letter-spacing: 0.05px; + font-variant-numeric: tabular-nums; + font-feature-settings: + "tnum" on, + "lnum" on; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-faint); +} + +.settings-v2-keybind-button:hover { + background-color: var(--v2-background-bg-layer-02); +} + +.settings-v2-keybind-button:focus-visible { + outline: 2px solid var(--v2-border-border-focus); + outline-offset: 2px; +} + +.settings-v2-keybind-button--active { + color: var(--v2-text-text-faint); + border-radius: 2px; + background-color: var(--v2-background-bg-layer-02); +} + +.settings-v2-shortcuts-status { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding-block: 48px; + font-size: 13px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-muted); + text-align: center; +} + +.settings-v2-shortcuts-status-filter { + color: var(--v2-text-text-base); +} + +.settings-v2-tab-body.settings-v2-servers { + gap: 0; +} + +.settings-v2-tab-header.settings-v2-servers-header { + padding-bottom: 24px; +} + +.settings-v2-servers-header .settings-v2-tab-header-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.settings-v2-tab-header.settings-v2-servers-header.settings-v2-tab-header--stacked { + gap: 24px; + padding-bottom: 24px; +} + +.settings-v2-servers [data-component="settings-v2-list"] { + display: flex; + flex-direction: column; + gap: 0; + padding: 20px; + border-radius: 6px; +} + +.settings-v2-servers-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.settings-v2-servers-row:not(:last-child) { + padding-bottom: 16px; + margin-bottom: 16px; + border-bottom: 0.5px solid var(--v2-border-border-base); +} + +.settings-v2-servers-actions { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: flex-end; + gap: 8px; +} + +.settings-v2-servers-lead { + display: flex; + min-width: 0; + flex: 1; + align-items: flex-start; + gap: 10px; +} + +.settings-v2-servers-copy { + display: flex; + min-width: 0; + flex: 1; + flex-direction: column; + gap: 6px; +} + +.settings-v2-servers-name { + font-size: 13px; + font-weight: 530; + line-height: 1; + color: var(--v2-text-text-base); +} + +.settings-v2-servers-meta { + font-size: 11px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-muted); +} + +.settings-v2-servers-status { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding-block: 48px; + font-size: 13px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-muted); + text-align: center; +} + +.settings-v2-servers-status-filter { + color: var(--v2-text-text-base); +} + +[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-container"] { + width: 480px; + max-width: calc(100vw - 32px); + height: auto; + border-radius: 8px; + align-items: stretch; +} + +[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-content"] { + align-items: stretch; + width: 100%; +} + +[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-header"] { + align-items: center; + padding: 24px 24px 16px; +} + +[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-body"] { + display: flex; + width: 100%; + min-width: 0; + flex-direction: column; + align-items: stretch; +} + +[data-component="dialog-v2"].settings-v2-server-dialog [data-slot="dialog-footer"] { + padding: 24px; +} + +.settings-v2-server-dialog-label { + font-size: 13px; + font-weight: 530; + line-height: 1; + color: var(--v2-text-text-base); +} + +.settings-v2-server-dialog-error { + font-size: 11px; + font-weight: 440; + line-height: 1; + color: var(--v2-state-fg-danger); +} diff --git a/packages/app/src/components/ui/drawer.tsx b/packages/app/src/components/ui/drawer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0fb01f5f6e41f0b2943924174959a5d46507f9e8 --- /dev/null +++ b/packages/app/src/components/ui/drawer.tsx @@ -0,0 +1,119 @@ +/** + * Taken from https://www.solid-ui.com/docs/components/drawer + * Only used in one place hence not a v2 component yet... can be promoted to ui/v2 later + */ + +import type { Component, ComponentProps, JSX, ValidComponent } from "solid-js" +import { splitProps } from "solid-js" +import type { ContentProps, DescriptionProps, DynamicProps, LabelProps, OverlayProps } from "@corvu/drawer" +import DrawerPrimitive from "@corvu/drawer" + +const Drawer = DrawerPrimitive + +const DrawerTrigger = DrawerPrimitive.Trigger + +const DrawerPortal = DrawerPrimitive.Portal + +const DrawerClose = DrawerPrimitive.Close + +type DrawerOverlayProps = OverlayProps & { class?: string } + +const DrawerOverlay = (props: DynamicProps>) => { + const [, rest] = splitProps(props as DrawerOverlayProps, ["class"]) + const drawerContext = DrawerPrimitive.useContext() + const overlayStyle = () => { + const state = drawerContext.transitionState() + if (state === "opening" || state === "closing") return undefined + const open = drawerContext.openPercentage() + return { + opacity: open, + "backdrop-filter": `blur(${4 * open}px)`, + } + } + return ( + + ) +} + +type DrawerContentProps = ContentProps & { + class?: string + children?: JSX.Element +} + +const DrawerContent = (props: DynamicProps>) => { + const [, rest] = splitProps(props as DrawerContentProps, ["class", "children"]) + return ( + + + + {props.children} + + + ) +} + +const DrawerHeader: Component> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return
+} + +const DrawerFooter: Component> = (props) => { + const [, rest] = splitProps(props, ["class"]) + return
+} + +type DrawerTitleProps = LabelProps & { class?: string } + +const DrawerTitle = (props: DynamicProps>) => { + const [, rest] = splitProps(props as DrawerTitleProps, ["class"]) + return ( + + ) +} + +type DrawerDescriptionProps = DescriptionProps & { + class?: string +} + +const DrawerDescription = (props: DynamicProps>) => { + const [, rest] = splitProps(props as DrawerDescriptionProps, ["class"]) + return ( + + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/packages/app/src/context/command.test.ts b/packages/app/src/context/command.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0d90575113381d70e7969bbf9168c7aa3efd238 --- /dev/null +++ b/packages/app/src/context/command.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, test } from "bun:test" +import { + activeCommandRegistrations, + addCommandRegistration, + commandPaletteOptions, + resolveKeybindOption, + type CommandOption, +} from "./command" + +const paletteOptions: CommandOption[] = [ + { id: "settings.open", title: "Open settings" }, + { id: "session.undo", title: "Undo" }, + { id: "file.open", title: "Open file" }, + { id: "hidden", title: "Hidden", hidden: true }, + { id: "disabled", title: "Disabled", disabled: true }, +] + +describe("commandPaletteOptions", () => { + test("keeps visible enabled commands", () => { + expect(commandPaletteOptions(paletteOptions).map((option) => option.id)).toEqual(["settings.open", "session.undo"]) + }) +}) + +describe("command registrations", () => { + test("shadows keyed registrations while retaining the previous owner", () => { + const one = () => [{ id: "one", title: "One" }] + const two = () => [{ id: "two", title: "Two" }] + + const registrations = addCommandRegistration([{ key: "layout", options: one }], { + key: "layout", + options: two, + }) + const active = activeCommandRegistrations(registrations) + + expect(registrations).toHaveLength(2) + expect(active).toHaveLength(1) + expect(active[0]?.options).toBe(two) + + const restored = activeCommandRegistrations(registrations.filter((entry) => entry.options !== two)) + expect(restored).toHaveLength(1) + expect(restored[0]?.options).toBe(one) + }) + + test("keeps unkeyed registrations additive", () => { + const one = () => [{ id: "one", title: "One" }] + const two = () => [{ id: "two", title: "Two" }] + + const next = activeCommandRegistrations(addCommandRegistration([{ options: one }], { options: two })) + + expect(next).toHaveLength(2) + expect(next[0]?.options).toBe(two) + expect(next[1]?.options).toBe(one) + }) +}) + +describe("resolveKeybindOption", () => { + test("prefers a matching contextual command over the global fallback", () => { + const fallback = { id: "tab.close", title: "Close tab" } + const contextual = { id: "terminal.close", title: "Close terminal", when: () => true } + + expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(contextual) + }) + + test("uses the global fallback outside the command context", () => { + const fallback = { id: "tab.close", title: "Close tab" } + const contextual = { id: "terminal.close", title: "Close terminal", when: () => false } + + expect(resolveKeybindOption([fallback, contextual], new KeyboardEvent("keydown"))).toBe(fallback) + }) +}) diff --git a/packages/app/src/context/comments.test.ts b/packages/app/src/context/comments.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5050409a81337da2573afb5aa023b9b71e8dbdd5 --- /dev/null +++ b/packages/app/src/context/comments.test.ts @@ -0,0 +1,188 @@ +import { beforeAll, describe, expect, mock, test } from "bun:test" +import { createRoot } from "solid-js" +import type { LineComment } from "./comments" + +let createCommentSessionForTest: typeof import("./comments").createCommentSessionForTest + +beforeAll(async () => { + mock.module("@solidjs/router", () => ({ + useNavigate: () => () => undefined, + useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], + })) + mock.module("@opencode-ai/ui/context", () => ({ + createSimpleContext: () => ({ + use: () => undefined, + provider: () => undefined, + }), + })) + const mod = await import("./comments") + createCommentSessionForTest = mod.createCommentSessionForTest +}) + +function line(file: string, id: string, time: number): LineComment { + return { + id, + file, + comment: id, + time, + selection: { start: 1, end: 1 }, + } +} + +describe("comments session indexing", () => { + test("keeps file list behavior and aggregate chronological order", () => { + createRoot((dispose) => { + const now = Date.now() + const comments = createCommentSessionForTest({ + "a.ts": [line("a.ts", "a-late", now + 20_000), line("a.ts", "a-early", now + 1_000)], + "b.ts": [line("b.ts", "b-mid", now + 10_000)], + }) + + expect(comments.list("a.ts").map((item) => item.id)).toEqual(["a-late", "a-early"]) + expect(comments.all().map((item) => item.id)).toEqual(["a-early", "b-mid", "a-late"]) + + const next = comments.add({ + file: "b.ts", + comment: "next", + selection: { start: 2, end: 2 }, + }) + + expect(comments.list("b.ts").at(-1)?.id).toBe(next.id) + expect(comments.all().map((item) => item.time)).toEqual( + comments + .all() + .map((item) => item.time) + .slice() + .sort((a, b) => a - b), + ) + + dispose() + }) + }) + + test("remove updates file and aggregate indexes consistently", () => { + createRoot((dispose) => { + const comments = createCommentSessionForTest({ + "a.ts": [line("a.ts", "a1", 10), line("a.ts", "shared", 20)], + "b.ts": [line("b.ts", "shared", 30)], + }) + + comments.setFocus({ file: "a.ts", id: "shared" }) + comments.setActive({ file: "a.ts", id: "shared" }) + comments.remove("a.ts", "shared") + + expect(comments.list("a.ts").map((item) => item.id)).toEqual(["a1"]) + expect( + comments + .all() + .filter((item) => item.id === "shared") + .map((item) => item.file), + ).toEqual(["b.ts"]) + expect(comments.focus()).toBeNull() + expect(comments.active()).toEqual({ file: "a.ts", id: "shared" }) + + dispose() + }) + }) + + test("clear resets file and aggregate indexes plus focus state", () => { + createRoot((dispose) => { + const comments = createCommentSessionForTest({ + "a.ts": [line("a.ts", "a1", 10)], + }) + + const next = comments.add({ + file: "b.ts", + comment: "next", + selection: { start: 2, end: 2 }, + }) + + comments.setActive({ file: "b.ts", id: next.id }) + comments.clear() + + expect(comments.list("a.ts")).toEqual([]) + expect(comments.list("b.ts")).toEqual([]) + expect(comments.all()).toEqual([]) + expect(comments.focus()).toBeNull() + expect(comments.active()).toBeNull() + + dispose() + }) + }) + + test("remove keeps focus when same comment id exists in another file", () => { + createRoot((dispose) => { + const comments = createCommentSessionForTest({ + "a.ts": [line("a.ts", "shared", 10)], + "b.ts": [line("b.ts", "shared", 20)], + }) + + comments.setFocus({ file: "b.ts", id: "shared" }) + comments.remove("a.ts", "shared") + + expect(comments.focus()).toEqual({ file: "b.ts", id: "shared" }) + expect(comments.list("a.ts")).toEqual([]) + expect(comments.list("b.ts").map((item) => item.id)).toEqual(["shared"]) + + dispose() + }) + }) + + test("setFocus and setActive updater callbacks receive current state", () => { + createRoot((dispose) => { + const comments = createCommentSessionForTest() + + comments.setFocus({ file: "a.ts", id: "a1" }) + comments.setFocus((current) => { + expect(current).toEqual({ file: "a.ts", id: "a1" }) + return { file: "b.ts", id: "b1" } + }) + + comments.setActive({ file: "c.ts", id: "c1" }) + comments.setActive((current) => { + expect(current).toEqual({ file: "c.ts", id: "c1" }) + return null + }) + + expect(comments.focus()).toEqual({ file: "b.ts", id: "b1" }) + expect(comments.active()).toBeNull() + + dispose() + }) + }) + + test("update changes only the targeted comment body", () => { + createRoot((dispose) => { + const comments = createCommentSessionForTest({ + "a.ts": [line("a.ts", "a1", 10), line("a.ts", "a2", 20)], + }) + + comments.update("a.ts", "a2", "edited") + + expect(comments.list("a.ts").map((item) => item.comment)).toEqual(["a1", "edited"]) + + dispose() + }) + }) + + test("replace swaps comment state and clears focus state", () => { + createRoot((dispose) => { + const comments = createCommentSessionForTest({ + "a.ts": [line("a.ts", "a1", 10)], + }) + + comments.setFocus({ file: "a.ts", id: "a1" }) + comments.setActive({ file: "a.ts", id: "a1" }) + comments.replace([line("b.ts", "b1", 30)]) + + expect(comments.list("a.ts")).toEqual([]) + expect(comments.list("b.ts").map((item) => item.id)).toEqual(["b1"]) + expect(comments.focus()).toBeNull() + expect(comments.active()).toBeNull() + + dispose() + }) + }) +}) diff --git a/packages/app/src/context/global.tsx b/packages/app/src/context/global.tsx new file mode 100644 index 0000000000000000000000000000000000000000..83438898b54de25fa1424c11c7c6f26f5979432f --- /dev/null +++ b/packages/app/src/context/global.tsx @@ -0,0 +1,161 @@ +import { createSimpleContext } from "@opencode-ai/ui/context" +import { createEffect, createMemo, createRoot } from "solid-js" +import { createStore } from "solid-js/store" +import { createServerProjects, RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server" +import { pathKey } from "@/utils/path-key" +import { useServerHealth } from "@/utils/server-health" +import { createServerSdkContext } from "./server-sdk" +import { createServerSyncContext } from "./server-sync" +import { getOwner } from "solid-js/web" +import { QueryClient } from "@tanstack/solid-query" +import type { ServerScope } from "@/utils/server-scope" + +export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext({ + name: "Global", + init: () => { + const server = useServer() + const serverHealth = useServerHealth( + () => server.list, + () => true, + ) + const [store, setStore] = createStore({ + settings: { + serverKey: undefined as ServerConnection.Key | undefined, + }, + }) + + const settingsServer = createMemo(() => { + const list = server.list + return list.find((conn) => ServerConnection.key(conn) === store.settings.serverKey) ?? list[0] + }) + + createEffect(() => { + const conn = settingsServer() + const key = conn ? ServerConnection.key(conn) : undefined + if (store.settings.serverKey !== key) setStore("settings", "serverKey", key) + }) + + const serverCtxs = new Map< + ServerConnection.Key, + { dispose: () => void; serverCtx: ReturnType } + >() + + const owner = getOwner() + + const ensureServerCtx = (conn: ServerConnection.Any) => { + const key = ServerConnection.key(conn) + const existing = serverCtxs.get(key) + if (existing) return existing.serverCtx + const root = createRoot((dispose) => { + const serverCtx = createServerCtx(conn, server.scope(key), server.projects.forServer(key)) + return { dispose, serverCtx } + }, owner as any) + serverCtxs.set(key, root) + return root.serverCtx + } + + createMemo(() => { + for (const conn of server.list) { + ensureServerCtx(conn) + } + }) + + createEffect(() => { + for (const [key] of serverCtxs) { + if (!server.list.find((conn) => ServerConnection.key(conn) === key)) { + const { dispose } = serverCtxs.get(key)! + dispose() + serverCtxs.delete(key) + } + } + }) + + return { + servers: { + list: () => server.list, + health: serverHealth, + }, + settings: { + server: { + get key() { + return store.settings.serverKey + }, + selected: settingsServer, + set(key: ServerConnection.Key) { + if (store.settings.serverKey !== key) setStore("settings", "serverKey", key) + }, + }, + }, + ensureServerCtx(conn: ServerConnection.Any) { + return ensureServerCtx(conn) + }, + } + }, +}) + +function createServerCtx( + conn: ServerConnection.Any, + scope: ServerScope, + projects: ReturnType, +) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnReconnect: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + }, + }, + }) + const sdk = createServerSdkContext(conn, scope) + const sync = createServerSyncContext(sdk) + + function enrich(project: { worktree: string; expanded: boolean }) { + const [childStore] = sync.child(project.worktree, { bootstrap: false }) + const projectID = childStore.project + const metadata = projectID + ? sync.data.project.find((x) => x.id === projectID) + : sync.data.project.find((x) => x.worktree === project.worktree) + + // Preserve local icon override from per-workspace localStorage cache (childStore.icon). + // Without this, different subdirectories of the same git repo would share the same + // icon from the database instead of using their individual overrides. + const base = { ...metadata, ...project } + if (childStore.icon) { + return { ...base, icon: { ...base.icon, override: childStore.icon } } + } + return base + } + + const projectsList = createMemo(() => projects.list().map(enrich)) + const recentlyClosedList = createMemo(() => { + const known = new Set(sync.data.project.map((project) => pathKey(project.worktree))) + return projects + .recentlyClosed() + .filter((worktree) => known.has(pathKey(worktree))) + .slice(0, RECENTLY_CLOSED_DISPLAY_LIMIT) + .map((worktree) => enrich({ worktree, expanded: false })) + }) + + const isLocal = + (conn?.type === "sidecar" && conn.variant === "base") || (conn?.type === "http" && isLocalHost(conn.http.url)) + + return { + queryClient, + sdk, + sync, + isLocal, + projects: { + ...projects, + list: projectsList, + recentlyClosed: recentlyClosedList, + }, + } +} + +export type ServerCtx = ReturnType + +function isLocalHost(url: string) { + const host = url.replace(/^https?:\/\//, "").split(":")[0] + if (host === "localhost" || host === "127.0.0.1") return "local" +} diff --git a/packages/app/src/context/layout-tabs.test.ts b/packages/app/src/context/layout-tabs.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..646ed086cef9f0d233effb5fc3edfcaad88731da --- /dev/null +++ b/packages/app/src/context/layout-tabs.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, test } from "bun:test" +import { + SESSION_OPEN_FILE_TAB, + closeSessionTab, + openSessionTab, + previewSessionTab, + type SessionTabState, +} from "./layout-tabs" + +const state = (all: string[], active?: string, preview?: string): SessionTabState => ({ + tabs: { all, active }, + preview, +}) + +describe("previewSessionTab", () => { + test("appends the Open File placeholder", () => { + expect(previewSessionTab(state(["file://a.ts"], "file://a.ts"), SESSION_OPEN_FILE_TAB)).toEqual( + state(["file://a.ts", SESSION_OPEN_FILE_TAB], SESSION_OPEN_FILE_TAB, SESSION_OPEN_FILE_TAB), + ) + }) + + test("replaces the current preview in place", () => { + expect( + previewSessionTab( + state(["context", SESSION_OPEN_FILE_TAB, "file://b.ts"], SESSION_OPEN_FILE_TAB, SESSION_OPEN_FILE_TAB), + "file://a.ts", + ), + ).toEqual(state(["context", "file://a.ts", "file://b.ts"], "file://a.ts", "file://a.ts")) + }) + + test("activates a durable tab without duplicating it", () => { + expect( + previewSessionTab( + state(["file://a.ts", SESSION_OPEN_FILE_TAB, "file://b.ts"], SESSION_OPEN_FILE_TAB, SESSION_OPEN_FILE_TAB), + "file://b.ts", + ), + ).toEqual(state(["file://a.ts", "file://b.ts"], "file://b.ts")) + }) + + test("replaces a restored Open File placeholder", () => { + expect( + previewSessionTab(state(["file://a.ts", SESSION_OPEN_FILE_TAB], SESSION_OPEN_FILE_TAB), "file://b.ts"), + ).toEqual(state(["file://a.ts", "file://b.ts"], "file://b.ts", "file://b.ts")) + }) +}) + +describe("openSessionTab", () => { + test("pins the current preview", () => { + expect(openSessionTab(state(["file://a.ts"], "file://a.ts", "file://a.ts"), "file://a.ts")).toEqual( + state(["file://a.ts"], "file://a.ts"), + ) + }) + + test("replaces a preview with a directly opened file", () => { + expect(openSessionTab(state(["file://a.ts"], "file://a.ts", "file://a.ts"), "file://b.ts")).toEqual( + state(["file://b.ts"], "file://b.ts"), + ) + }) + + test("keeps the preview when switching to Review", () => { + expect(openSessionTab(state(["file://a.ts"], "file://a.ts", "file://a.ts"), "review")).toEqual( + state(["file://a.ts"], "review", "file://a.ts"), + ) + }) + + test("replaces a restored Open File placeholder with a direct open", () => { + expect(openSessionTab(state(["file://a.ts", SESSION_OPEN_FILE_TAB], SESSION_OPEN_FILE_TAB), "file://b.ts")).toEqual( + state(["file://a.ts", "file://b.ts"], "file://b.ts"), + ) + }) +}) + +describe("closeSessionTab", () => { + test("clears preview metadata and selects the left neighbor", () => { + expect( + closeSessionTab( + state(["file://a.ts", "file://b.ts", "file://c.ts"], "file://b.ts", "file://b.ts"), + "file://b.ts", + ), + ).toEqual(state(["file://a.ts", "file://c.ts"], "file://a.ts")) + }) +}) diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a80cf2e5804d6b94b137806a1fc36ab53f75e508 --- /dev/null +++ b/packages/app/src/context/models.tsx @@ -0,0 +1,173 @@ +import { type Accessor, createMemo, createResource } from "solid-js" +import { createStore } from "solid-js/store" +import { DateTime } from "luxon" +import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { useProviders } from "@/hooks/use-providers" +import { Persist, persisted } from "@/utils/persist" + +export type ModelKey = { providerID: string; modelID: string } + +type Visibility = "show" | "hide" +type User = ModelKey & { visibility: Visibility; favorite?: boolean } +type Store = { + user: User[] + recent: ModelKey[] + variant?: Record +} + +const RECENT_LIMIT = 5 + +function modelKey(model: ModelKey) { + return `${model.providerID}:${model.modelID}` +} + +export const { use: useModels, provider: ModelsProvider } = createSimpleContext({ + name: "Models", + gate: false, + init: (props: { directory?: Accessor } = {}) => { + const providers = useProviders(() => props.directory?.()) + + const [store, setStore, _, ready] = persisted( + Persist.global("model", ["model.v1"]), + createStore({ + user: [], + recent: [], + variant: {}, + }), + ) + + const available = createMemo(() => + providers.connected().flatMap((p) => + Object.values(p.models).map((m) => ({ + ...m, + provider: p, + })), + ), + ) + + const release = createMemo( + () => + new Map( + available().map((model) => { + const parsed = DateTime.fromISO(model.release_date) + return [modelKey({ providerID: model.provider.id, modelID: model.id }), parsed] as const + }), + ), + ) + + const latest = createMemo(() => + pipe( + available(), + filter( + (x) => + Math.abs( + (release().get(modelKey({ providerID: x.provider.id, modelID: x.id })) ?? DateTime.invalid("invalid")) + .diffNow() + .as("months"), + ) < 6, + ), + groupBy((x) => x.provider.id), + mapValues((models) => + pipe( + models, + groupBy((x) => x.family), + values(), + (groups) => + groups.flatMap((g) => { + const first = firstBy(g, [(x) => x.release_date, "desc"]) + return first ? [{ modelID: first.id, providerID: first.provider.id }] : [] + }), + ), + ), + values(), + flat(), + ), + ) + + const latestSet = createMemo(() => new Set(latest().map((x) => modelKey(x)))) + + const visibility = createMemo(() => { + const map = new Map() + for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility) + return map + }) + + const list = createMemo(() => + available().map((m) => ({ + ...m, + name: m.name.replace("(latest)", "").trim(), + latest: m.name.includes("(latest)"), + })), + ) + + const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID) + + function update(model: ModelKey, state: Visibility) { + const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID) + if (index >= 0) { + setStore("user", index, (current) => ({ ...current, visibility: state })) + return + } + setStore("user", store.user.length, { ...model, visibility: state }) + } + + const visible = (model: ModelKey) => { + const key = modelKey(model) + const state = visibility().get(key) + if (state === "hide") return false + if (state === "show") return true + if (latestSet().has(key)) return true + const date = release().get(key) + if (!date?.isValid) return true + return false + } + + const setVisibility = (model: ModelKey, state: boolean) => { + update(model, state ? "show" : "hide") + } + + const push = (model: ModelKey) => { + const uniq = uniqueBy([model, ...store.recent], (x) => `${x.providerID}:${x.modelID}`) + if (uniq.length > RECENT_LIMIT) uniq.pop() + setStore("recent", uniq) + } + + const variantKey = (model: ModelKey) => `${model.providerID}/${model.modelID}` + const getVariant = (model: ModelKey) => store.variant?.[variantKey(model)] + + const setVariant = (model: ModelKey, value: string | undefined) => { + const key = variantKey(model) + if (!store.variant) { + setStore("variant", { [key]: value }) + return + } + setStore("variant", key, value) + } + + const [recentModels] = createResource( + async () => { + const recent = store.recent + await ready.promise + return recent + }, + (p) => p, + { initialValue: [] }, + ) + return { + ready, + list, + find, + visible, + setVisibility, + recent: { + list: () => recentModels()!, + push, + }, + variant: { + get: getVariant, + set: setVariant, + }, + } + }, +}) diff --git a/packages/app/src/context/permission-auto-respond.test.ts b/packages/app/src/context/permission-auto-respond.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..794e03ae488213af6f695e09016e77d2c1ceb3a7 --- /dev/null +++ b/packages/app/src/context/permission-auto-respond.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, test } from "bun:test" +import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { autoRespondsPermission, isDirectoryAutoAccepting, sessionAutoAccept } from "./permission-auto-respond" + +const session = (input: { id: string; parentID?: string }) => + ({ + id: input.id, + parentID: input.parentID, + }) as Session + +const permission = (sessionID: string) => + ({ + sessionID, + }) as Pick + +describe("autoRespondsPermission", () => { + test("uses a parent session's directory-scoped auto-accept", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/root`]: true, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true) + }) + + test("uses a parent session's legacy auto-accept key", () => { + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + + expect(autoRespondsPermission({ root: true }, sessions, permission("child"), "/tmp/project")).toBe(true) + }) + + test("defaults to requiring approval when no lineage override exists", () => { + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" }), session({ id: "other" })] + const autoAccept = { + other: true, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("child"), "/tmp/project")).toBe(false) + }) + + test("inherits a parent session's false override", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/root`]: false, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(false) + }) + + test("prefers a child override over parent override", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/root`]: false, + [`${base64Encode(directory)}/child`]: true, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true) + }) + + test("falls back to directory-level auto-accept", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/*`]: true, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(true) + expect(sessionAutoAccept(autoAccept, sessions, permission("root"), directory)).toBeUndefined() + }) + + test("session-level override takes precedence over directory-level", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/*`]: true, + [`${base64Encode(directory)}/root`]: false, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("root"), directory)).toBe(false) + }) + + test("parent false override takes precedence over directory-level auto-accept", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/*`]: true, + [`${base64Encode(directory)}/root`]: false, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(false) + }) + + test("parent true override takes precedence over disabled directory fallback", () => { + const directory = "/tmp/project" + const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })] + const autoAccept = { + [`${base64Encode(directory)}/*`]: false, + [`${base64Encode(directory)}/root`]: true, + } + + expect(autoRespondsPermission(autoAccept, sessions, permission("child"), directory)).toBe(true) + }) +}) + +describe("isDirectoryAutoAccepting", () => { + test("returns true when directory key is set", () => { + const directory = "/tmp/project" + const autoAccept = { [`${base64Encode(directory)}/*`]: true } + expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(true) + }) + + test("returns false when directory key is not set", () => { + expect(isDirectoryAutoAccepting({}, "/tmp/project")).toBe(false) + }) + + test("returns false when directory key is explicitly false", () => { + const directory = "/tmp/project" + const autoAccept = { [`${base64Encode(directory)}/*`]: false } + expect(isDirectoryAutoAccepting(autoAccept, directory)).toBe(false) + }) +}) diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0408b233ffa8a4e334acb07edbbe57a8d81cf4d3 --- /dev/null +++ b/packages/app/src/context/platform.tsx @@ -0,0 +1,143 @@ +import { createSimpleContext } from "@opencode-ai/ui/context" +import type { AsyncStorage, SyncStorage } from "@solid-primitives/storage" +import type { Accessor } from "solid-js" +import type { DesktopMenuAction } from "../desktop-menu" +import { ServerConnection } from "./server" +import type { WslServersPlatform } from "../wsl/types" +import type { UpdaterPlatform } from "../updater" +import type { DraftStore } from "@/utils/draft-store" + +type PickerPaths = string | string[] | null +type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean } +type OpenAttachmentPickerOptions = { + title?: string + multiple?: boolean + accept?: string[] + extensions?: string[] + defaultPath?: string +} +type SaveFilePickerOptions = { title?: string; defaultPath?: string } +type PlatformName = "web" | "desktop" +type DesktopOS = "macos" | "windows" | "linux" + +export type FatalRendererErrorLog = { + error: string + url: string + version?: string + platform: PlatformName + os?: DesktopOS +} + +type PlatformBase = { + /** App version */ + version?: string + + /** Open a web or mail URL in the default system application */ + openExternal(url: string): void + + /** Open a local path in a local app (desktop only) */ + openPath?(path: string, app?: string): Promise + + /** Open a local file URL in its default app (desktop only) */ + openLocalFile?(url: string): void + + /** Reveal a local path in the system file manager; false when the path does not exist (desktop only) */ + revealPath?(path: string): Promise + + /** Restart the app */ + restart(): Promise + + /** Send a system notification */ + notify(title: string, description?: string, onClick?: () => void): Promise + + /** Open a native attachment picker and read selected files sequentially (desktop only) */ + openAttachmentPickerDialog?( + opts: OpenAttachmentPickerOptions, + onFile: (file: File) => Promise, + ): Promise + + /** Resolve the native source path for a desktop File. */ + getPathForFile?(file: File): string + + /** Open a native save file picker dialog (desktop only) */ + saveFilePickerDialog?(opts?: SaveFilePickerOptions): Promise + + /** Storage mechanism, defaults to localStorage */ + storage?: (name?: string) => SyncStorage | AsyncStorage + + /** Prompt drafts, history, and their blobs. */ + draftStore?: DraftStore + + /** Stable platform window identity for window-scoped persistence */ + windowID?: string + + /** Application-global desktop updater */ + updater?: UpdaterPlatform + + /** Fetch override */ + fetch?: typeof fetch + + /** Get the configured default server URL (platform-specific) */ + getDefaultServer?(): Promise + + /** Set the default server URL to use on app startup (platform-specific) */ + setDefaultServer?(url: ServerConnection.Key | null): Promise | void + + /** Manage WSL sidecar servers (Electron on Windows only) */ + wslServers?: WslServersPlatform + + /** Get the preferred display backend (desktop only) */ + getDisplayBackend?(): Promise | DisplayBackend | null + + /** Set the preferred display backend (desktop only) */ + setDisplayBackend?(backend: DisplayBackend): Promise + + /** Webview zoom level (desktop only) */ + webviewZoom?: Accessor + + /** Whether the native desktop window is fullscreen */ + windowFullscreen?: Accessor + + /** Get whether native pinch/Ctrl-scroll zoom gestures are enabled (desktop only) */ + getPinchZoomEnabled?(): Promise | boolean + + /** Allow native pinch/Ctrl-scroll zoom gestures (desktop only) */ + setPinchZoomEnabled?(enabled: boolean): Promise | void + + /** Run a desktop-only menu action from the app chrome */ + runDesktopMenuAction?(action: DesktopMenuAction): Promise | void + + /** Check if an editor app exists (desktop only) */ + checkAppExists?(appName: string): Promise + + /** Read image from clipboard (desktop only) */ + readClipboardImage?(): Promise + + /** Export collected diagnostic logs (desktop only) */ + exportDebugLogs?(): Promise + + /** Force focus styles on interactive elements through desktop devtools (desktop only) */ + setForceFocus?(enabled: boolean): Promise + + /** Record a fatal renderer error in platform logs (desktop only) */ + recordFatalRendererError?(error: FatalRendererErrorLog): Promise +} + +export type Platform = PlatformBase & + ( + | { platform: "web"; os?: never } + | { + platform: "desktop" + os?: DesktopOS + openDirectoryPickerDialog(opts?: OpenDirectoryPickerOptions): Promise + } + ) + +export type DisplayBackend = "auto" | "wayland" + +export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({ + name: "Platform", + init: (props: { value: Platform }) => { + return props.value + }, +}) diff --git a/packages/app/src/context/prompt-state.test.ts b/packages/app/src/context/prompt-state.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..24afe5424e71ac5d71537600a68787bf34625f7c --- /dev/null +++ b/packages/app/src/context/prompt-state.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test" +import { createRoot } from "solid-js" +import { createPromptState, DEFAULT_PROMPT } from "./prompt-state" + +describe("prompt state initialization", () => { + test("initializes prompt text, cursor, and model together", () => { + createRoot((dispose) => { + const model = { providerID: "anthropic", modelID: "claude", variant: "high" } + const prompt = createPromptState({ prompt: "hello", model }) + + expect(prompt.current()).toEqual([{ type: "text", content: "hello", start: 0, end: 5 }]) + expect(prompt.cursor()).toBe(5) + expect(prompt.model.current()).toEqual(model) + expect(prompt.model.current()).not.toBe(model) + dispose() + }) + }) + + test("uses the default prompt without initial values", () => { + createRoot((dispose) => { + const prompt = createPromptState() + + expect(prompt.current()).toEqual(DEFAULT_PROMPT) + expect(prompt.cursor()).toBeUndefined() + expect(prompt.model.current()).toBeUndefined() + dispose() + }) + }) +}) diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx new file mode 100644 index 0000000000000000000000000000000000000000..41a99f3b67e1333a5ba8890ac6379c93ddf93160 --- /dev/null +++ b/packages/app/src/context/prompt.tsx @@ -0,0 +1,170 @@ +import { base64Encode } from "@opencode-ai/core/util/encode" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { useParams, useSearchParams } from "@solidjs/router" +import { createMemo, createResource, createRoot, getOwner, onCleanup } from "solid-js" +import { requireServerKey } from "@/utils/session-route" +import { ServerConnection } from "./server" +import { useServerSDK } from "./server-sdk" +import { useSettings } from "./settings" +import { useSDK } from "./sdk" +import { useTabs, type Tab } from "./tabs" +import { + createPromptReady, + createPromptSession, + type ContextItem, + type FileContextItem, + type Prompt, + type PromptModel, + type PromptScope, + type PromptSession, +} from "./prompt-state" + +export { + createPromptReady, + createPromptSession, + createPromptState, + DEFAULT_PROMPT, + isCommentItem, + isPromptEqual, +} from "./prompt-state" +export type { + AgentPart, + ContentPart, + ContextItem, + FileAttachmentPart, + FileContextItem, + ImageAttachmentPart, + Prompt, + PromptModel, + PromptStore, + PromptScope, + PromptSession, + TextPart, +} from "./prompt-state" + +const WORKSPACE_KEY = "__workspace__" +const MAX_PROMPT_SESSIONS = 20 + +export function selectPromptTab(tabs: Tab[], scope: PromptScope, server: ServerConnection.Key) { + if ("draftID" in scope) return tabs.find((tab) => tab.type === "draft" && tab.draftID === scope.draftID) + if (!scope.id) return + return ( + tabs.find((tab) => tab.type === "session" && tab.server === server && tab.sessionId === scope.id) ?? + ({ type: "session", server, sessionId: scope.id } satisfies Tab) + ) +} + +function scopeKey(scope: PromptScope) { + if ("draftID" in scope) return `draft:${scope.draftID}` + return `${scope.dir}:${scope.id ?? WORKSPACE_KEY}` +} + +type PromptCacheEntry = { + value: PromptSession + dispose: VoidFunction +} + +export const createTabPromptState = ( + tabs: ReturnType, + tab: Tab, + ...args: Parameters +) => tabs.state(tab, "prompt", () => createPromptSession(...args)) + +export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({ + name: "Prompt", + gate: false, + init: () => { + const params = useParams<{ serverKey?: string; id?: string }>() + const sdk = useSDK() + const [search] = useSearchParams<{ draftId?: string }>() + const serverSDK = useServerSDK() + const tabs = useTabs() + const settings = useSettings() + const cache = new Map() + + const disposeAll = () => { + for (const entry of cache.values()) entry.dispose() + cache.clear() + } + + onCleanup(disposeAll) + + const prune = () => { + while (cache.size > MAX_PROMPT_SESSIONS) { + const first = cache.keys().next().value + if (!first) return + const entry = cache.get(first) + entry?.dispose() + cache.delete(first) + } + } + + const owner = getOwner() + const serverKey = () => + params.serverKey ? requireServerKey(params.serverKey) : ServerConnection.key(serverSDK().server) + const scope = (): PromptScope => + search.draftId ? { draftID: search.draftId } : { dir: base64Encode(sdk().directory), id: params.id } + const load = (scope: PromptScope) => { + const current = settings.general.newLayoutDesigns() ? selectPromptTab(tabs.store, scope, serverKey()) : undefined + if (current) return createTabPromptState(tabs, current, serverSDK().scope, scope) + + const key = scopeKey(scope) + const existing = cache.get(key) + if (existing) { + cache.delete(key) + cache.set(key, existing) + return existing.value + } + + const entry = createRoot( + (dispose) => ({ + value: createPromptSession(serverSDK().scope, scope), + dispose, + }), + owner, + ) + + cache.set(key, entry) + prune() + return entry.value + } + + const session = createMemo(() => load(scope())) + const pick = (scope?: PromptScope) => (scope ? load(scope) : session()) + const ready = createPromptReady(session) + + const withSuspense = (cb: () => T): (() => T) => + createResource( + async () => { + const value = cb() + await session().ready.promise + return value + }, + cb, + { initialValue: cb() }, + )[0] + + return { + ready, + capture: (scope?: PromptScope) => pick(scope).capture(), + current: withSuspense(() => session().current()), + cursor: withSuspense(() => session().cursor()), + dirty: withSuspense(() => session().dirty()), + model: { + current: withSuspense(() => session().model.current()), + set: (model: PromptModel | undefined) => session().model.set(model), + }, + context: { + items: withSuspense(() => session().context.items()), + add: (item: ContextItem) => session().context.add(item), + remove: (key: string) => session().context.remove(key), + removeComment: (path: string, commentID: string) => session().context.removeComment(path, commentID), + updateComment: (path: string, commentID: string, next: Partial & { comment?: string }) => + session().context.updateComment(path, commentID, next), + replaceComments: (items: FileContextItem[]) => session().context.replaceComments(items), + }, + set: (prompt: Prompt, cursorPosition?: number, scope?: PromptScope) => pick(scope).set(prompt, cursorPosition), + reset: (scope?: PromptScope) => pick(scope).reset(), + } + }, +}) diff --git a/packages/app/src/context/server-session-v2-reducer.ts b/packages/app/src/context/server-session-v2-reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..b34ab3985ec1a9fccfeefd3928e313fb95620dba --- /dev/null +++ b/packages/app/src/context/server-session-v2-reducer.ts @@ -0,0 +1,509 @@ +import type { OpenCodeEvent, SessionMessageInfo, SessionPendingMessage } from "@opencode-ai/client/promise" + +type Assistant = Extract +type Compaction = Extract +type Shell = Extract + +export type V2SessionReduction = { + sessionID: string + messages: SessionMessageInfo[] + touched: string[] + missing?: string +} + +export function createV2SessionReducer() { + const pending = new Map() + + const reduce = (source: readonly SessionMessageInfo[], event: OpenCodeEvent): V2SessionReduction | undefined => { + if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return + const sessionID = event.data.sessionID + const result = (messages: SessionMessageInfo[], touched: string[] = []): V2SessionReduction => ({ + sessionID, + messages, + touched, + }) + const append = (message: SessionMessageInfo) => + result(source.some((item) => item.id === message.id) ? [...source] : [...source, message], [message.id]) + + switch (event.type) { + case "session.input.admitted": + pending.set(key(sessionID, event.data.inputID), event.data.input) + return result([...source]) + case "session.input.promoted": { + const input = pending.get(key(sessionID, event.data.inputID)) + pending.delete(key(sessionID, event.data.inputID)) + if (!input) return { ...result([...source]), missing: event.data.inputID } + if (input.type === "user") + return append({ + id: event.data.inputID, + type: "user", + metadata: input.data.metadata, + text: input.data.text, + files: input.data.files, + agents: input.data.agents, + time: { created: event.created }, + }) + return append({ + id: event.data.inputID, + type: "synthetic", + metadata: input.data.metadata, + text: input.data.text, + description: input.data.description, + time: { created: event.created }, + }) + } + case "session.agent.selected": + return append({ + id: messageID(event.id), + type: "agent-switched", + metadata: event.metadata, + agent: event.data.agent, + time: { created: event.created }, + }) + case "session.model.selected": + return append({ + id: messageID(event.id), + type: "model-switched", + metadata: event.metadata, + model: event.data.model, + previous: source.findLast( + (item): item is Extract => + item.type === "model-switched" || item.type === "assistant", + )?.model, + time: { created: event.created }, + }) + case "session.synthetic": + return append({ + id: messageID(event.id), + type: "synthetic", + metadata: event.data.metadata, + text: event.data.text, + description: event.data.description, + time: { created: event.created }, + }) + case "session.skill.activated": + return append({ + id: messageID(event.id), + type: "skill", + metadata: event.metadata, + skill: event.data.id, + name: event.data.name, + text: event.data.text, + time: { created: event.created }, + }) + case "session.shell.started": + return append({ + id: messageID(event.id), + type: "shell", + metadata: event.metadata, + shellID: event.data.shell.id, + command: event.data.shell.command, + status: event.data.shell.status, + exit: event.data.shell.exit, + time: { created: event.created }, + }) + case "session.shell.ended": + return updateMessage( + source, + (item): item is Shell => item.type === "shell" && item.shellID === event.data.shell.id, + (item) => ({ + ...item, + status: event.data.shell.status, + exit: event.data.shell.exit, + output: event.data.output, + time: { ...item.time, completed: event.created }, + }), + sessionID, + ) + case "session.step.started": { + const current = source.findLast((item): item is Assistant => item.type === "assistant" && !item.time.completed) + const completed = + current && current.id !== event.data.assistantMessageID + ? update(source, current.id, (item) => + item.type === "assistant" + ? { ...item, retry: undefined, time: { ...item.time, completed: event.created } } + : item, + ) + : [...source] + const existing = completed.find((item) => item.id === event.data.assistantMessageID) + if (existing?.type === "assistant") + return result( + update(completed, existing.id, (item) => + item.type === "assistant" + ? { + ...item, + agent: event.data.agent, + model: event.data.model, + retry: undefined, + error: undefined, + finish: undefined, + snapshot: event.data.snapshot ? { ...item.snapshot, start: event.data.snapshot } : item.snapshot, + time: { ...item.time, completed: undefined }, + } + : item, + ), + current && current.id !== existing.id ? [current.id, existing.id] : [existing.id], + ) + return result( + [ + ...completed, + { + id: event.data.assistantMessageID, + type: "assistant", + metadata: event.metadata, + agent: event.data.agent, + model: event.data.model, + content: [], + snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, + time: { created: event.created }, + }, + ], + current ? [current.id, event.data.assistantMessageID] : [event.data.assistantMessageID], + ) + } + case "session.step.ended": + return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ + ...item, + finish: event.data.finish, + cost: event.data.cost, + tokens: event.data.tokens, + snapshot: + event.data.snapshot || event.data.files + ? { ...item.snapshot, end: event.data.snapshot, files: event.data.files } + : item.snapshot, + time: { ...item.time, completed: event.created }, + })) + case "session.step.failed": + return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ + ...item, + finish: "error", + error: event.data.error, + retry: undefined, + cost: event.data.cost ?? item.cost, + tokens: event.data.tokens ?? item.tokens, + snapshot: + event.data.snapshot || event.data.files + ? { ...item.snapshot, end: event.data.snapshot, files: event.data.files } + : item.snapshot, + time: { ...item.time, completed: event.created }, + })) + case "session.text.started": + return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ + ...item, + content: insertOrdinal(item.content, "text", event.data.ordinal, { type: "text", text: "" }), + })) + case "session.text.delta": + return updateContent(source, event.data.assistantMessageID, sessionID, "text", event.data.ordinal, (item) => ({ + ...item, + text: item.text + event.data.delta, + })) + case "session.text.ended": + return updateContent(source, event.data.assistantMessageID, sessionID, "text", event.data.ordinal, (item) => ({ + ...item, + text: event.data.text, + })) + case "session.reasoning.started": + return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ + ...item, + content: insertOrdinal(item.content, "reasoning", event.data.ordinal, { + type: "reasoning", + text: "", + state: event.data.state, + time: { created: event.created }, + }), + })) + case "session.reasoning.delta": + return updateContent( + source, + event.data.assistantMessageID, + sessionID, + "reasoning", + event.data.ordinal, + (item) => ({ + ...item, + text: item.text + event.data.delta, + }), + ) + case "session.reasoning.ended": + return updateContent( + source, + event.data.assistantMessageID, + sessionID, + "reasoning", + event.data.ordinal, + (item) => ({ + ...item, + text: event.data.text, + state: event.data.state ?? item.state, + time: { created: item.time?.created ?? event.created, completed: event.created }, + }), + ) + case "session.tool.input.started": + return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ + ...item, + content: item.content.some((content) => content.type === "tool" && content.id === event.data.callID) + ? item.content + : [ + ...item.content, + { + type: "tool", + id: event.data.callID, + name: event.data.name, + state: { status: "streaming", input: "" }, + time: { created: event.created }, + }, + ], + })) + case "session.tool.input.delta": + return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => + tool.state.status === "streaming" + ? { ...tool, state: { ...tool.state, input: tool.state.input + event.data.delta } } + : tool, + ) + case "session.tool.input.ended": + return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => + tool.state.status === "streaming" ? { ...tool, state: { ...tool.state, input: event.data.text } } : tool, + ) + case "session.tool.called": + return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => ({ + ...tool, + executed: event.data.executed, + providerState: event.data.state, + // structured: {}, content: [] + state: { status: "running", input: event.data.input, metadata: {} }, + time: { ...tool.time, ran: event.created }, + })) + case "session.tool.progress": + return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => + tool.state.status === "running" + ? { + ...tool, + // state: { ...tool.state, structured: event.data.structured, content: event.data.content }, + state: { ...tool.state, metadata: event.data.metadata }, + } + : tool, + ) + case "session.tool.success": + return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => { + if (tool.state.status !== "running") return tool + return { + ...tool, + executed: event.data.executed || tool.executed === true, + providerResultState: event.data.resultState, + state: { + status: "completed", + input: tool.state.input, + // structured: event.data.structured, + metadata: event.data.metadata, + content: event.data.content, + // result: event.data.result, + }, + time: { ...tool.time, completed: event.created }, + } + }) + case "session.tool.failed": + return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => { + if (tool.state.status !== "streaming" && tool.state.status !== "running") return tool + return { + ...tool, + executed: event.data.executed || tool.executed === true, + providerResultState: event.data.resultState, + state: { + status: "error", + input: typeof tool.state.input === "string" ? {} : tool.state.input, + // structured: tool.state.status === "running" ? tool.state.structured : {}, + metadata: event.data.metadata ?? (tool.state.status === "running" ? tool.state.metadata : {}), + content: event.data.content, + error: event.data.error, + // result: event.data.result, + }, + time: { ...tool.time, completed: event.created }, + } + }) + case "session.retry.scheduled": + return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ + ...item, + retry: { attempt: event.data.attempt, at: event.data.at, error: event.data.error }, + })) + case "session.execution.succeeded": + case "session.execution.failed": + case "session.execution.interrupted": { + const current = source.findLast((item): item is Assistant => item.type === "assistant" && !item.time.completed) + if (!current?.retry) return result([...source]) + return updateAssistant(source, current.id, sessionID, (item) => ({ ...item, retry: undefined })) + } + case "session.compaction.started": + return append({ + id: event.data.inputID ?? messageID(event.id), + type: "compaction", + status: "running", + metadata: event.metadata, + reason: event.data.reason, + summary: "", + recent: event.data.recent, + time: { created: event.created }, + }) + case "session.compaction.delta": + return updateMessage>( + source, + (item): item is Extract => + item.type === "compaction" && item.status === "running", + (item) => ({ + ...item, + summary: item.summary + event.data.text, + }), + sessionID, + ) + case "session.compaction.ended": { + const current = source.findLast( + (item): item is Extract => + item.type === "compaction" && item.status === "running", + ) + if (!current) + return append({ + id: messageID(event.id), + type: "compaction", + status: "completed", + metadata: event.metadata, + reason: event.data.reason, + summary: event.data.text, + recent: event.data.recent, + time: { created: event.created }, + }) + return result( + update(source, current.id, () => ({ + ...current, + status: "completed", + reason: event.data.reason, + summary: event.data.text, + recent: event.data.recent, + })), + [current.id], + ) + } + case "session.compaction.failed": { + const current = source.findLast( + (item): item is Extract => + item.type === "compaction" && item.status === "running", + ) + const failed: Extract = { + id: current?.id ?? event.data.inputID ?? messageID(event.id), + type: "compaction", + status: "failed", + metadata: current?.metadata ?? event.metadata, + reason: event.data.reason, + error: event.data.error, + time: current?.time ?? { created: event.created }, + } + if (!current) return append(failed) + return result( + update(source, current.id, () => failed), + [failed.id], + ) + } + default: + return + } + } + + return { + reduce, + clear(sessionID: string) { + for (const id of pending.keys()) { + if (id.startsWith(`${sessionID}:`)) pending.delete(id) + } + }, + } +} + +function key(sessionID: string, inputID: string) { + return `${sessionID}:${inputID}` +} + +function messageID(eventID: string) { + return eventID.replace(/^evt_/, "msg_") +} + +function update( + source: readonly SessionMessageInfo[], + id: string, + apply: (item: SessionMessageInfo) => SessionMessageInfo, +) { + return source.map((item) => (item.id === id ? apply(item) : item)) +} + +function updateMessage( + source: readonly SessionMessageInfo[], + matches: (item: SessionMessageInfo) => item is T, + apply: (item: T) => T, + sessionID: string, +): V2SessionReduction { + const current = source.findLast(matches) + if (!current) return { sessionID, messages: [...source], touched: [] } + return { + sessionID, + messages: update(source, current.id, (item) => (matches(item) ? apply(item) : item)), + touched: [current.id], + } +} + +function updateAssistant( + source: readonly SessionMessageInfo[], + id: string, + sessionID: string, + apply: (item: Assistant) => Assistant, +): V2SessionReduction { + return { + sessionID, + messages: update(source, id, (item) => (item.type === "assistant" ? apply(item) : item)), + touched: source.some((item) => item.id === id && item.type === "assistant") ? [id] : [], + } +} + +function updateContent( + source: readonly SessionMessageInfo[], + messageID: string, + sessionID: string, + type: T, + ordinal: number, + apply: ( + item: Extract, + ) => Extract, +) { + return updateAssistant(source, messageID, sessionID, (assistant) => { + let index = -1 + return { + ...assistant, + content: assistant.content.map((item) => { + if (item.type !== type || ++index !== ordinal) return item + return apply(item as Extract) + }), + } + }) +} + +function updateTool( + source: readonly SessionMessageInfo[], + messageID: string, + callID: string, + sessionID: string, + apply: ( + item: Extract, + ) => Extract, +) { + return updateAssistant(source, messageID, sessionID, (assistant) => ({ + ...assistant, + content: assistant.content.map((item) => (item.type === "tool" && item.id === callID ? apply(item) : item)), + })) +} + +function insertOrdinal( + source: Assistant["content"], + type: T, + ordinal: number, + item: Extract, +) { + const matches = source.filter((content) => content.type === type) + if (matches[ordinal]) return source + return [...source, item] +} diff --git a/packages/app/src/context/server-sync.test.ts b/packages/app/src/context/server-sync.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..7821a09e207faad07e8ce1530957694fcafb9b36 --- /dev/null +++ b/packages/app/src/context/server-sync.test.ts @@ -0,0 +1,233 @@ +import { describe, expect, test } from "bun:test" +import type { OpencodeClient } from "@opencode-ai/sdk/v2/client" +import type { + McpListInput, + McpResourceCatalogInput, + SessionApi, + SessionInfo, + SessionListInput, +} from "@opencode-ai/client/promise" +import { QueryClient } from "@tanstack/solid-query" +import { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction" +import { estimateRootSessionTotal, loadRootSessions } from "./global-sync/session-load" +import { loadActiveSessionsQuery, loadMcpQuery, loadMcpResourcesQuery, seedActiveSessionStatuses } from "./server-sync" +import { ServerScope } from "@/utils/server-scope" +import { createServerSession } from "./server-session" +import type { ServerApi } from "@/utils/server" + +type McpApi = ServerApi["mcp"] + +describe("MCP queries", () => { + test("loads current servers for the requested location", async () => { + const calls: unknown[] = [] + const queryClient = new QueryClient() + const result = await queryClient.fetchQuery( + loadMcpQuery(ServerScope.local, "/project", { + list: async (input: McpListInput = {}) => { + calls.push(input) + return { + location: { directory: "/project", project: { id: "project", directory: "/project" } }, + data: [ + { name: "docs", status: { status: "connected" } }, + { name: "search", status: { status: "pending" } }, + ], + } + }, + } as unknown as McpApi), + ) + + expect(calls).toEqual([{ location: { directory: "/project" } }]) + expect(result).toEqual({ docs: { status: "connected" }, search: { status: "pending" } }) + }) + + test("loads and keys the current resource catalog", async () => { + const calls: unknown[] = [] + const queryClient = new QueryClient() + const result = await queryClient.fetchQuery( + loadMcpResourcesQuery(ServerScope.local, "/project", { + resource: { + catalog: async (input: McpResourceCatalogInput = {}) => { + calls.push(input) + return { + location: { directory: "/project", project: { id: "project", directory: "/project" } }, + data: { + resources: [{ server: "docs", name: "Guide", uri: "docs://guide" }], + templates: [], + }, + } + }, + }, + } as unknown as McpApi), + ) + + expect(calls).toEqual([{ location: { directory: "/project" } }]) + expect(result).toEqual({ "docs:docs://guide": { server: "docs", name: "Guide", uri: "docs://guide" } }) + }) +}) + +describe("active session query", () => { + test("loads active sessions immediately and once per server cache", async () => { + let calls = 0 + const queryClient = new QueryClient() + const options = loadActiveSessionsQuery(ServerScope.local, { + active: async () => { + calls++ + return { ses_running: { type: "running" } } + }, + }) + + expect(await queryClient.fetchQuery(options)).toEqual({ ses_running: { type: "running" } }) + expect(await queryClient.fetchQuery(options)).toEqual({ ses_running: { type: "running" } }) + expect(calls).toBe(1) + expect(options.enabled).toBe(true) + expect([...options.queryKey]).toEqual([ServerScope.local, "activeSessions"]) + }) + + test("does not overwrite statuses already written by events", () => { + const session = createServerSession({} as OpencodeClient) + session.set("session_status", "ses_retry", { type: "retry", attempt: 2, message: "retrying", next: 10 }) + + seedActiveSessionStatuses(session, { + ses_running: { type: "running" }, + ses_retry: { type: "running" }, + }) + + expect(session.data.session_status.ses_running).toEqual({ type: "busy" }) + expect(session.data.session_status.ses_retry).toEqual({ + type: "retry", + attempt: 2, + message: "retrying", + next: 10, + }) + }) +}) + +describe("pickDirectoriesToEvict", () => { + test("keeps pinned stores and evicts idle stores", () => { + const now = 5_000 + const picks = pickDirectoriesToEvict({ + stores: ["a", "b", "c", "d"], + state: new Map([ + ["a", { lastAccessAt: 1_000 }], + ["b", { lastAccessAt: 4_900 }], + ["c", { lastAccessAt: 4_800 }], + ["d", { lastAccessAt: 3_000 }], + ]), + pins: new Set(["a"]), + max: 2, + ttl: 1_500, + now, + }) + + expect(picks).toEqual(["d", "c"]) + }) +}) + +describe("loadRootSessions", () => { + test("loads and normalizes a limited page of root sessions", async () => { + const calls: SessionListInput[] = [] + + const result = await loadRootSessions({ + api: { + list: async (query = {}) => { + calls.push(query) + return { data: [sessionInfo("session-1")], cursor: {} } + }, + } satisfies Pick, + directory: "dir", + limit: 10, + }) + + expect(result.data).toEqual([ + expect.objectContaining({ id: "session-1", directory: "dir", slug: "session-1", version: "" }), + ]) + expect(result.limited).toBe(true) + expect(calls).toEqual([{ directory: "dir", parentID: null, limit: 10, order: "desc" }]) + }) + + test("propagates list failures", () => { + expect( + loadRootSessions({ + api: { + list: async () => { + throw new Error("failed") + }, + } satisfies Pick, + directory: "dir", + limit: 25, + }), + ).rejects.toThrow("failed") + }) +}) + +function sessionInfo(id: string) { + return { + id, + projectID: "project-1", + agent: "build", + model: { id: "model-1", providerID: "provider-1" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, updated: 1 }, + title: id, + location: { directory: "dir" }, + } as SessionInfo +} + +describe("estimateRootSessionTotal", () => { + test("keeps exact total for full fetches", () => { + expect(estimateRootSessionTotal({ count: 42, limit: 10, limited: false })).toBe(42) + }) + + test("marks has-more for full-limit limited fetches", () => { + expect(estimateRootSessionTotal({ count: 10, limit: 10, limited: true })).toBe(11) + }) + + test("keeps exact total when limited fetch is under limit", () => { + expect(estimateRootSessionTotal({ count: 9, limit: 10, limited: true })).toBe(9) + }) +}) + +describe("canDisposeDirectory", () => { + test("rejects pinned or inflight directories", () => { + expect( + canDisposeDirectory({ + directory: "dir", + hasStore: true, + pinned: true, + booting: false, + loadingSessions: false, + }), + ).toBe(false) + expect( + canDisposeDirectory({ + directory: "dir", + hasStore: true, + pinned: false, + booting: true, + loadingSessions: false, + }), + ).toBe(false) + expect( + canDisposeDirectory({ + directory: "dir", + hasStore: true, + pinned: false, + booting: false, + loadingSessions: true, + }), + ).toBe(false) + }) + + test("accepts idle unpinned directory store", () => { + expect( + canDisposeDirectory({ + directory: "dir", + hasStore: true, + pinned: false, + booting: false, + loadingSessions: false, + }), + ).toBe(true) + }) +}) diff --git a/packages/app/src/context/server-sync.tsx b/packages/app/src/context/server-sync.tsx new file mode 100644 index 0000000000000000000000000000000000000000..13a0b74bc6f3a3e1156787e9facc17ba3241d53a --- /dev/null +++ b/packages/app/src/context/server-sync.tsx @@ -0,0 +1,760 @@ +import type { + Config, + OpencodeClient, + Path, + Project, + ProviderAuthResponse, + SessionStatus, +} from "@opencode-ai/sdk/v2/client" +import { showToast } from "@/utils/toast" +import { getFilename } from "@opencode-ai/core/util/path" +import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js" +import { createStore, produce, reconcile } from "solid-js/store" +import { useLanguage } from "@/context/language" +import type { InitError } from "../pages/error" +import { ServerSDK } from "./server-sdk" +import { + bootstrapDirectory, + bootstrapGlobal, + clearProviderRev, + loadAgentsQuery, + loadCommands, + loadGlobalConfigQuery, + loadPathQuery, + loadProjectsQuery, + loadProvidersQuery, + loadReferencesQuery, +} from "./global-sync/bootstrap" +import { createChildStoreManager } from "./global-sync/child-store" +import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer" +import { estimateRootSessionTotal, loadRootSessions, loadRootSessionsV1 } from "./global-sync/session-load" +import { trimSessions } from "./global-sync/session-trim" +import type { ProjectMeta } from "./global-sync/types" +import { SESSION_RECENT_LIMIT } from "./global-sync/types" +import { formatServerError } from "@/utils/server-errors" +import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" +import type { SolidQueryOptions } from "@tanstack/solid-query" +import { createRefreshQueue } from "./global-sync/queue" +import { directoryKey } from "./global-sync/utils" +import { PathKey } from "@/utils/path-key" +import { createDirSyncContext } from "./directory-sync" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" +import { createRefCountMap } from "@/utils/refcount" +import { useGlobal } from "./global" +import { ServerConnection, useServer } from "./server" +import { retry } from "@opencode-ai/core/util/retry" +import type { ServerScope } from "@/utils/server-scope" +import { createHomeSessionIndexCache } from "./global-sync/home-session-index" +import { persisted } from "@/utils/persist" +import type { ServerApi } from "@/utils/server" +import type { + McpListInput, + McpListOutput, + McpResource, + McpResourceCatalogInput, + McpResourceCatalogOutput, + McpServer, + SessionActiveOutput, +} from "@opencode-ai/client/promise" +import { toggleMcp } from "./global-sync/mcp" +import { createServerSession, type ServerSession } from "./server-session" + +type GlobalStore = { + ready: boolean + error?: InitError + path: Path + project: Project[] + provider: NormalizedProviderListResponse + provider_auth: ProviderAuthResponse + config: Config + reload: undefined | "pending" | "complete" +} + +type McpListApi = { + readonly list: (input?: McpListInput) => Promise +} + +type McpResourceApi = { + readonly resource: { + readonly catalog: (input?: McpResourceCatalogInput) => Promise + } +} + +type ApiQueryOptions = SolidQueryOptions & { + initialData?: undefined + queryKey: K +} + +type SessionActiveApi = { + readonly active: () => Promise +} + +export const loadMcpQuery = ( + scope: ServerScope, + directory: string, + api: McpListApi, + legacy?: OpencodeClient, + protocol?: Promise<"v1" | "v2">, +): ApiQueryOptions, readonly [ServerScope, string, "mcp"]> => + queryOptions< + Record, + Error, + Record, + readonly [ServerScope, string, "mcp"] + >({ + queryKey: [scope, directory, "mcp"] as const, + queryFn: async () => { + if ((await protocol) === "v1" && legacy) return (await legacy.mcp.status()).data ?? {} + return api + .list({ location: { directory } }) + .then((result) => Object.fromEntries(result.data.map((server) => [server.name, server.status]))) + }, + }) + +export const loadMcpResourcesQuery = ( + scope: ServerScope, + directory: string, + api: McpResourceApi, + legacy?: OpencodeClient, + protocol?: Promise<"v1" | "v2">, +): ApiQueryOptions, readonly [ServerScope, string, "mcpResources"]> => + queryOptions< + Record, + Error, + Record, + readonly [ServerScope, string, "mcpResources"] + >({ + queryKey: [scope, directory, "mcpResources"] as const, + queryFn: async () => { + if ((await protocol) === "v1" && legacy) { + return Object.fromEntries( + Object.entries((await legacy.experimental.resource.list()).data ?? {}).map(([key, resource]) => [ + key, + { ...resource, server: resource.client }, + ]), + ) + } + return api.resource + .catalog({ location: { directory } }) + .then((result) => + Object.fromEntries(result.data.resources.map((resource) => [`${resource.server}:${resource.uri}`, resource])), + ) + }, + placeholderData: {}, + }) + +export const loadLspQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) => + queryOptions({ + queryKey: [scope, directory, "lsp"] as const, + queryFn: () => sdk.lsp.status().then((r) => r.data ?? []), + }) + +export const loadActiveSessionsQuery = ( + scope: ServerScope, + api: SessionActiveApi, +): ApiQueryOptions => + queryOptions({ + queryKey: [scope, "activeSessions"] as const, + queryFn: () => api.active(), + enabled: true, + staleTime: Number.POSITIVE_INFINITY, + gcTime: Number.POSITIVE_INFINITY, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + }) + +export function seedActiveSessionStatuses( + session: Pick, + active: SessionActiveOutput | Record, +) { + for (const sessionID of Object.keys(active)) { + if (session.data.session_status[sessionID] !== undefined) continue + const status = active[sessionID] + session.set("session_status", sessionID, status?.type === "running" ? { type: "busy" } : status) + } +} + +function makeQueryOptionsApi( + scope: ServerScope, + serverSDK: () => OpencodeClient, + serverAPI: ServerApi, + sdkFor: (dir: PathKey) => OpencodeClient, + protocol: Promise<"v1" | "v2">, +) { + return { + globalConfig: () => loadGlobalConfigQuery(scope, serverSDK(), protocol), + projects: () => loadProjectsQuery(scope, serverAPI.project), + providers: (directory: PathKey | null) => + loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol), + path: (directory: PathKey | null) => + loadPathQuery(scope, directory, directory ? sdkFor(directory) : serverSDK(), protocol), + agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent, sdkFor(directory), protocol), + references: (directory: PathKey) => + loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol), + mcp: (directory: PathKey) => loadMcpQuery(scope, directory, serverAPI.mcp, sdkFor(directory), protocol), + mcpResources: (directory: PathKey) => + loadMcpResourcesQuery(scope, directory, serverAPI.mcp, sdkFor(directory), protocol), + lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)), + sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }), + } +} +export type QueryOptionsApi = ReturnType + +export function createServerSyncContextInner(serverSDK: ServerSDK) { + const language = useLanguage() + const owner = getOwner() + if (!owner) throw new Error("ServerSync must be created within owner") + + const sdkCache = new Map() + const booting = new Map>() + const sessionLoads = new Map>() + const sessionMeta = new Map() + + const sdkFor = (directory: string) => { + const key = directoryKey(directory) + const cached = sdkCache.get(key) + if (cached) return cached + const sdk = serverSDK.createClient({ + directory, + throwOnError: true, + }) + sdkCache.set(key, sdk) + return sdk + } + + const session = createServerSession(serverSDK.client, serverSDK.api.session, serverSDK.api.message, { + protocol: serverSDK.protocol, + }) + const queryOptionsApi = makeQueryOptionsApi( + serverSDK.scope, + () => serverSDK.client, + serverSDK.api, + sdkFor, + serverSDK.protocol, + ) + + const [configQuery, providerQuery, pathQuery] = useQueries(() => ({ + queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)], + })) + const activeSessionsQuery = useQuery(() => + loadActiveSessionsQuery(serverSDK.scope, { + active: async () => { + if ((await serverSDK.protocol) === "v1") { + const statuses = (await serverSDK.client.session.status()).data ?? {} + seedActiveSessionStatuses(session, statuses) + for (const sessionID of Object.keys(statuses)) { + void session.resolve(sessionID).catch(() => undefined) + } + return Object.fromEntries( + Object.entries(statuses).flatMap(([sessionID, status]) => + status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]], + ), + ) + } + const active = await serverSDK.api.session.active() + seedActiveSessionStatuses(session, active) + for (const sessionID of Object.keys(active)) { + void session.resolve(sessionID).catch(() => undefined) + } + return active + }, + }), + ) + + const [globalStore, setGlobalStore] = createStore({ + get ready() { + return !bootstrap.isPending + }, + project: [], + provider_auth: {}, + get path() { + const EMPTY = { state: "", config: "", worktree: "", directory: "", home: "" } + if (pathQuery.isLoading) return EMPTY + return pathQuery.data ?? EMPTY + }, + get provider() { + const EMPTY = { all: new Map(), connected: [], default: {} } + if (providerQuery.isLoading) return EMPTY + return providerQuery.data ?? EMPTY + }, + get config() { + if (configQuery.isLoading) return {} + return configQuery.data ?? {} + }, + get reload() { + return updateConfigMutation.isPending ? "pending" : undefined + }, + }) + + const queryClient = useQueryClient() + const homeSessions = createHomeSessionIndexCache(queryClient, ServerConnection.key(serverSDK.server)) + const refreshProviders = () => + queryClient.refetchQueries({ + predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers", + }) + + let bootedAt = 0 + let bootingRoot = false + let eventFrame: number | undefined + let eventTimer: ReturnType | undefined + + onCleanup(() => { + if (eventFrame !== undefined) cancelAnimationFrame(eventFrame) + if (eventTimer !== undefined) clearTimeout(eventTimer) + }) + + const setProjects = (next: Project[] | ((draft: Project[]) => Project[])) => { + setGlobalStore("project", next) + } + + const setBootStore = ((...input: unknown[]) => { + if (input[0] === "project" && Array.isArray(input[1])) { + setProjects(input[1] as Project[]) + return input[1] + } + return (setGlobalStore as (...args: unknown[]) => unknown)(...input) + }) as typeof setGlobalStore + + const bootstrap = useQuery(() => ({ + queryKey: [serverSDK.scope, "bootstrap"], + queryFn: async () => { + await bootstrapGlobal({ + serverSDK: serverSDK.client, + serverAPI: serverSDK.api, + protocol: serverSDK.protocol, + scope: serverSDK.scope, + requestFailedTitle: language.t("common.requestFailed"), + translate: language.t, + formatMoreCount: (count) => language.t("common.moreCountSuffix", { count }), + setGlobalStore: setBootStore, + queryClient, + }) + bootedAt = Date.now() + return bootedAt + }, + })) + + const set = ((...input: unknown[]) => { + if (input[0] === "project" && (Array.isArray(input[1]) || typeof input[1] === "function")) { + setProjects(input[1] as Project[] | ((draft: Project[]) => Project[])) + return input[1] + } + return (setGlobalStore as (...args: unknown[]) => unknown)(...input) + }) as typeof setGlobalStore + + const paused = () => untrack(() => globalStore.reload) !== undefined + + const queue = createRefreshQueue({ + paused, + key: directoryKey, + bootstrap: () => queryClient.fetchQuery({ queryKey: [serverSDK.scope, "bootstrap"] }), + bootstrapInstance, + }) + + const children = createChildStoreManager({ + owner, + scope: serverSDK.scope, + persist: persisted, + isBooting: (directory) => booting.has(directory), + isLoadingSessions: (directory) => sessionLoads.has(directory), + onBootstrap: (directory) => { + void bootstrapInstance(directory) + }, + onMcp: (directory, setStore) => { + void loadCommands(directory, serverSDK.api.command, sdkFor(directory), serverSDK.protocol) + .then((commands) => setStore("command", commands)) + .catch((err) => { + showToast({ + variant: "error", + title: language.t("toast.project.reloadFailed.title", { project: getFilename(directory) }), + description: formatServerError(err, language.t), + }) + }) + }, + onDispose: (directory) => { + const key = directoryKey(directory) + queue.clear(key) + sessionMeta.delete(key) + sdkCache.delete(key) + clearProviderRev(serverSDK.scope, key) + }, + translate: language.t, + queryOptions: queryOptionsApi, + global: { + provider: globalStore.provider, + }, + }) + + async function loadSessions(directory: string, options?: { limit?: number }) { + const key = directoryKey(directory) + const pending = sessionLoads.get(key) + if (pending) { + await pending + return loadSessions(directory, options) + } + + children.pin(key) + const [store, setStore] = children.child(directory, { bootstrap: false }) + const meta = sessionMeta.get(key) + const retainedLimit = Math.max(store.limit, options?.limit ?? 0, meta?.limit ?? 0) + if (meta && meta.limit >= retainedLimit) { + const next = trimSessions(store.session, { + limit: retainedLimit, + permission: session.data.permission, + }) + if (next.length !== store.session.length) { + setStore("session", reconcile(next, { key: "id" })) + } + children.unpin(key) + return + } + + const limit = Math.max(retainedLimit + SESSION_RECENT_LIMIT, SESSION_RECENT_LIMIT) + const promise = queryClient + .fetchQuery({ + ...queryOptionsApi.sessions(key), + queryFn: () => + serverSDK.protocol + .then((protocol) => + protocol === "v1" + ? loadRootSessionsV1({ client: sdkFor(directory), directory, limit }) + : loadRootSessions({ api: serverSDK.api.session, directory, limit }), + ) + .then((x) => { + const nonArchived = (x.data ?? []) + .filter((s) => !!s?.id) + .filter((s) => !s.time?.archived) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) + const limit = Math.max(store.limit, options?.limit ?? 0, sessionMeta.get(key)?.limit ?? 0) + const childSessions = store.session.filter((s) => !!s.parentID) + const next = trimSessions([...nonArchived, ...childSessions], { + limit, + permission: session.data.permission, + }) + batch(() => { + next.forEach(session.remember) + setStore( + "sessionTotal", + estimateRootSessionTotal({ + count: nonArchived.length, + limit: x.limit, + limited: x.limited, + }), + ) + setStore("session", reconcile(next, { key: "id" })) + }) + sessionMeta.set(key, { limit }) + }) + .catch((err) => { + console.error("Failed to load sessions", err) + const project = getFilename(directory) + showToast({ + variant: "error", + title: language.t("toast.session.listFailed.title", { project }), + description: formatServerError(err, language.t), + }) + }) + .then(() => null), + }) + .then(() => {}) + + sessionLoads.set(key, promise) + void promise.finally(() => { + sessionLoads.delete(key) + children.unpin(key) + }) + return promise + } + + async function bootstrapInstance(directory: string) { + const key = directoryKey(directory) + if (!key) return + const pending = booting.get(key) + if (pending) return pending + + children.pin(key) + const promise = Promise.resolve().then(async () => { + const child = children.ensureChild(directory) + const cache = children.vcsCache.get(key) + if (!cache) return + const sdk = sdkFor(directory) + await bootstrapDirectory({ + directory, + scope: serverSDK.scope, + mcp: children.mcp(key), + global: { + config: globalStore.config, + path: globalStore.path, + project: globalStore.project, + provider: globalStore.provider, + }, + sdk, + api: serverSDK.api, + store: child[0], + setStore: child[1], + vcsCache: cache, + loadSessions, + translate: language.t, + queryClient, + session, + protocol: serverSDK.protocol, + }) + }) + + booting.set(key, promise) + void promise.finally(() => { + booting.delete(key) + children.unpin(key) + }) + return promise + } + + const indexSession = (info: Parameters[0]) => { + const key = directoryKey(info.directory) + const existing = children.children[key] + if (!existing) return + applyDirectoryEvent({ + event: { type: "session.created", properties: { info } }, + directory: key, + store: existing[0], + setStore: existing[1], + push: queue.push, + retainedLimit: sessionMeta.get(key)?.limit, + sessionContent: false, + permission: session.data.permission, + loadLsp() {}, + }) + } + + const unsub = serverSDK.event.listen((e) => { + const directory = e.name + const key = directoryKey(directory) + const event = e.details + const eventType: string = event.type + const recent = bootingRoot || Date.now() - bootedAt < 1500 + + if (event.current) session.applyV2(event.current) + session.apply(event) + if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted") { + homeSessions.apply(event) + } + homeSessions.refresh(event.type) + if (eventType === "integration.connection.updated") void refreshProviders() + + if (directory === "global") { + if (eventType === "server.connected" && activeSessionsQuery.data === undefined && !activeSessionsQuery.isFetching) + void activeSessionsQuery.refetch() + applyGlobalEvent({ + event, + project: globalStore.project, + refresh: () => { + if (recent) return + bootstrap.refetch() + }, + setGlobalProject: setProjects, + }) + if ( + eventType === "config.updated" || + eventType === "catalog.updated" || + eventType === "agent.updated" || + eventType === "project.directories.updated" + ) + bootstrap.refetch() + if (eventType === "server.connected" || eventType === "global.disposed") { + if (recent) return + for (const directory of Object.keys(children.children)) { + if (!children.active(directory)) continue + queue.push(directory) + } + } + return + } + + if (event.current?.type === "session.moved") { + const info = session.get(event.current.data.sessionID) + if (info) indexSession(info) + } + if (event.current?.type === "session.forked") + void session + .resolve(event.current.data.sessionID, { force: true }) + .then(indexSession) + .catch(() => {}) + + const existing = children.children[key] + if (!existing) return + children.mark(key) + if ( + event.current?.type === "session.moved" || + // event.current?.type === "session.archived" || + event.current?.type === "session.forked" || + eventType === "command.updated" || + eventType === "config.updated" || + eventType === "agent.updated" + ) + queue.push(key) + if (eventType === "mcp.status.changed") void queryClient.invalidateQueries(queryOptionsApi.mcp(key)) + if (eventType === "mcp.resources.changed") void queryClient.invalidateQueries(queryOptionsApi.mcpResources(key)) + const [store, setStore] = existing + applyDirectoryEvent({ + event, + directory, + store, + setStore, + push: (directory) => { + if (children.active(directory)) queue.push(directory) + }, + retainedLimit: sessionMeta.get(key)?.limit, + sessionContent: false, + permission: session.data.permission, + vcsCache: children.vcsCache.get(key), + loadLsp: () => { + if (!children.active(key)) return + void queryClient.fetchQuery(queryOptionsApi.lsp(key)) + }, + loadReferences: () => { + if (!children.active(key)) return + void queryClient.fetchQuery(queryOptionsApi.references(key)) + }, + }) + }) + + onCleanup(unsub) + onCleanup(() => { + queue.dispose() + }) + onCleanup(() => { + for (const directory of Object.keys(children.children)) { + children.disposeDirectory(directoryKey(directory)) + } + }) + + onMount(() => { + if (typeof requestAnimationFrame === "function") { + eventFrame = requestAnimationFrame(() => { + eventFrame = undefined + eventTimer = setTimeout(() => { + eventTimer = undefined + void serverSDK.event.start() + }, 0) + }) + } else { + eventTimer = setTimeout(() => { + eventTimer = undefined + void serverSDK.event.start() + }, 0) + } + }) + + const projectApi = { + loadSessions, + meta(directory: string, patch: ProjectMeta) { + children.projectMeta(directory, patch) + }, + icon(directory: string, value: string | undefined) { + children.projectIcon(directory, value) + }, + } + + const updateConfigMutation = useMutation(() => ({ + mutationFn: (config: Config) => serverSDK.client.global.config.update({ config }), + onSuccess: () => { + bootstrap.refetch() + // Invalidate all provider queries so newly configured custom providers + // appear immediately in the available provider list across all directories. + queryClient.invalidateQueries({ queryKey: [serverSDK.scope, null, "providers"] }) + queryClient.invalidateQueries({ + predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers", + }) + }, + })) + + return { + data: globalStore, + set, + get ready() { + return globalStore.ready + }, + get error() { + return globalStore.error + }, + child: children.child, + peek: children.peek, + disableMcp: children.disableMcp, + queryOptions: queryOptionsApi, + refreshProviders, + // bootstrap, + updateConfig: updateConfigMutation.mutateAsync, + project: projectApi, + session, + homeSessions, + mcp: { + toggle: async (directory: string, name: string) => { + const key = directoryKey(directory) + const sdk = sdkFor(key) + const status = children.child(key, { bootstrap: false })[0].mcp[name]?.status + if (!status) return + await toggleMcp({ + status, + connect: async () => { + if ((await serverSDK.protocol) === "v1") { + await sdk.mcp.connect({ name }) + return + } + await serverSDK.api.mcp.connect({ server: name, location: { directory: key } }) + }, + disconnect: async () => { + if ((await serverSDK.protocol) === "v1") { + await sdk.mcp.disconnect({ name }) + return + } + await serverSDK.api.mcp.disconnect({ server: name, location: { directory: key } }) + }, + authenticate: async () => { + await sdk.mcp.auth.authenticate({ name }) + }, + refresh: async () => { + await queryClient.refetchQueries(queryOptionsApi.mcp(key)) + await queryClient.refetchQueries(queryOptionsApi.mcpResources(key)) + }, + }) + }, + }, + } +} + +export function createServerSyncContext(serverSDK: ServerSDK) { + const inner = createServerSyncContextInner(serverSDK) + return Object.assign(inner, { + ensureDirSyncContext: createRefCountMap( + (dir) => createDirSyncContext(dir, inner, serverSDK), + (dir) => inner.disableMcp(dir), + directoryKey, + ), + }) +} + +export type ServerSync = ReturnType + +export const { use: useServerSync, provider: ServerSyncProvider } = createSimpleContext({ + name: "ServerSync", + // Returns an accessor so the resolved server can change reactively without + // re-instantiating the subtree (mirrors useServerSDK). + init: (props: { server?: Accessor }) => { + const global = useGlobal() + const language = useLanguage() + const server = useServer() + + return createMemo(() => { + const conn = props.server?.() ?? server.current + if (!conn) throw new Error(language.t("error.serverSDK.noServerAvailable")) + return global.ensureServerCtx(conn).sync + }) + }, +}) + +export function useQueryOptions() { + const sync = useServerSync() + return createMemo(() => sync().queryOptions) +} diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx new file mode 100644 index 0000000000000000000000000000000000000000..450129f4588429bc4d1388796b81257998c1848f --- /dev/null +++ b/packages/app/src/context/server.tsx @@ -0,0 +1,360 @@ +import { createSimpleContext } from "@opencode-ai/ui/context" +import { type Accessor, batch, createMemo } from "solid-js" +import { createStore, type SetStoreFunction, type Store } from "solid-js/store" +import { Persist, persisted } from "@/utils/persist" +import { pathKey } from "@/utils/path-key" +import { ServerScope } from "@/utils/server-scope" + +type StoredProject = { worktree: string; expanded: boolean } +type StoredServer = string | ServerConnection.HttpBase | ServerConnection.Http +type ServerProjectState = { + projects: Record + lastProject: Record + recentlyClosed: Record +} +const HEALTH_POLL_INTERVAL_MS = 10_000 +// The store retains more history than is displayed. Consumers filter recently closed entries +// against the live project list (dropping deleted projects) and then cap the visible count via +// RECENTLY_CLOSED_DISPLAY_LIMIT. Retaining extra history ensures entries that are temporarily +// filtered out do not evict still-visible ones from the persisted store. +const RECENTLY_CLOSED_HISTORY_LIMIT = 16 +export const RECENTLY_CLOSED_DISPLAY_LIMIT = 5 + +export function normalizeServerUrl(input: string) { + const trimmed = input.trim() + if (!trimmed) return + const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}` + return withProtocol.replace(/\/+$/, "") +} + +export function serverName(conn?: ServerConnection.Any, ignoreDisplayName = false) { + if (!conn) return "" + if (conn.displayName && !ignoreDisplayName) return conn.displayName + return conn.http.url.replace(/^https?:\/\//, "").replace(/\/+$/, "") +} + +function isLocalHost(url: string) { + const host = url.replace(/^https?:\/\//, "").split(":")[0] + if (host === "localhost" || host === "127.0.0.1") return "local" +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +export function migrateCanonicalLocalServerState(value: unknown, canonicalLocalServer?: ServerConnection.Key) { + if (!canonicalLocalServer || canonicalLocalServer === "local") return value + if (!isRecord(value)) return value + const projects = isRecord(value.projects) ? value.projects : undefined + const lastProject = isRecord(value.lastProject) ? value.lastProject : undefined + const previousProjects = projects?.[canonicalLocalServer] + const previousLastProject = lastProject?.[canonicalLocalServer] + if (!Array.isArray(previousProjects) && typeof previousLastProject !== "string") return value + + const next = { ...value } + if (projects && Array.isArray(previousProjects)) { + const local = Array.isArray(projects.local) ? projects.local : [] + const worktrees = new Set( + local.flatMap((project) => (isRecord(project) && typeof project.worktree === "string" ? [project.worktree] : [])), + ) + const migrated = previousProjects.filter((project) => { + if (!isRecord(project) || typeof project.worktree !== "string") return true + if (worktrees.has(project.worktree)) return false + worktrees.add(project.worktree) + return true + }) + const nextProjects: Record = { ...projects, local: [...local, ...migrated] } + delete nextProjects[canonicalLocalServer] + next.projects = nextProjects + } + if (lastProject && typeof previousLastProject === "string") { + const nextLastProject = { ...lastProject } + if (typeof nextLastProject.local !== "string") nextLastProject.local = previousLastProject + delete nextLastProject[canonicalLocalServer] + next.lastProject = nextLastProject + } + return next +} + +export function createServerProjects(input: { + scope: Accessor + store: Store + setStore: SetStoreFunction +}) { + const setStore = input.setStore as unknown as SetStoreFunction + const current = () => input.store.projects[input.scope()] ?? [] + const currentClosed = () => input.store.recentlyClosed?.[input.scope()] ?? [] + const remove = (directory: string) => { + setStore( + "projects", + input.scope(), + current().filter((project) => project.worktree !== directory), + ) + } + return { + list: current, + recentlyClosed: currentClosed, + remove, + open(directory: string) { + const scope = input.scope() + const key = pathKey(directory) + const closed = currentClosed() + if (closed.some((worktree) => pathKey(worktree) === key)) { + setStore( + "recentlyClosed", + scope, + closed.filter((worktree) => pathKey(worktree) !== key), + ) + } + if (current().some((project) => project.worktree === directory)) return + setStore("projects", scope, [{ worktree: directory, expanded: true }, ...current()]) + }, + // User-initiated close: removes the project and records it in recently closed. + // Internal, non-user removals (e.g. sandbox/worktree normalization) should use remove(). + close(directory: string) { + remove(directory) + const key = pathKey(directory) + const closed = [directory, ...currentClosed().filter((worktree) => pathKey(worktree) !== key)].slice( + 0, + RECENTLY_CLOSED_HISTORY_LIMIT, + ) + setStore("recentlyClosed", input.scope(), closed) + }, + expand(directory: string) { + const index = current().findIndex((project) => project.worktree === directory) + if (index !== -1) setStore("projects", input.scope(), index, "expanded", true) + }, + collapse(directory: string) { + const index = current().findIndex((project) => project.worktree === directory) + if (index !== -1) setStore("projects", input.scope(), index, "expanded", false) + }, + move(directory: string, toIndex: number) { + const fromIndex = current().findIndex((project) => project.worktree === directory) + if (fromIndex === -1 || fromIndex === toIndex) return + const next = [...current()] + const [item] = next.splice(fromIndex, 1) + next.splice(toIndex, 0, item) + setStore("projects", input.scope(), next) + }, + last() { + return input.store.lastProject[input.scope()] + }, + touch(directory: string) { + setStore("lastProject", input.scope(), directory) + }, + } +} + +export function resolveServerList(input: { + props?: Array + stored: StoredServer[] +}): Array { + const deduped = new Map( + input.props?.map((v) => [ServerConnection.key(v), v]) ?? [], + ) + + for (const value of input.stored) { + const conn: ServerConnection.Http = + typeof value === "string" + ? { + type: "http" as const, + http: { url: value }, + } + : "http" in value + ? value + : { type: "http", http: value } + const key = ServerConnection.key(conn) + + const existing = deduped.get(key) + if (existing) + deduped.set(key, { + ...existing, + ...conn, + http: { ...existing.http, ...conn.http }, + }) + else deduped.set(key, conn) + } + + return [...deduped.values()] +} + +export namespace ServerConnection { + type Base = { displayName?: string; label?: string } + + export type HttpBase = { + url: string + username?: string + password?: string + } + + // Regular web connections + export type Http = { + type: "http" + http: HttpBase + authToken?: boolean + } & Base + + export type Sidecar = { + type: "sidecar" + http: HttpBase + } & ( + | // Regular desktop server + { variant: "base" } + // WSL server (windows only) + | { + variant: "wsl" + distro: string + } + ) & + Base + + // Remote server desktop can SSH into + export type Ssh = { + type: "ssh" + host: string + // SSH client exposes an HTTP server for the app to use as a proxy + http: HttpBase + } & Base + + export type Any = + | Http + // All these are desktop-only + | (Sidecar | Ssh) + + export const key = (conn: Any): Key => { + switch (conn.type) { + case "http": + return Key.make(conn.http.url) + case "sidecar": { + if (conn.variant === "wsl") return Key.make(`wsl:${conn.distro}`) + return Key.make("sidecar") + } + case "ssh": + return Key.make(`ssh:${conn.host}`) + } + } + + export type Key = string & { _brand: "Key" } + export const Key = { make: (v: string) => v as Key } + + export const builtin = (conn: Any) => conn.type === "sidecar" && conn.variant === "base" + export const local = (conn?: Any) => + !!conn && (builtin(conn) || (conn.type === "http" && isLocalHost(conn.http.url) === "local")) +} + +export function nextServerAfterRemoval( + servers: ServerConnection.Any[], + removed: ServerConnection.Key, + fallback: ServerConnection.Key, +) { + const remaining = servers.filter((server) => ServerConnection.key(server) !== removed) + const next = remaining.find((server) => ServerConnection.key(server) === fallback) ?? remaining[0] + return next ? ServerConnection.key(next) : fallback +} + +export const { use: useServer, provider: ServerProvider } = createSimpleContext({ + name: "Server", + gate: true, + init: (props: { + defaultServer: ServerConnection.Key + canonicalLocalServer?: ServerConnection.Key + servers?: Array + }) => { + const [store, setStore, _, ready] = persisted( + { + ...Persist.global("server", ["server.v3"]), + migrate: (value) => migrateCanonicalLocalServerState(value, props.canonicalLocalServer), + }, + createStore({ + list: [] as StoredServer[], + projects: {} as Record, + lastProject: {} as Record, + recentlyClosed: {} as Record, + }), + ) + + const url = (x: StoredServer) => (typeof x === "string" ? x : "type" in x ? x.http.url : x.url) + + const allServers = createMemo((): Array => { + return resolveServerList({ stored: store.list, props: props.servers }) + }) + + const [state, setState] = createStore({ + active: props.defaultServer, + }) + + function setActive(input: ServerConnection.Key) { + if (state.active !== input) setState("active", input) + } + + function add(input: ServerConnection.Http) { + const url_ = normalizeServerUrl(input.http.url) + if (!url_) return + const conn: ServerConnection.Http = { ...input, authToken: undefined, http: { ...input.http, url: url_ } } + return batch(() => { + const existing = store.list.findIndex((x) => url(x) === url_) + if (existing !== -1) { + setStore("list", existing, conn) + } else { + setStore("list", store.list.length, conn) + } + setState("active", ServerConnection.key(conn)) + return conn + }) + } + + function remove(key: ServerConnection.Key) { + const next = nextServerAfterRemoval(allServers(), key, props.defaultServer) + const list = store.list.filter((x) => url(x) !== key) + batch(() => { + setStore("list", list) + if (state.active === key) setState("active", next) + }) + } + + const isReady = Object.assign( + createMemo(() => ready() && !!state.active), + { promise: ready.promise }, + ) + + const scope = (key = state.active) => ServerScope.fromServerKey(key, props.canonicalLocalServer) + const projects = createServerProjects({ scope, store, setStore }) + const projectStores = new Map>() + const projectsForServer = (key: ServerConnection.Key) => { + const existing = projectStores.get(key) + if (existing) return existing + const next = createServerProjects({ scope: () => scope(key), store, setStore }) + projectStores.set(key, next) + return next + } + const current: Accessor = createMemo( + () => allServers().find((s) => ServerConnection.key(s) === state.active) ?? allServers()[0], + ) + const isLocal = createMemo(() => ServerConnection.local(current())) + + return { + ready: isReady, + isLocal, + get key() { + return state.active + }, + get name() { + return serverName(current()) + }, + get list() { + return allServers() + }, + get current() { + return current() + }, + setActive, + add, + remove, + scope, + projects: { + ...projects, + forServer: projectsForServer, + }, + } + }, +}) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1a118b654479dc5e573350ddc739dcc3d12010c5 --- /dev/null +++ b/packages/app/src/context/settings.tsx @@ -0,0 +1,547 @@ +import { createStore, reconcile } from "solid-js/store" +import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { persisted } from "@/utils/persist" +import { usePlatform } from "@/context/platform" + +export interface NotificationSettings { + agent: boolean + permissions: boolean + errors: boolean +} + +export interface SoundSettings { + agentEnabled: boolean + agent: string + permissionsEnabled: boolean + permissions: string + errorsEnabled: boolean + errors: string +} + +export interface Settings { + general: { + autoSave: boolean + releaseNotes: boolean + followup: "queue" | "steer" + showFileTree: boolean + showNavigation: boolean + showSearch: boolean + showStatus: boolean + showTerminal: boolean + showReasoningSummaries: boolean + shellToolPartsExpanded: boolean + editToolPartsExpanded: boolean + showCustomAgents: boolean + mobileTitlebarPosition: "top" | "bottom" + newLayoutDesigns?: boolean + layoutTransitionEligible?: boolean + agentVisibilityInitialized?: boolean + newInterfaceNoticeDismissed?: boolean + shouldDisplayTabsToast?: boolean + } + appearance: { + fontSize: number + mono: string + sans: string + terminal: string + } + keybinds: Record + permissions: { + autoApprove: boolean + } + notifications: NotificationSettings + sounds: SoundSettings +} + +export const monoDefault = "System Mono" +export const sansDefault = "System Sans" +export const terminalDefault = "JetBrainsMono Nerd Font Mono" +const legacyNewLayoutDesignsDefault = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" +export const newLayoutDesignsDefault = true +// Existing users can switch layouts until local midnight on this date. Set new Date(YYYY, M-1, D) to show. +export const oldInterfaceSunset = new Date(2026, 8, 14) +const newLayoutDesignsUpgradeCutoff = "1.17.19" + +function compareVersions(a: string, b: string) { + const parse = (version: string) => { + const match = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/i.exec(version.trim()) + if (!match) return + return match.slice(1).map(Number) + } + const left = parse(a) + const right = parse(b) + if (!left || !right) return + const index = left.findIndex((part, index) => part !== right[index]) + return index === -1 ? 0 : left[index]! - right[index]! +} + +export function isAppUpgrade(previous: string | undefined, current: string | undefined) { + if (!previous || !current) return false + const comparison = compareVersions(current, previous) + return comparison !== undefined && comparison > 0 +} + +export function shouldDisplayTabsToast( + previous: string | undefined, + current: string | undefined, + existingInstall: boolean, +) { + return isAppUpgrade(previous, current) || (!previous && existingInstall) +} + +export function hasExistingWebState(settings: Promise | string | null, previousVersion: string | undefined) { + return settings !== null || previousVersion !== undefined +} + +export function initialAgentVisibility(initialized: boolean | undefined, existing: boolean, previousVersion?: string) { + if (initialized === true) return + return existing || previousVersion !== undefined +} + +export function shouldEnableNewLayout(previous: string | undefined, current: string | undefined) { + if (!current) return false + const currentComparison = compareVersions(current, newLayoutDesignsUpgradeCutoff) + if (!previous) return currentComparison !== undefined && currentComparison > 0 + if (!isAppUpgrade(previous, current)) return false + const previousComparison = compareVersions(previous, newLayoutDesignsUpgradeCutoff) + return ( + previousComparison !== undefined && + currentComparison !== undefined && + previousComparison <= 0 && + currentComparison > 0 + ) +} + +export function layoutTransitionState(scheduled: boolean, eligible: boolean, retired: boolean, dismissed: boolean) { + return { + available: scheduled && eligible && !retired, + notice: scheduled && eligible && retired && !dismissed, + } +} + +export const maximumSunsetTimeout = 2_147_483_647 + +export function nextSunsetCheckDelay(sunset: number, now: number) { + return Math.min(Math.max(0, sunset - now), maximumSunsetTimeout) +} + +export function resolveNewLayoutDesigns(retired: boolean, preference: boolean | undefined, fallback = true) { + if (retired) return true + return preference ?? fallback +} + +const monoFallback = + 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' +const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' +const terminalFallback = + '"JetBrainsMono Nerd Font Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' + +const monoBase = monoFallback +const sansBase = sansFallback +const terminalBase = terminalFallback + +function input(font: string | undefined) { + return font ?? "" +} + +function family(font: string) { + if (/^[\w-]+$/.test(font)) return font + return `"${font.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` +} + +function stack(font: string | undefined, base: string) { + const value = font?.trim() ?? "" + if (!value) return base + return `${family(value)}, ${base}` +} + +export function monoInput(font: string | undefined) { + return input(font) +} + +export function sansInput(font: string | undefined) { + return input(font) +} + +export function monoFontFamily(font: string | undefined) { + return stack(font, monoBase) +} + +export function sansFontFamily(font: string | undefined) { + return stack(font, sansBase) +} + +export function terminalInput(font: string | undefined) { + return input(font) +} + +export function terminalFontFamily(font: string | undefined) { + return stack(font, terminalBase) +} + +const defaultSettings: Settings = { + general: { + autoSave: true, + releaseNotes: true, + followup: "steer", + showFileTree: false, + showNavigation: false, + showSearch: false, + showStatus: false, + showTerminal: false, + showReasoningSummaries: false, + shellToolPartsExpanded: false, + editToolPartsExpanded: false, + showCustomAgents: false, + mobileTitlebarPosition: "top", + }, + appearance: { + fontSize: 14, + mono: "", + sans: "", + terminal: "", + }, + keybinds: {}, + permissions: { + autoApprove: false, + }, + notifications: { + agent: true, + permissions: true, + errors: false, + }, + sounds: { + agentEnabled: true, + agent: "staplebops-01", + permissionsEnabled: true, + permissions: "staplebops-02", + errorsEnabled: true, + errors: "nope-03", + }, +} + +function withFallback(read: () => T | undefined, fallback: T) { + return createMemo(() => read() ?? fallback) +} + +export const { use: useSettings, provider: SettingsProvider } = createSimpleContext({ + name: "Settings", + gate: false, + init: () => { + const platform = usePlatform() + const [store, setStore, settingsInit, ready] = persisted("settings.v3", createStore(defaultSettings)) + const [launch, setLaunch, , launchReady] = persisted( + "app-version.v1", + createStore<{ version?: string }>({ version: undefined }), + ) + const [launchState, setLaunchState] = createStore({ + classified: false, + migrationApplied: false, + previous: undefined as string | undefined, + }) + const showFileTree = withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree) + const showSearch = withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch) + const showStatus = withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus) + const showCustomAgents = withFallback( + () => store.general?.showCustomAgents, + defaultSettings.general.showCustomAgents, + ) + const sunset = oldInterfaceSunset + const [oldInterfaceRetired, setOldInterfaceRetired] = createSignal(sunset ? Date.now() >= sunset.getTime() : false) + const layoutTransitionClassified = createMemo(() => typeof store.general?.layoutTransitionEligible === "boolean") + const layoutTransitionEligible = withFallback(() => store.general?.layoutTransitionEligible, false) + const newInterfaceNoticeDismissed = withFallback(() => store.general?.newInterfaceNoticeDismissed, false) + const layoutUpgrade = createMemo(() => + launchState.classified && !launchState.migrationApplied + ? shouldEnableNewLayout(launchState.previous, platform.version) + : false, + ) + const layoutTransition = createMemo(() => + layoutTransitionState(!!sunset, layoutTransitionEligible(), oldInterfaceRetired(), newInterfaceNoticeDismissed()), + ) + const newLayoutDesigns = createMemo(() => { + if (layoutUpgrade()) return true + if (!ready() && !oldInterfaceRetired()) return legacyNewLayoutDesignsDefault + if (!layoutTransitionClassified()) { + return resolveNewLayoutDesigns( + oldInterfaceRetired(), + store.general?.newLayoutDesigns, + legacyNewLayoutDesignsDefault, + ) + } + return resolveNewLayoutDesigns( + oldInterfaceRetired(), + store.general?.newLayoutDesigns, + layoutTransitionEligible() ? legacyNewLayoutDesignsDefault : newLayoutDesignsDefault, + ) + }) + const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference()) + const initializeAgentVisibility = (existing: boolean) => { + const initial = initialAgentVisibility(store.general?.agentVisibilityInitialized, existing, launchState.previous) + if (initial === undefined) return + batch(() => { + setStore("general", "showCustomAgents", initial) + setStore("general", "agentVisibilityInitialized", true) + }) + } + + if (sunset && !oldInterfaceRetired()) { + const timeout = { current: undefined as ReturnType | undefined } + const checkSunset = () => { + if (Date.now() >= sunset.getTime()) { + setOldInterfaceRetired(true) + return + } + timeout.current = setTimeout(checkSunset, nextSunsetCheckDelay(sunset.getTime(), Date.now())) + } + checkSunset() + onCleanup(() => { + if (timeout.current !== undefined) clearTimeout(timeout.current) + }) + } + + createEffect(() => { + if (!launchReady() || launchState.classified) return + setLaunchState({ + classified: true, + previous: launch.version, + }) + if (!platform.version || launch.version === platform.version) return + setLaunch("version", platform.version) + }) + + createEffect(() => { + if (!ready() || !launchState.classified || platform.platform !== "web") return + const existing = hasExistingWebState(settingsInit, launchState.previous) + if (!layoutTransitionClassified()) setStore("general", "layoutTransitionEligible", existing) + initializeAgentVisibility(existing) + }) + + createEffect(() => { + if (!ready() || !launchState.classified || launchState.migrationApplied) return + if (layoutUpgrade() && store.general?.newLayoutDesigns !== true) { + setStore("general", "newLayoutDesigns", true) + } + setLaunchState("migrationApplied", true) + }) + + createEffect(() => { + if (!ready() || !launchState.classified) return + if (typeof store.general?.shouldDisplayTabsToast === "boolean") return + if (!launchState.previous && !layoutTransitionClassified()) return + setStore( + "general", + "shouldDisplayTabsToast", + shouldDisplayTabsToast(launchState.previous, platform.version, layoutTransitionEligible()), + ) + }) + + createEffect(() => { + if (!ready() || !oldInterfaceRetired()) return + if (store.general?.newLayoutDesigns === true) return + setStore("general", "newLayoutDesigns", true) + }) + + createEffect(() => { + if (typeof document === "undefined") return + const root = document.documentElement + root.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.mono)) + root.style.setProperty("--font-family-sans", sansFontFamily(store.appearance?.sans)) + }) + + createEffect(() => { + if (store.general?.followup !== "queue") return + setStore("general", "followup", "steer") + }) + + return { + ready, + get current() { + return store + }, + general: { + autoSave: withFallback(() => store.general?.autoSave, defaultSettings.general.autoSave), + setAutoSave(value: boolean) { + setStore("general", "autoSave", value) + }, + releaseNotes: withFallback(() => store.general?.releaseNotes, defaultSettings.general.releaseNotes), + setReleaseNotes(value: boolean) { + setStore("general", "releaseNotes", value) + }, + followup: withFallback( + () => (store.general?.followup === "queue" ? "steer" : store.general?.followup), + defaultSettings.general.followup, + ), + setFollowup(value: "queue" | "steer") { + setStore("general", "followup", value === "queue" ? "steer" : value) + }, + showFileTree, + setShowFileTree(value: boolean) { + setStore("general", "showFileTree", value) + }, + showNavigation: withFallback(() => store.general?.showNavigation, defaultSettings.general.showNavigation), + setShowNavigation(value: boolean) { + setStore("general", "showNavigation", value) + }, + showSearch, + setShowSearch(value: boolean) { + setStore("general", "showSearch", value) + }, + showStatus, + setShowStatus(value: boolean) { + setStore("general", "showStatus", value) + }, + showTerminal: withFallback(() => store.general?.showTerminal, defaultSettings.general.showTerminal), + setShowTerminal(value: boolean) { + setStore("general", "showTerminal", value) + }, + showReasoningSummaries: withFallback( + () => store.general?.showReasoningSummaries, + defaultSettings.general.showReasoningSummaries, + ), + setShowReasoningSummaries(value: boolean) { + setStore("general", "showReasoningSummaries", value) + }, + shellToolPartsExpanded: withFallback( + () => store.general?.shellToolPartsExpanded, + defaultSettings.general.shellToolPartsExpanded, + ), + setShellToolPartsExpanded(value: boolean) { + setStore("general", "shellToolPartsExpanded", value) + }, + editToolPartsExpanded: withFallback( + () => store.general?.editToolPartsExpanded, + defaultSettings.general.editToolPartsExpanded, + ), + setEditToolPartsExpanded(value: boolean) { + setStore("general", "editToolPartsExpanded", value) + }, + showCustomAgents, + setShowCustomAgents(value: boolean) { + setStore("general", "showCustomAgents", value) + }, + mobileTitlebarPosition: withFallback( + () => store.general?.mobileTitlebarPosition, + defaultSettings.general.mobileTitlebarPosition, + ), + setMobileTitlebarPosition(value: "top" | "bottom") { + setStore("general", "mobileTitlebarPosition", value) + }, + newLayoutDesigns, + setNewLayoutDesigns(value: boolean) { + const next = oldInterfaceRetired() ? true : value + if (newLayoutDesigns() === next) return + setStore("general", "newLayoutDesigns", next) + if (typeof window !== "undefined") setTimeout(() => window.location.reload()) + }, + layoutTransitionClassified, + setOldLayoutEligible(eligible: boolean) { + const current = store.general?.layoutTransitionEligible + if (typeof current === "boolean") return + setStore("general", "layoutTransitionEligible", eligible) + }, + initializeAgentVisibility, + layoutTransitionAvailable: createMemo(() => ready() && layoutTransition().available), + newInterfaceNoticeVisible: createMemo(() => ready() && layoutTransition().notice), + dismissNewInterfaceNotice() { + setStore("general", "newInterfaceNoticeDismissed", true) + }, + shouldDisplayTabsToast: withFallback(() => store.general?.shouldDisplayTabsToast, false), + dismissTabsToast() { + setStore("general", "shouldDisplayTabsToast", false) + }, + }, + visibility: { + fileTree: visible(showFileTree), + search: visible(showSearch), + status: visible(showStatus), + customAgents: visible(showCustomAgents), + }, + appearance: { + fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize), + setFontSize(value: number) { + setStore("appearance", "fontSize", value) + }, + font: withFallback(() => store.appearance?.mono, defaultSettings.appearance.mono), + setFont(value: string) { + setStore("appearance", "mono", value.trim() ? value : "") + }, + uiFont: withFallback(() => store.appearance?.sans, defaultSettings.appearance.sans), + setUIFont(value: string) { + setStore("appearance", "sans", value.trim() ? value : "") + }, + terminalFont: withFallback(() => store.appearance?.terminal, defaultSettings.appearance.terminal), + setTerminalFont(value: string) { + setStore("appearance", "terminal", value.trim() ? value : "") + }, + }, + keybinds: { + get: (action: string) => store.keybinds?.[action], + set(action: string, keybind: string) { + setStore("keybinds", action, keybind) + }, + reset(action: string) { + setStore("keybinds", (current) => { + if (!Object.prototype.hasOwnProperty.call(current, action)) return current + const next = { ...current } + delete next[action] + return next + }) + }, + resetAll() { + setStore("keybinds", reconcile({})) + }, + }, + permissions: { + autoApprove: withFallback(() => store.permissions?.autoApprove, defaultSettings.permissions.autoApprove), + setAutoApprove(value: boolean) { + setStore("permissions", "autoApprove", value) + }, + }, + notifications: { + agent: withFallback(() => store.notifications?.agent, defaultSettings.notifications.agent), + setAgent(value: boolean) { + setStore("notifications", "agent", value) + }, + permissions: withFallback(() => store.notifications?.permissions, defaultSettings.notifications.permissions), + setPermissions(value: boolean) { + setStore("notifications", "permissions", value) + }, + errors: withFallback(() => store.notifications?.errors, defaultSettings.notifications.errors), + setErrors(value: boolean) { + setStore("notifications", "errors", value) + }, + }, + sounds: { + agentEnabled: withFallback(() => store.sounds?.agentEnabled, defaultSettings.sounds.agentEnabled), + setAgentEnabled(value: boolean) { + setStore("sounds", "agentEnabled", value) + }, + agent: withFallback(() => store.sounds?.agent, defaultSettings.sounds.agent), + setAgent(value: string) { + setStore("sounds", "agent", value) + }, + permissionsEnabled: withFallback( + () => store.sounds?.permissionsEnabled, + defaultSettings.sounds.permissionsEnabled, + ), + setPermissionsEnabled(value: boolean) { + setStore("sounds", "permissionsEnabled", value) + }, + permissions: withFallback(() => store.sounds?.permissions, defaultSettings.sounds.permissions), + setPermissions(value: string) { + setStore("sounds", "permissions", value) + }, + errorsEnabled: withFallback(() => store.sounds?.errorsEnabled, defaultSettings.sounds.errorsEnabled), + setErrorsEnabled(value: boolean) { + setStore("sounds", "errorsEnabled", value) + }, + errors: withFallback(() => store.sounds?.errors, defaultSettings.sounds.errors), + setErrors(value: string) { + setStore("sounds", "errors", value) + }, + }, + } + }, +}) diff --git a/packages/app/src/context/tab-migration.ts b/packages/app/src/context/tab-migration.ts new file mode 100644 index 0000000000000000000000000000000000000000..04c1cb6804ab5dc4422024e11072e67f007ec07f --- /dev/null +++ b/packages/app/src/context/tab-migration.ts @@ -0,0 +1,23 @@ +import type { ServerConnection } from "./server" +import type { Tab } from "./tabs" + +export function migrateTabs(value: unknown, fallback: ServerConnection.Key): Tab[] { + if (!Array.isArray(value)) return [] + return value.flatMap((tab) => { + if (!tab || typeof tab !== "object") return [] + if ("server" in tab && typeof tab.server !== "string") return [] + const server = ("server" in tab ? tab.server : fallback) as ServerConnection.Key + if (tab.type === "session" && typeof tab.sessionId === "string") { + return [{ type: tab.type, server, sessionId: tab.sessionId }] + } + if ( + tab.type === "draft" && + typeof tab.draftID === "string" && + typeof tab.directory === "string" && + (tab.worktree === undefined || typeof tab.worktree === "string") + ) { + return [{ type: tab.type, server, draftID: tab.draftID, directory: tab.directory, worktree: tab.worktree }] + } + return [] + }) +} diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b6608e868112aa673b01aaa482c72861c110fd2c --- /dev/null +++ b/packages/app/src/context/tabs.tsx @@ -0,0 +1,385 @@ +import type { Session } from "@opencode-ai/sdk/v2/client" +import { createSimpleContext } from "@opencode-ai/ui/context" +import { createStore, produce } from "solid-js/store" +import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/utils/persist" +import { ServerConnection, useServer } from "./server" +import { createEffect, getOwner, onCleanup, startTransition } from "solid-js" +import { useLocation, useNavigate, useParams } from "@solidjs/router" +import { usePlatform } from "./platform" +import { uuid } from "@/utils/uuid" +import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events" +import { sessionHref } from "@/utils/session-route" +import { createTabMemory } from "./tab-memory" +import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" +import { createDraftPromptSession, type PromptModel } from "./prompt-state" +import { migrateTabs } from "./tab-migration" + +export type SessionTab = { + type: "session" + server: ServerConnection.Key + sessionId: string +} + +export type DraftTab = { + type: "draft" + draftID: string + server: ServerConnection.Key + directory: string + worktree?: string +} + +export type Tab = SessionTab | DraftTab + +export type TabInfo = { + title?: string + directory?: string +} + +type RecentTab = { + key?: string +} + +export const draftHref = (draftID: string) => `/new-session?draftId=${encodeURIComponent(draftID)}` + +export const tabHref = (tab: Tab) => + tab.type === "draft" ? draftHref(tab.draftID) : sessionHref(tab.server, tab.sessionId) + +export const tabKey = (tab: Tab) => (tab.type === "draft" ? `draft:${tab.draftID}` : `${tab.server}\n${tabHref(tab)}`) + +export function sessionHasOpenTab(tabs: Tab[], server: ServerConnection.Key, session: Session) { + return tabs.some((tab) => tab.type === "session" && tab.server === server && tab.sessionId === session.id) +} + +export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ + name: "Tabs", + gate: false, + init: () => { + const server = useServer() + const platform = usePlatform() + const fallback = server.key + const [store, setStore, _, ready] = persisted( + { + ...Persist.window("tabs"), + migrate: (value: unknown) => migrateTabs(value, fallback), + }, + createStore([]), + ) + const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore({})) + const [info, setInfo] = persisted(Persist.window("tabs.info"), createStore>({})) + const [closed, setClosed, , closedReady] = persisted(Persist.window("tabs.closed"), createStore([])) + + const params = useParams() + const navigate = useNavigate() + const location = useLocation() + const memory = createTabMemory(getOwner()) + + const closing = new Set() + let recentWrite = 0 + let recentValue: string | undefined + + const recentKey = () => (recentWrite ? recentValue : recent.key) + + const setRecentKey = (key: string | undefined) => { + const write = ++recentWrite + recentValue = key + if (recentReady()) { + setRecent("key", key) + return + } + void recentReady.promise?.then(() => { + if (write === recentWrite) setRecent("key", key) + }) + } + + const updateClosed = (update: (stack: ClosedTab[]) => ClosedTab[]) => { + const apply = () => setClosed((stack) => update(stack)) + if (closedReady()) { + apply() + return + } + void closedReady.promise?.then(apply) + } + + const removeDraftPersisted = (draftID: string) => { + for (const key of draftPersistedKeys()) { + const target = Persist.draft(draftID, key) + removePersisted(key === "prompt" ? Persist.prompt(target) : target, platform) + } + } + + const removeInfo = (key: string) => { + if (!info[key]) return + setInfo( + produce((draft) => { + delete draft[key] + }), + ) + } + + onCleanup(memory.dispose) + + createEffect(() => { + if (!ready() || !recentReady()) return + const servers = new Set(server.list.map(ServerConnection.key)) + const next = store.filter((tab) => servers.has(tab.server)) + if (next.length !== store.length) { + for (const tab of store) { + if (!servers.has(tab.server)) { + const key = tabKey(tab) + memory.remove(key) + removeInfo(key) + } + } + setStore(() => next) + } + if (recent.key && !next.some((tab) => tabKey(tab) === recent.key)) setRecentKey(undefined) + const keys = new Set(next.map(tabKey)) + for (const key of Object.keys(info)) { + if (!keys.has(key)) removeInfo(key) + } + }) + + createEffect(() => { + if (!closedReady()) return + const servers = new Set(server.list.map(ServerConnection.key)) + const next = closed.filter((entry) => servers.has(entry.tab.server)) + if (next.length !== closed.length) setClosed(() => next) + }) + + const navigateTab = (tab: Tab) => { + const href = tabHref(tab) + setRecentKey(tabKey(tab)) + navigate(href) + } + + const removeTab = (index: number) => { + const tab = store[index] + if (!tab) return + const key = tabKey(tab) + const draftID = tab.type === "draft" ? tab.draftID : undefined + const nextTab = nextTabAfterClose(store, index, recentKey() === key && location.pathname !== "/") + closing.add(key) + void startTransition(() => { + setStore( + produce((tabs) => { + tabs.splice(index, 1) + }), + ) + if (nextTab === null) { + setRecentKey(undefined) + navigate("/") + } + if (nextTab) navigateTab(nextTab) + }).finally(() => closing.delete(key)) + memory.remove(key) + removeInfo(key) + if (draftID) removeDraftPersisted(draftID) + } + + const actions = { + addSessionTab: (tab: Omit) => { + const next = { type: "session" as const, ...tab } + const existing = store.find((item) => tabKey(item) === tabKey(next)) + if (existing) return existing + void startTransition(() => { + setStore( + produce((tabs) => { + if (tabs.some((item) => tabKey(item) === tabKey(next))) return + tabs.push(next) + }), + ) + }) + return next + }, + reorder(keys: string[]) { + setStore( + produce((tabs) => { + const byKey = new Map(tabs.map((tab) => [tabKey(tab), tab])) + const next = keys.map((key) => byKey.get(key)).filter((tab): tab is Tab => !!tab) + if (next.length !== tabs.length) return + tabs.splice(0, tabs.length, ...next) + }), + ) + }, + draft(draftID: string) { + const tab = store.find((item) => item.type === "draft" && item.draftID === draftID) + if (!tab || tab.type !== "draft") throw new Error(`Draft not found: ${draftID}`) + return tab + }, + async newDraft(draft: Omit, prompt?: string, model?: PromptModel) { + const draftID = uuid() + const tab = { type: "draft" as const, draftID, ...draft } + memory.ensure(tabKey(tab), "prompt", () => createDraftPromptSession(draftID, { prompt, model })) + await startTransition(() => { + setStore( + produce((tabs) => { + tabs.push(tab) + }), + ) + navigate(draftHref(draftID)) + }) + return tab + }, + updateDraft(draftID: string, draft: Partial>) { + void startTransition(() => { + setStore( + (tab) => tab.type === "draft" && tab.draftID === draftID, + produce((tab) => Object.assign(tab, draft)), + ) + }) + }, + promoteDraft(draftID: string, session: Omit) { + // Keep the replacement and navigation atomic so /new-session never renders + // after its backing draft tab has been removed from the store. + const active = location.pathname === "/new-session" && location.query.draftId === draftID + const next = { type: "session" as const, ...session } + void startTransition(() => { + setStore( + produce((tabs) => { + const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID) + if (index !== -1) tabs[index] = next + }), + ) + if (recent.key === `draft:${draftID}`) setRecentKey(tabKey(next)) + if (active) navigateTab(next) + }) + memory.remove(`draft:${draftID}`) + removeDraftPersisted(draftID) + }, + removeTab, + // User-initiated close: records the tab so it can be reopened. + // Cleanup paths (missing sessions, archive, server removal) go through + // removeTab and friends directly and are not recorded. + closeTab(index: number) { + const tab = store[index] + if (!tab) return + if (tab.type === "session") updateClosed((stack) => pushClosedTab(stack, tab, index)) + removeTab(index) + }, + reopenClosedTab() { + if (!closedReady()) { + void closedReady.promise?.then(() => actions.reopenClosedTab()) + return + } + const result = takeClosedTab(closed, store) + if (result.stack.length === closed.length) return + setClosed(() => result.stack) + const entry = result.entry + if (!entry) return + const index = Math.min(entry.index, store.length) + void startTransition(() => { + setStore( + produce((tabs) => { + if (tabs.some((item) => tabKey(item) === tabKey(entry.tab))) return + tabs.splice(index, 0, entry.tab) + }), + ) + navigateTab(entry.tab) + }) + }, + removeSessionTab(input: Omit) { + updateClosed((stack) => removeClosedTabs(stack, input.server, [input.sessionId])) + const index = store.findIndex( + (tab) => tab.type === "session" && tab.server === input.server && tab.sessionId === input.sessionId, + ) + if (index !== -1) removeTab(index) + }, + removeServer(key: ServerConnection.Key) { + updateClosed((stack) => stack.filter((entry) => entry.tab.server !== key)) + const drafts = store.flatMap((tab) => (tab.type === "draft" && tab.server === key ? [tab.draftID] : [])) + const removed = store.filter((tab) => tab.server === key).map(tabKey) + setStore((tabs) => tabs.filter((tab) => tab.server !== key)) + for (const key of removed) memory.remove(key) + for (const key of removed) removeInfo(key) + if (recent.key && removed.includes(recent.key)) setRecentKey(undefined) + for (const draftID of drafts) removeDraftPersisted(draftID) + if (server.key === key) navigate("/") + }, + removeSessions: (input: SessionTabsRemovedDetail) => { + const targetServer = input.server ?? server.key + updateClosed((stack) => removeClosedTabs(stack, targetServer, input.sessionIDs)) + const removed = store + .filter( + (tab) => tab.type === "session" && tab.server === targetServer && input.sessionIDs.includes(tab.sessionId), + ) + .map(tabKey) + void startTransition(() => { + setStore( + produce((tabs) => { + const sessionIDs = new Set(input.sessionIDs) + const currentHref = + targetServer === server.key && params.dir && params.id + ? tabHref({ + type: "session", + server: targetServer, + sessionId: params.id, + }) + : undefined + const currentIndex = currentHref + ? tabs.findIndex( + (tab) => tab.type === "session" && tab.server === targetServer && tabHref(tab) === currentHref, + ) + : -1 + const currentTab = tabs[currentIndex] + const removedCurrent = + currentTab?.type === "session" && + currentTab.server === targetServer && + sessionIDs.has(currentTab.sessionId) + + for (let i = tabs.length - 1; i >= 0; i--) { + const tab = tabs[i] + if (!tab || tab.type !== "session") continue + if (tab.server !== targetServer) continue + if (!sessionIDs.has(tab.sessionId)) continue + tabs.splice(i, 1) + } + + if (!removedCurrent) return + const nextTab = + tabs.slice(currentIndex).find((tab) => tab.type === "session") ?? + tabs.slice(0, currentIndex).findLast((tab) => tab.type === "session") + if (nextTab) navigateTab(nextTab) + else navigate("/") + }), + ) + if (recent.key && removed.includes(recent.key)) setRecentKey(undefined) + }) + for (const key of removed) memory.remove(key) + for (const key of removed) removeInfo(key) + }, + rememberSessionInfo(tab: SessionTab, session: Session) { + const key = tabKey(tab) + const next = { title: session.title, directory: session.directory } + const current = info[key] + if (current?.title === next.title && current.directory === next.directory) return + setInfo(key, next) + }, + select: navigateTab, + remember(tab: Tab) { + const key = tabKey(tab) + if (recentKey() !== key) setRecentKey(key) + }, + toggleHome(input: { home: boolean; current?: Tab }) { + if (input.home) { + const tab = store.find((tab) => tabKey(tab) === recentKey()) + if (tab) navigateTab(tab) + return + } + if (input.current) { + setRecentKey(tabKey(input.current)) + navigate("/") + return + } + navigate("/") + }, + state(tab: Tab, name: string, init: () => T) { + return memory.ensure(tabKey(tab), name, init) + }, + stateValue(tab: Tab, name: string) { + return memory.get(tabKey(tab), name) + }, + } + + return { ...actions, store, info, ready, recentReady } + }, +}) diff --git a/packages/app/src/context/terminal-title.ts b/packages/app/src/context/terminal-title.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8b18f42119f5b529b29525d8d4e35b5ad77f9f6 --- /dev/null +++ b/packages/app/src/context/terminal-title.ts @@ -0,0 +1,24 @@ +const template = "Terminal {{number}}" + +const numbered = [ + template, + "محطة طرفية {{number}}", + "Терминал {{number}}", + "ターミナル {{number}}", + "터미널 {{number}}", + "เทอร์มินัล {{number}}", + "终端 {{number}}", + "終端機 {{number}}", +] + +export function defaultTitle(number: number) { + return template.replace("{{number}}", String(number)) +} + +export function isDefaultTitle(title: string, number: number) { + return numbered.some((text) => title === text.replace("{{number}}", String(number))) +} + +export function titleNumber(title: string, max: number) { + return Array.from({ length: max }, (_, idx) => idx + 1).find((number) => isDefaultTitle(title, number)) +} diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..25ef35dd413876c8cf8cf50dd1865ec981e817c9 --- /dev/null +++ b/packages/app/src/context/terminal.test.ts @@ -0,0 +1,91 @@ +import { beforeAll, describe, expect, mock, test } from "bun:test" +import { ServerScope } from "@/utils/server-scope" + +let getWorkspaceTerminalCacheKey: typeof import("./terminal").getWorkspaceTerminalCacheKey +let getLegacyTerminalStorageKeys: (dir: string, legacySessionID?: string) => string[] +let migrateTerminalState: (value: unknown) => unknown + +beforeAll(async () => { + mock.module("@solidjs/router", () => ({ + useNavigate: () => () => undefined, + useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], + })) + mock.module("@opencode-ai/ui/context", () => ({ + createSimpleContext: () => ({ + use: () => undefined, + provider: () => undefined, + }), + })) + const mod = await import("./terminal") + getWorkspaceTerminalCacheKey = mod.getWorkspaceTerminalCacheKey + getLegacyTerminalStorageKeys = mod.getLegacyTerminalStorageKeys + migrateTerminalState = mod.migrateTerminalState +}) + +describe("getWorkspaceTerminalCacheKey", () => { + test("uses workspace-only directory cache key", () => { + expect(String(getWorkspaceTerminalCacheKey("/repo"))).toBe("local\u0000/repo\u0000__workspace__") + }) + + test("can include a server scope", () => { + expect(String(getWorkspaceTerminalCacheKey("/repo", "ssh:debian" as ServerScope))).toBe( + "ssh:debian\u0000/repo\u0000__workspace__", + ) + }) +}) + +describe("getLegacyTerminalStorageKeys", () => { + test("keeps workspace storage path when no legacy session id", () => { + expect(getLegacyTerminalStorageKeys("/repo")).toEqual(["/repo/terminal.v1"]) + }) + + test("includes legacy session path before workspace path", () => { + expect(getLegacyTerminalStorageKeys("/repo", "session-123")).toEqual([ + "/repo/terminal/session-123.v1", + "/repo/terminal.v1", + ]) + }) +}) + +describe("migrateTerminalState", () => { + test("drops invalid terminals and restores a valid active terminal", () => { + expect( + migrateTerminalState({ + active: "missing", + all: [ + null, + { id: "one", title: "Terminal 2" }, + { id: "one", title: "duplicate", titleNumber: 9 }, + { id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 }, + { title: "no-id" }, + ], + }), + ).toEqual({ + active: "one", + all: [ + { id: "one", title: "Terminal 2", titleNumber: 2 }, + { id: "two", title: "logs", titleNumber: 4, rows: 24, cols: 80 }, + ], + }) + }) + + test("keeps a valid active id", () => { + expect( + migrateTerminalState({ + active: "two", + all: [ + { id: "one", title: "Terminal 1" }, + { id: "two", title: "shell", titleNumber: 7 }, + ], + }), + ).toEqual({ + active: "two", + all: [ + { id: "one", title: "Terminal 1", titleNumber: 1 }, + { id: "two", title: "shell", titleNumber: 7 }, + ], + }) + }) +}) diff --git a/packages/app/src/custom-elements.d.ts b/packages/app/src/custom-elements.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..49ec4449fa209d7b5e60d8d4b735f9350dfc066f --- /dev/null +++ b/packages/app/src/custom-elements.d.ts @@ -0,0 +1,17 @@ +import { DIFFS_TAG_NAME } from "@pierre/diffs" + +/** + * TypeScript declaration for the custom element. + * This tells TypeScript that is a valid JSX element in SolidJS. + * Required for using the @pierre/diffs web component in .tsx files. + */ + +declare module "solid-js" { + namespace JSX { + interface IntrinsicElements { + [DIFFS_TAG_NAME]: HTMLAttributes + } + } +} + +export {} diff --git a/packages/app/src/desktop-menu.test.ts b/packages/app/src/desktop-menu.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..79dbe89adbd86d84f4a734e7d6705e2aaf8dcb0c --- /dev/null +++ b/packages/app/src/desktop-menu.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "bun:test" +import { DESKTOP_MENU } from "./desktop-menu" + +describe("desktop menu", () => { + test("exports logs through the desktop command registry", () => { + const items = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter( + (item) => item.type === "item" && item.labelKey === "desktop.menu.exportLogs", + ) + + expect(items).toHaveLength(2) + expect(items.every((item) => item.type === "item" && item.command === "logs.export" && !item.action)).toBe(true) + }) + + test("provides translated labels for role-backed entries", () => { + const windowMenu = DESKTOP_MENU.find((menu) => menu.role === "windowMenu") + const roleItems = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).filter( + (item) => item.type === "item" && item.role && item.labelKey, + ) + + expect(windowMenu?.labelKey).toBe("desktop.menu.window") + expect(roleItems.length).toBeGreaterThan(0) + }) +}) diff --git a/packages/app/src/desktop-menu.ts b/packages/app/src/desktop-menu.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbddbea169bbbb3aaf23cca328cb99e56b01f660 --- /dev/null +++ b/packages/app/src/desktop-menu.ts @@ -0,0 +1,302 @@ +import type { DesktopNativeKey } from "./i18n/desktop-native" + +export type DesktopMenuPlatform = "macos" | "windows" + +export type DesktopMenuAction = + | "app.checkForUpdates" + | "app.relaunch" + | "edit.undo" + | "edit.redo" + | "edit.cut" + | "edit.copy" + | "edit.paste" + | "edit.delete" + | "edit.selectAll" + | "view.reload" + | "view.toggleDevTools" + | "view.resetZoom" + | "view.zoomIn" + | "view.zoomOut" + | "view.toggleFullscreen" + | "window.new" + | "window.close" + | "window.minimize" + | "window.toggleMaximize" + +export type DesktopMenuRole = + | "about" + | "close" + | "copy" + | "cut" + | "hide" + | "hideOthers" + | "paste" + | "quit" + | "redo" + | "reload" + | "resetZoom" + | "selectAll" + | "toggleDevTools" + | "togglefullscreen" + | "undo" + | "unhide" + | "windowMenu" + | "zoomIn" + | "zoomOut" + +export type DesktopMenuItem = { + type: "item" + labelKey?: DesktopNativeKey + command?: string + action?: DesktopMenuAction + role?: DesktopMenuRole + href?: string + accelerator?: Partial> + enabled?: "updater" + platforms?: DesktopMenuPlatform[] +} + +export type DesktopMenuSeparator = { + type: "separator" + platforms?: DesktopMenuPlatform[] +} + +export type DesktopMenuEntry = DesktopMenuItem | DesktopMenuSeparator + +export type DesktopMenu = { + id: string + labelKey: DesktopNativeKey + role?: DesktopMenuRole + items?: DesktopMenuEntry[] + platforms?: DesktopMenuPlatform[] +} + +export const DESKTOP_MENU: DesktopMenu[] = [ + { + id: "app", + labelKey: "desktop.menu.app", + platforms: ["macos"], + items: [ + { type: "item", role: "about" }, + { + type: "item", + labelKey: "desktop.menu.checkForUpdates", + action: "app.checkForUpdates", + enabled: "updater", + }, + { type: "item", labelKey: "desktop.menu.settings", command: "settings.open", accelerator: { macos: "Cmd+," } }, + { type: "item", labelKey: "desktop.menu.reloadWebview", action: "view.reload" }, + { type: "item", labelKey: "desktop.menu.restart", action: "app.relaunch" }, + { type: "item", labelKey: "desktop.menu.exportLogs", command: "logs.export" }, + { type: "separator" }, + { type: "item", role: "hide" }, + { type: "item", role: "hideOthers" }, + { type: "item", role: "unhide" }, + { type: "separator" }, + { type: "item", role: "quit" }, + ], + }, + { + id: "file", + labelKey: "desktop.menu.file", + items: [ + { + type: "item", + labelKey: "desktop.menu.newSession", + command: "session.new", + accelerator: { macos: "Shift+Cmd+S" }, + }, + { + type: "item", + labelKey: "desktop.menu.openProject", + command: "project.open", + accelerator: { macos: "Cmd+O" }, + }, + { + type: "item", + labelKey: "desktop.menu.settings", + command: "settings.open", + accelerator: { windows: "Ctrl+," }, + platforms: ["windows"], + }, + { + type: "item", + labelKey: "desktop.menu.newWindow", + action: "window.new", + accelerator: { macos: "Cmd+Shift+N", windows: "Ctrl+Shift+N" }, + }, + { type: "separator" }, + { type: "item", labelKey: "desktop.menu.closeWindow", action: "window.close", role: "close" }, + ], + }, + { + id: "edit", + labelKey: "desktop.menu.edit", + items: [ + { + type: "item", + labelKey: "desktop.menu.undo", + action: "edit.undo", + role: "undo", + accelerator: { windows: "Ctrl+Z" }, + }, + { + type: "item", + labelKey: "desktop.menu.redo", + action: "edit.redo", + role: "redo", + accelerator: { windows: "Ctrl+Y" }, + }, + { type: "separator" }, + { + type: "item", + labelKey: "desktop.menu.cut", + action: "edit.cut", + role: "cut", + accelerator: { windows: "Ctrl+X" }, + }, + { + type: "item", + labelKey: "desktop.menu.copy", + action: "edit.copy", + role: "copy", + accelerator: { windows: "Ctrl+C" }, + }, + { + type: "item", + labelKey: "desktop.menu.paste", + action: "edit.paste", + role: "paste", + accelerator: { windows: "Ctrl+V" }, + }, + { type: "item", labelKey: "desktop.menu.delete", action: "edit.delete" }, + { + type: "item", + labelKey: "desktop.menu.selectAll", + action: "edit.selectAll", + role: "selectAll", + accelerator: { windows: "Ctrl+A" }, + }, + ], + }, + { + id: "view", + labelKey: "desktop.menu.view", + items: [ + { type: "item", labelKey: "desktop.menu.toggleSidebar", command: "sidebar.toggle" }, + { + type: "item", + labelKey: "desktop.menu.toggleTerminal", + command: "terminal.toggle", + accelerator: { macos: "Ctrl+`" }, + }, + { type: "item", labelKey: "desktop.menu.toggleFileTree", command: "fileTree.toggle" }, + { type: "separator" }, + { type: "item", labelKey: "desktop.menu.reload", action: "view.reload", role: "reload" }, + { + type: "item", + labelKey: "desktop.menu.toggleDeveloperTools", + action: "view.toggleDevTools", + role: "toggleDevTools", + }, + { type: "separator" }, + { + type: "item", + labelKey: "desktop.menu.actualSize", + action: "view.resetZoom", + role: "resetZoom", + accelerator: { windows: "Ctrl+0" }, + }, + { + type: "item", + labelKey: "desktop.menu.zoomIn", + action: "view.zoomIn", + role: "zoomIn", + accelerator: { windows: "Ctrl++" }, + }, + { + type: "item", + labelKey: "desktop.menu.zoomOut", + action: "view.zoomOut", + role: "zoomOut", + accelerator: { windows: "Ctrl+-" }, + }, + { type: "separator" }, + { + type: "item", + labelKey: "desktop.menu.toggleFullScreen", + action: "view.toggleFullscreen", + role: "togglefullscreen", + }, + ], + }, + { + id: "go", + labelKey: "desktop.menu.go", + items: [ + { type: "item", labelKey: "desktop.menu.back", command: "common.goBack", accelerator: { macos: "Cmd+[" } }, + { type: "item", labelKey: "desktop.menu.forward", command: "common.goForward", accelerator: { macos: "Cmd+]" } }, + { type: "separator" }, + { + type: "item", + labelKey: "desktop.menu.previousSession", + command: "session.previous", + accelerator: { macos: "Option+Up" }, + }, + { + type: "item", + labelKey: "desktop.menu.nextSession", + command: "session.next", + accelerator: { macos: "Option+Down" }, + }, + { type: "separator" }, + { + type: "item", + labelKey: "desktop.menu.previousProject", + command: "project.previous", + accelerator: { macos: "Cmd+Option+Up" }, + }, + { + type: "item", + labelKey: "desktop.menu.nextProject", + command: "project.next", + accelerator: { macos: "Cmd+Option+Down" }, + }, + ], + }, + { + id: "window", + labelKey: "desktop.menu.window", + role: "windowMenu", + items: [ + { type: "item", labelKey: "desktop.menu.minimize", action: "window.minimize" }, + { type: "item", labelKey: "desktop.menu.maximize", action: "window.toggleMaximize" }, + { type: "separator" }, + { type: "item", labelKey: "desktop.menu.closeWindow", action: "window.close" }, + ], + }, + { + id: "help", + labelKey: "desktop.menu.help", + items: [ + { type: "item", labelKey: "desktop.menu.documentation", href: "https://opencode.ai/docs" }, + { type: "item", labelKey: "desktop.menu.supportForum", href: "https://discord.com/invite/opencode" }, + { type: "item", labelKey: "desktop.menu.exportLogs", command: "logs.export" }, + { type: "separator" }, + { + type: "item", + labelKey: "desktop.menu.shareFeedback", + href: "https://github.com/anomalyco/opencode/issues/new?template=feature_request.yml", + }, + { + type: "item", + labelKey: "desktop.menu.reportBug", + href: "https://github.com/anomalyco/opencode/issues/new?template=bug_report.yml", + }, + ], + }, +] + +export function desktopMenuVisible(item: { platforms?: DesktopMenuPlatform[] }, platform: DesktopMenuPlatform) { + return !item.platforms || item.platforms.includes(platform) +} diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6e5f63fd4e954a2258785d0128b4532cd62ee751 --- /dev/null +++ b/packages/app/src/entry.tsx @@ -0,0 +1,180 @@ +// @refresh reload + +import * as Sentry from "@sentry/solid" +import { render } from "solid-js/web" +import { AppBaseProviders, AppInterface } from "@/app" +import { loadInitialLocale } from "@/context/language" +import { type Platform, PlatformProvider } from "@/context/platform" +import { createBrowserDraftStore } from "@/utils/draft-store" +import { dict as en } from "@/i18n/en" +import { dict as zh } from "@/i18n/zh" +import { authFromToken } from "@/utils/server" +import pkg from "../package.json" +import { ServerConnection } from "./context/server" + +const DEFAULT_SERVER_URL_KEY = "opencode.settings.dat:defaultServerUrl" + +const getLocale = () => { + if (typeof navigator !== "object") return "en" as const + const languages = navigator.languages?.length ? navigator.languages : [navigator.language] + for (const language of languages) { + if (!language) continue + if (language.toLowerCase().startsWith("zh")) return "zh" as const + } + return "en" as const +} + +const getRootNotFoundError = () => { + const key = "error.dev.rootNotFound" as const + const locale = getLocale() + return locale === "zh" ? (zh[key] ?? en[key]) : en[key] +} + +const getStorage = (key: string) => { + if (typeof localStorage === "undefined") return null + try { + return localStorage.getItem(key) + } catch { + return null + } +} + +const setStorage = (key: string, value: string | null) => { + if (typeof localStorage === "undefined") return + try { + if (value !== null) { + localStorage.setItem(key, value) + return + } + localStorage.removeItem(key) + } catch { + return + } +} + +const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY) +const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url) + +const notify: Platform["notify"] = async (title, description, onClick) => { + if (!("Notification" in window)) return + + const permission = + Notification.permission === "default" + ? await Notification.requestPermission().catch(() => "denied") + : Notification.permission + + if (permission !== "granted") return + + const inView = document.visibilityState === "visible" && document.hasFocus() + if (inView) return + + const notification = new Notification(title, { + body: description ?? "", + icon: "https://opencode.ai/favicon-96x96-v3.png", + }) + + notification.onclick = () => { + window.focus() + onClick?.() + notification.close() + } +} + +const openExternal: Platform["openExternal"] = (value) => { + if (!URL.canParse(value)) return + const url = new URL(value) + if (url.protocol !== "http:" && url.protocol !== "https:" && url.protocol !== "mailto:") return + window.open(url.href, "_blank", "noopener,noreferrer") +} + +const restart: Platform["restart"] = async () => { + window.location.reload() +} + +const root = document.getElementById("root") +if (!(root instanceof HTMLElement) && import.meta.env.DEV) { + throw new Error(getRootNotFoundError()) +} + +const getCurrentUrl = () => { + if (location.hostname.includes("opencode.ai")) return "http://localhost:4096" + if (import.meta.env.DEV) + return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}` + return location.origin +} + +const getDefaultUrl = () => { + const lsDefault = readDefaultServerUrl() + if (lsDefault) return lsDefault + return getCurrentUrl() +} + +const clearAuthToken = () => { + const params = new URLSearchParams(location.search) + if (!params.has("auth_token")) return + params.delete("auth_token") + history.replaceState(null, "", location.pathname + (params.size ? `?${params}` : "") + location.hash) +} + +const platform: Platform = { + platform: "web", + draftStore: createBrowserDraftStore(), + version: pkg.version, + openExternal, + restart, + notify, + getDefaultServer: async () => { + const stored = readDefaultServerUrl() + return stored ? ServerConnection.Key.make(stored) : null + }, + setDefaultServer: writeDefaultServerUrl, +} + +if (import.meta.env.VITE_SENTRY_DSN) { + Sentry.init({ + dsn: import.meta.env.VITE_SENTRY_DSN, + environment: import.meta.env.VITE_SENTRY_ENVIRONMENT ?? import.meta.env.MODE, + release: import.meta.env.VITE_SENTRY_RELEASE ?? `web@${pkg.version}`, + initialScope: { + tags: { + platform: "web", + }, + }, + integrations: (integrations) => { + return integrations.filter( + (i) => + i.name !== "Breadcrumbs" && !(import.meta.env.OPENCODE_CHANNEL === "prod" && i.name === "GlobalHandlers"), + ) + }, + }) +} + +if (root instanceof HTMLElement) { + void loadInitialLocale().then((locale) => { + const auth = authFromToken(new URLSearchParams(location.search).get("auth_token")) + clearAuthToken() + const server: ServerConnection.Http = { + type: "http", + authToken: !!auth, + http: { + url: getCurrentUrl(), + ...auth, + }, + } + render( + () => ( + + + + + + ), + root, + ) + }) +} diff --git a/packages/app/src/env.d.ts b/packages/app/src/env.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..5754df76838db13d081a27489ff68909570be6b9 --- /dev/null +++ b/packages/app/src/env.d.ts @@ -0,0 +1,31 @@ +interface ImportMetaEnv { + readonly VITE_OPENCODE_SERVER_HOST: string + readonly VITE_OPENCODE_SERVER_PORT: string + readonly VITE_OPENCODE_CHANNEL?: "dev" | "beta" | "prod" + + readonly VITE_SENTRY_DSN?: string + readonly VITE_SENTRY_ENVIRONMENT?: string + readonly VITE_SENTRY_RELEASE?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +declare module "*.png" { + const src: string + export default src +} + +declare module "*.mp4" { + const src: string + export default src +} + +export declare module "solid-js" { + namespace JSX { + interface Directives { + sortable: true + } + } +} diff --git a/packages/app/src/i18n/am.ts b/packages/app/src/i18n/am.ts new file mode 100644 index 0000000000000000000000000000000000000000..bda48350e5321f5747876678f370354480bd8fe4 --- /dev/null +++ b/packages/app/src/i18n/am.ts @@ -0,0 +1,1130 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ፋይል", + "desktop.menu.edit": "አርትዕ", + "desktop.menu.view": "እይታ", + "desktop.menu.go": "ሂድ", + "desktop.menu.window": "መስኮት", + "desktop.menu.help": "እገዛ", + "desktop.menu.checkForUpdates": "ዝማኔዎችን ይመልከቱ...", + "desktop.menu.settings": "ቅንጅቶች", + "desktop.menu.reloadWebview": "ዳግም ጫን Webview", + "desktop.menu.restart": "ዳግም አስጀምር", + "desktop.menu.exportLogs": "የመላክ ምዝግብ ማስታወሻዎች...", + "desktop.menu.newSession": "አዲስ ክፍለ ጊዜ", + "desktop.menu.openProject": "ክፍት ፕሮጀክት...", + "desktop.menu.newWindow": "አዲስ መስኮት", + "desktop.menu.closeWindow": "መስኮትን ዝጋ", + "desktop.menu.undo": "ቀልብስ", + "desktop.menu.redo": "ድገም", + "desktop.menu.cut": "የተቆረጠ", + "desktop.menu.copy": "ኮፒ", + "desktop.menu.paste": "ለጥፍ", + "desktop.menu.delete": "ሰርዝ", + "desktop.menu.selectAll": "ሁሉንም ምረጥ", + "desktop.menu.toggleSidebar": "የጎን አሞሌን ቀይር", + "desktop.menu.toggleTerminal": "መቀያየር ተርሚናል", + "desktop.menu.toggleFileTree": "ፋይል ዛፍን ቀይር", + "desktop.menu.reload": "ዳግም ጫን", + "desktop.menu.toggleDeveloperTools": "የገንቢ መሣሪያዎችን ቀይር", + "desktop.menu.actualSize": "ትክክለኛው መጠን", + "desktop.menu.zoomIn": "አጉላ", + "desktop.menu.zoomOut": "አጉላ", + "desktop.menu.toggleFullScreen": "ሙሉ ስክሪን ቀይር", + "desktop.menu.back": "ተመለስ", + "desktop.menu.forward": "ወደፊት", + "desktop.menu.previousSession": "የቀድሞው ክፍለ ጊዜ", + "desktop.menu.nextSession": "ቀጣይ ክፍለ ጊዜ", + "desktop.menu.previousProject": "የቀድሞው ፕሮጀክት", + "desktop.menu.nextProject": "ቀጣይ ፕሮጀክት", + "desktop.menu.minimize": "አሳንስ", + "desktop.menu.maximize": "ከፍተኛ", + "desktop.menu.documentation": "የOpenCode ሰነድ", + "desktop.menu.supportForum": "የድጋፍ መድረክ", + "desktop.menu.shareFeedback": "አጋራ ግብረመልስ", + "desktop.menu.reportBug": "ስህተትን ሪፖርት አድርግ", + "desktop.menu.ariaLabel": "OpenCodeምናሌ", + "desktop.updater.dialog.checkFailed.message": "የዝማኔ ማረጋገጫ አልተሳካም።", + "desktop.updater.dialog.checkFailed.title": "ስህተት ማዘመን", + "desktop.updater.dialog.upToDate.message": "ወቅታዊ ነዎት።", + "desktop.updater.dialog.upToDate.title": "ምንም ማሻሻያ የለም", + "desktop.updater.dialog.ready.message": "ዝማኔ {{version}} ወርዷል። አሁን እንደገና ይጀመር?", + "desktop.updater.dialog.ready.title": "አዘምን ዝግጁ", + "desktop.updater.dialog.restart": "ዳግም አስጀምር", + "desktop.updater.dialog.later": "በኋላ", + "desktop.recovery.action.relaunch": "ዳግም አስጀምር", + "desktop.recovery.action.exportLogs": "የመላክ ምዝግብ ማስታወሻዎች", + "desktop.recovery.action.keepWaiting": "መጠበቅዎን ይቀጥሉ", + "desktop.recovery.action.quit": "አቁም", + "desktop.recovery.loadFailed": "OpenCode መጫን አልቻለም", + "desktop.recovery.terminated": "OpenCode መስኮት በድንገት ተቋርጧል", + "desktop.recovery.unresponsive": "OpenCode ምላሽ እየሰጠ አይደለም", + "desktop.recovery.unresponsive.detail": "መተግበሪያውን እንደገና ማስጀመር፣ መዝገቦችን መክፈት ወይም መጠበቅ ትችላለህ።", + "desktop.recovery.loadFailed.detail": "መስኮት፡ {{window}}\nURL፡ {{url}}\nስህተት፡ {{code}} {{description}}", + "desktop.recovery.terminated.detail": "መስኮት፡ {{window}}\nምክንያት፡ {{reason}}\nኮድ፡ {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "አቃፊ ምረጥ", + "desktop.dialog.chooseFile": "ፋይል ምረጥ", + "desktop.dialog.saveFile": "ፋይሉን አስቀምጥ", + "desktop.dialog.files": "ፋይሎች", + "desktop.server.local": "አካባቢያዊ አገልጋይ", + "desktop.wsl.error.windowsOnly": "WSL የሚገኘው በWindows", + "desktop.wsl.error.unavailable": "WSL አይገኝም", + "desktop.wsl.error.listInstalled": "የተጫኑትን WSL distros መዘርዘር አልተሳካም", + "desktop.wsl.error.listOnline": "ኦንላይን WSL distros መዘርዘር አልተሳካም", + "desktop.wsl.error.executeDistro": "በዲስትሮ ውስጥ ትዕዛዞችን ማስፈጸም አይቻልም", + "desktop.wsl.error.installWsl": "WSLመጫኑ አልተሳካም", + "desktop.wsl.error.installDistro": "distroን መጫን አልተቻለም፡ {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCodeመጫኑ አልተሳካም", + "desktop.wsl.error.alreadyAdded": "{{distro}} አስቀድሞ ታክሏል", + "desktop.wsl.error.opencodeMissing": "ክፍት ኮድ በዚህ ዲስትሪ ውስጥ አልተጫነም", + "desktop.wsl.error.opencodeCannotRun": "ክፍት ኮድ ተጭኗል ግን ማሄድ አልቻለም", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode በ{{distro}}] ውስጥ አልተጫነም", + "desktop.wsl.error.updateVersion": + "OpenCode ዝማኔ አልቋል ግን {{distro}} አሁንም ሪፖርት ያደርጋል {{installed}}; የሚጠበቀው {{expected}}", + "desktop.wsl.error.noVersion": "ምንም ስሪት የለም", + "desktop.wsl.error.serverExited": "WSL አገልጋይ ከተጀመረ በኋላ ወጥቷል (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL አገልጋይ ጤናማ ከመሆኑ በፊት ወጥቷል (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Sidecar for {{distro}} የጤና ፍተሻ ጊዜው አልፎበታል ከ{{timeout}}ሚሴ በኋላ", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} ጊዜው አልፎበታል ከ{{timeout}}ሚሴ በኋላ", + "desktop.wsl.error.failedPort": "ወደብ ማግኘት አልተሳካም", + "desktop.picker.error.notSelected": "ፋይሉ በመራጭው አልተመረጠም", + "desktop.picker.error.sizeLimit": "የተመረጡት ዓባሪዎች ከ{{limit}}ሜባ ገደቡ በላይ", + "command.category.suggested": "የሚመከር", + "command.category.view": "እይታ", + "command.category.project": "ፕሮጀክት", + "command.category.provider": "አቅራቢ", + "command.category.server": "አገልጋይ", + "command.category.session": "ክፍለ ጊዜ", + "command.category.theme": "ገጽታ", + "command.category.language": "ቋንቋ", + "command.category.file": "ፋይል", + "command.category.context": "አውድ", + "command.category.terminal": "ተርሚናል", + "command.category.model": "ሞዴል", + "command.category.mcp": "MCP", + "command.category.agent": "ወኪል", + "command.category.permissions": "ፍቃዶች", + "command.category.workspace": "Workspace", + "command.category.settings": "ቅንጅቶች", + "command.logs.export": "የመላክ ምዝግብ ማስታወሻዎች", + "theme.scheme.system": "ስርዓት", + "theme.scheme.light": "ብርሃን", + "theme.scheme.dark": "ጨለማ", + "command.sidebar.toggle": "የጎን አሞሌን ቀይር", + "command.project.open": "ክፍት ፕሮጀክት", + "command.project.previous": "የቀድሞው ፕሮጀክት", + "command.project.next": "ቀጣይ ፕሮጀክት", + "command.project.index": "ወደ ፕሮጀክት {{index}}] ቀይር", + "command.provider.connect": "አቅራቢን አገናኝ", + "command.server.switch": "አገልጋይ ቀይር", + "command.settings.open": "ክፍት ቅንብሮች", + "command.session.previous": "የቀድሞው ክፍለ ጊዜ", + "command.session.next": "ቀጣይ ክፍለ ጊዜ", + "command.session.previous.unseen": "ያለፈው ያልተነበበ ክፍለ ጊዜ", + "command.session.next.unseen": "ቀጣይ ያልተነበበ ክፍለ ጊዜ", + "command.session.archive": "ክፍለ ጊዜን በማህደር አስቀምጥ", + "command.palette": "የትእዛዝ ቤተ-ስዕል", + "command.theme.cycle": "የዑደት ጭብጥ", + "command.theme.set": "ገጽታ ይጠቀሙ፡ {{theme}}", + "command.theme.scheme.cycle": "የዑደት ቀለም ንድፍ", + "command.theme.scheme.set": "የቀለም ንድፍ ተጠቀም፡ {{scheme}}", + "command.language.cycle": "ሳይክል ቋንቋ", + "command.language.set": "ቋንቋን ተጠቀም፡ {{language}}", + "command.session.new": "አዲስ ክፍለ ጊዜ", + "command.file.open": "ክፍት ፋይል", + "command.tab.close": "ትርፉን ዝጋ", + "command.tab.reopenClosed": "የተዘጋውን ትር እንደገና ክፈት", + "command.context.addSelection": "ምርጫ ወደ አውድ አክል", + "command.context.addSelection.description": "የተመረጡትን መስመሮች ከአሁኑ ፋይል ያክሉ", + "command.input.focus": "የትኩረት ግቤት", + "command.terminal.toggle": "ተርሚናል ቀይር", + "command.fileTree.toggle": "ፋይል ዛፍን ቀይር", + "command.review.toggle": "ግምገማውን ቀይር", + "command.terminal.new": "አዲስ ተርሚናል", + "command.terminal.new.description": "አዲስ ተርሚናል ትር ፍጠር", + "command.steps.toggle": "ደረጃዎችን ቀይር", + "command.steps.toggle.description": "ለአሁኑ መልእክት ደረጃዎችን አሳይ ወይም ደብቅ", + "command.message.previous": "የቀድሞ መልእክት", + "command.message.previous.description": "ወደ ቀዳሚው የተጠቃሚ መልእክት ይሂዱ", + "command.message.next": "ቀጣይ መልእክት", + "command.message.next.description": "ወደሚቀጥለው የተጠቃሚ መልእክት ይሂዱ", + "command.model.choose": "ሞዴል ምረጥ", + "command.model.choose.description": "የተለየ ሞዴል ይምረጡ", + "command.mcp.toggle": "MCPs", + "command.mcp.toggle.description": "MCPs", + "command.agent.cycle": "ወደሚቀጥለው ወኪል ቀይር", + "command.agent.cycle.description": "ወደሚቀጥለው ወኪል ቀይር", + "command.agent.cycle.reverse": "ወደቀድሞው ወኪል ቀይር", + "command.agent.cycle.reverse.description": "ወደ ቀዳሚው ወኪል ቀይር", + "command.model.variant.cycle": "የማሰብ ጥረት ደረጃን ቀይር", + "command.model.variant.cycle.description": "ወደሚቀጥለው የጥረት ደረጃ ቀይር", + "command.prompt.mode.shell": "ሼል", + "command.prompt.mode.normal": "ፕሮምፕት", + "command.permissions.autoaccept.enable": "በራስ-ተቀበል ፈቃዶች", + "command.permissions.autoaccept.disable": "በራስ-መቀበል ፍቃዶችን አቁም", + "command.workspace.toggle": "Workspace ቀይር", + "command.workspace.toggle.description": "በጎን አሞሌው ውስጥ ብዙ workspace-ዎችን አንቃ ወይም አሰናክል", + "command.session.undo": "ቀልብስ", + "command.session.undo.description": "የመጨረሻውን መልእክት ቀልብስ", + "command.session.redo": "ድገም", + "command.session.redo.description": "የመጨረሻውን የተቀለበሰ መልእክት ይድገሙት", + "command.session.compact": "የታመቀ ክፍለ ጊዜ", + "command.session.compact.description": "የአውድ መጠንን ለመቀነስ ክፍለ-ጊዜውን ያጠቃልሉት", + "command.session.fork": "ከመልዕክት አዲስ ቅርንጫፍ ፍጠር", + "command.session.fork.description": "ከቀደመው መልእክት አዲስ ክፍለ ጊዜ ፍጠር", + "command.session.share": "አጋራ ክፍለ ጊዜ", + "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": "ምንም ውጤቶች አልተገኙም", + "palette.group.commands": "ትዕዛዞች", + "palette.group.files": "ፋይሎች", + "dialog.provider.search.placeholder": "የፍለጋ አቅራቢዎች", + "dialog.provider.empty": "ምንም አቅራቢዎች አልተገኙም", + "dialog.provider.group.popular": "ታዋቂ", + "dialog.provider.group.other": "ሌላ", + "dialog.provider.custom.label": "ብጁ OpenAIተኳሃኝ አቅራቢ", + "dialog.provider.tag.recommended": "የሚመከር", + "dialog.provider.opencode.note": "Claude፣ GPT፣ Gemini እና ሌሎችንም ጨምሮ የተስተካከሉ ሞዴሎች", + "dialog.provider.opencode.tagline": "ታማኝ የተመቻቹ ሞዴሎች", + "dialog.provider.opencodeGo.tagline": "ዝቅተኛ ወጪ ለሁሉም ሰው", + "dialog.provider.anthropic.note": "የቀጥታ የClaude ሞዴሎች፣ ፕሮ እና ማክስን ጨምሮ", + "dialog.provider.copilot.note": "AI ሞዴሎች በGitHub Copilot] በኩል ኮድ ለማድረግ እገዛ", + "dialog.provider.openai.note": "GPT ሞዴሎች ለፈጣን እና ችሎታ ላለው አጠቃላይ AI ተግባራት", + "dialog.provider.google.note": "Gemini ለፈጣን፣ የተዋቀሩ ምላሾች ሞዴሎች", + "dialog.provider.openrouter.note": "ሁሉንም የሚደገፉ ሞዴሎች ከአንድ አቅራቢ ይድረሱ", + "dialog.provider.vercel.note": "የተዋሃደ የ AI ሞዴሎች ከስማርት ማዘዋወር ጋር", + "dialog.model.select.title": "ሞዴል ምረጥ", + "dialog.model.search.placeholder": "የፍለጋ ሞዴሎች", + "dialog.model.empty": "ምንም የሞዴል ውጤቶች የሉም", + "dialog.model.manage": "ሞዴሎችን አስተዳድር", + "dialog.model.manage.description": "በሞዴል መራጭ ውስጥ የትኞቹ ሞዴሎች እንደሚታዩ አብጅ።", + "dialog.model.manage.provider.toggle": "ሁሉንም {{provider}} ሞዴሎች ቀያይር", + "dialog.model.unpaid.freeModels.title": "ነጻ ሞዴሎች በOpenCode", + "dialog.model.unpaid.addMore.title": "ተጨማሪ ሞዴሎችን ከታዋቂ አቅራቢዎች ያክሉ", + "dialog.model.unpaid.viewMoreProviders": "ከ70+ በላይ አቅራቢዎችን ይመልከቱ", + "dialog.provider.viewAll": "ተጨማሪ አቅራቢዎችን አሳይ", + "provider.connect.title": "{{provider}} አገናኝ", + "provider.connect.title.anthropicProMax": "በClaude Pro/Max ይግቡ", + "provider.connect.selectMethod": "የመግቢያ ዘዴን ለ{{provider}} ምረጥ።", + "provider.connect.method.apiKey": "API ቁልፍ", + "provider.connect.method.browser": "አሳሽ", + "provider.connect.method.headless": "ያለ ግራፊክ በይነገጽ", + "provider.connect.status.inProgress": "ፍቃድ በሂደት ላይ...", + "provider.connect.status.waiting": "ፈቀዳን በመጠበቅ ላይ...", + "provider.connect.status.failed": "ፍቃድ መስጠት አልተሳካም፡ {{error}}", + "provider.connect.apiKey.description": + "መለያዎን ለማገናኘት የ{{provider}} API ቁልፍዎን ያስገቡ እና {{provider}} ሞዴሎችን በOpenCode ይጠቀሙ።", + "provider.connect.apiKey.label": "{{provider}} API ቁልፍ", + "provider.connect.apiKey.placeholder": "API ቁልፍ", + "provider.connect.apiKey.required": "API ቁልፍ ያስፈልጋል", + "provider.connect.opencodeZen.line1": "OpenCode ዜን ለኮድ ወኪሎች የተመቻቹ አስተማማኝ ሞዴሎች ስብስብ መዳረሻ ይሰጥዎታል።", + "provider.connect.opencodeZen.line2": "በአንድ የAPI ቁልፍ እንደ Claude፣ GPT፣ Gemini፣ GLM እና ሌሎችም ያሉ ሞዴሎችን መድረስ ይችላሉ።", + "provider.connect.opencodeZen.visit.prefix": "ጎብኝ ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " የእርስዎን API ቁልፍ ለመሰብሰብ።", + "provider.connect.oauth.code.visit.prefix": "ጎብኝ ", + "provider.connect.oauth.code.visit.link": "ይህ ሊንክ", + "provider.connect.oauth.code.visit.suffix": + " መለያዎን ለማገናኘት የፈቀዳ ኮድዎን ለመሰብሰብ እና {{provider}} ሞዴሎችን በOpenCode ውስጥ ይጠቀሙ።", + "provider.connect.oauth.code.label": "{{method}}የፈቀዳ ኮድ", + "provider.connect.oauth.code.placeholder": "የፈቃድ ኮድ", + "provider.connect.oauth.code.required": "የፈቃድ ኮድ ያስፈልጋል", + "provider.connect.oauth.code.invalid": "ልክ ያልሆነ የፈቀዳ ኮድ", + "provider.connect.oauth.auto.visit.prefix": "ጎብኝ ", + "provider.connect.oauth.auto.visit.link": "ይህ ሊንክ", + "provider.connect.oauth.auto.visit.suffix": " እና መለያዎን ለማገናኘት ከታች ያለውን ኮድ ያስገቡ እና {{provider}} ሞዴሎችን በOpenCode ይጠቀሙ።", + "provider.connect.oauth.auto.confirmationCode": "የማረጋገጫ ኮድ", + "provider.connect.toast.connected.title": "{{provider}}ተገናኝቷል", + "provider.connect.toast.connected.description": "{{provider}} ሞዴሎች አሁን ለመጠቀም ይገኛሉ።", + "provider.custom.title": "ብጁ አቅራቢ", + "provider.custom.unavailable": "ብጁ አቅራቢዎች በዚህ አገልጋይ ላይ አይገኙም", + "provider.custom.description.prefix": "OpenAI-ተኳሃኝ አቅራቢን ያዋቅሩ። ይመልከቱ ", + "provider.custom.description.link": "የአቅራቢ ውቅረት ሰነዶች", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "የአቅራቢ መታወቂያ", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "ዝቅተኛ ፊደሎች፣ ቁጥሮች፣ ሰረዞች፣ ወይም ከስር ያሉ ምልክቶች", + "provider.custom.field.name.label": "የማሳያ ስም", + "provider.custom.field.name.placeholder": "የእኔ AI አቅራቢ", + "provider.custom.field.baseURL.label": "መሰረት URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API ቁልፍ", + "provider.custom.field.apiKey.placeholder": "API ቁልፍ", + "provider.custom.field.apiKey.description": "አማራጭ. በራስጌዎች በኩል ማተምን የሚያስተዳድሩ ከሆነ ባዶ ይተዉት።", + "provider.custom.models.label": "ሞዴሎች", + "provider.custom.models.id.label": "መታወቂያ", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "ስም", + "provider.custom.models.name.placeholder": "የማሳያ ስም", + "provider.custom.models.remove": "ሞዴሉን አስወግድ", + "provider.custom.models.add": "ሞዴል አክል", + "provider.custom.headers.label": "ራስጌዎች (አማራጭ)", + "provider.custom.headers.key.label": "ራስጌ", + "provider.custom.headers.key.placeholder": "ራስጌ-ስም", + "provider.custom.headers.value.label": "ዋጋ", + "provider.custom.headers.value.placeholder": "ዋጋ", + "provider.custom.headers.remove": "ራስጌ አስወግድ", + "provider.custom.headers.add": "ራስጌ አክል", + "provider.custom.error.providerID.required": "የአቅራቢ መታወቂያ ያስፈልጋል", + "provider.custom.error.providerID.format": "ትንሽ ሆሄያትን፣ ቁጥሮችን፣ ሰረዞችን ወይም የስር ምልክቶችን ተጠቀም", + "provider.custom.error.providerID.exists": "ያ የአቅራቢ መታወቂያ አስቀድሞ አለ", + "provider.custom.error.name.required": "የማሳያ ስም ያስፈልጋል", + "provider.custom.error.baseURL.required": "መሠረት URL ያስፈልጋል", + "provider.custom.error.baseURL.format": "በhttp:// ወይም https://] መጀመር አለበት", + "provider.custom.error.required": "የሚያስፈልግ", + "provider.custom.error.duplicate": "የተባዛ", + "provider.disconnect.toast.disconnected.title": "{{provider}} ተቋርጧል", + "provider.disconnect.toast.disconnected.description": "{{provider}} ሞዴሎች ከአሁን በኋላ አይገኙም።", + "model.tag.free": "ነጻ", + "model.tag.latest": "የቅርብ", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "ጽሑፍ", + "model.input.image": "ምስል", + "model.input.audio": "ድምጽ", + "model.input.video": "ቪዲዮ", + "model.input.pdf": "pdf", + "model.tooltip.allows": "ይፈቅዳል፡ {{inputs}}", + "model.tooltip.context.label": "አውድ", + "model.tooltip.inputs": "ግቤቶች", + "model.tooltip.model": "ሞዴል", + "model.tooltip.provider": "አቅራቢ", + "model.tooltip.reasoning": "ምክንያት", + "model.tooltip.reasoning.allowed": "ማመዛዘን ይፈቅዳል", + "model.tooltip.reasoning.none": "ምክንያት የለም", + "model.tooltip.context": "የአውድ ገደብ {{limit}}", + "common.search.placeholder": "ፍለጋ", + "common.goBack": "ወደ ኋላ ዳስስ", + "common.goForward": "ወደ ፊት ዳስስ", + "common.loading": "በመጫን ላይ", + "common.loading.ellipsis": "...", + "common.cancel": "ሰርዝ", + "common.clear": "ግልጽ", + "common.open": "ክፍት", + "common.connect": "አገናኝ", + "common.disconnect": "ግንኙነቱን አቋርጥ", + "common.continue": "ቀጥል", + "common.submit": "አስረክብ", + "common.save": "አስቀምጥ", + "common.saving": "በማስቀመጥ ላይ...", + "common.default": "ነባሪ", + "common.attachment": "አባሪ", + "prompt.placeholder.shell": "የሼል ትዕዛዝ አስገባ... {{example}}", + "prompt.placeholder.normal": 'ማንኛውንም ነገር ይጠይቁ... "{{example}}"', + "prompt.placeholder.simple": "ማንኛውንም ነገር ይጠይቁ...", + "prompt.placeholder.summarizeComments": "አስተያየቶችን ማጠቃለል…", + "prompt.placeholder.summarizeComment": "አስተያየቱን ማጠቃለል…", + "prompt.mode.shell": "ሼል", + "prompt.mode.normal": "ፕሮምፕት", + "prompt.mode.shell.exit": "esc ለመውጣት", + "session.child.promptDisabled": "ንዑስ ክፍለ ጊዜዎች ሊጠየቁ አይችሉም።", + "session.child.backToParent": "ወደ ዋናው ክፍለ ጊዜ ተመለስ", + "prompt.example.1": "TODOን በኮድ ቤዝ ውስጥ ያስተካክሉ", + "prompt.example.2": "የዚህ ፕሮጀክት የቴክኖሎጂ ቁልል ምንድን ነው?", + "prompt.example.3": "የተበላሹ ሙከራዎችን ያስተካክሉ", + "prompt.example.4": "ማረጋገጫ እንዴት እንደሚሰራ ያብራሩ", + "prompt.example.5": "የደህንነት ተጋላጭነቶችን ፈልግ እና ያስተካክሉ", + "prompt.example.6": "ለተጠቃሚው አገልግሎት የክፍል ሙከራዎችን ያክሉ", + "prompt.example.7": "ይህን ተግባር የበለጠ እንዲነበብ አስተካክል", + "prompt.example.8": "ይህ ስህተት ምን ማለት ነው?", + "prompt.example.9": "ይህን ችግር እንዳስተካክል እርዳኝ", + "prompt.example.10": "APIሰነድ ፍጠር", + "prompt.example.11": "ዳታቤዝ መጠይቆችን ያመቻቹ", + "prompt.example.12": "የግቤት ማረጋገጫን አክል", + "prompt.example.13": "አዲስ አካል ለ...", + "prompt.example.14": "ይህን ፕሮጀክት እንዴት ማሰማራት እችላለሁ?", + "prompt.example.15": "ለምርጥ ልምዶች የእኔን ኮድ ይገምግሙ", + "prompt.example.16": "የስህተት አያያዝን ወደዚህ ተግባር ያክሉ", + "prompt.example.17": "ይህንን regex pattern ያብራሩ", + "prompt.example.18": "ይህንን ወደ TypeScript ቀይር", + "prompt.example.19": "መመዝገብን በኮድ ቤዝ ውስጥ አክል", + "prompt.example.20": "ምን ዓይነት ጥገኛዎች ጊዜ ያለፈባቸው ናቸው?", + "prompt.example.21": "የስደት ስክሪፕት እንድጽፍ እርዳኝ", + "prompt.example.22": "ለዚህ የመጨረሻ ነጥብ መሸጎጫ ተግባራዊ ያድርጉ", + "prompt.example.23": "ወደዚህ ዝርዝር ገፅ ጨምር", + "prompt.example.24": "CLI ትዕዛዝ ፍጠር ለ...", + "prompt.example.25": "አካባቢያዊ ተለዋዋጮች እዚህ እንዴት ይሰራሉ?", + "prompt.popover.emptyResults": "ምንም ተዛማጅ ውጤቶች የሉም", + "prompt.popover.emptyCommands": "ምንም ተዛማጅ ትዕዛዞች የሉም", + "prompt.dropzone.label": "ምስሎችን፣ ፒዲኤፎችን ወይም የጽሑፍ ፋይሎችን እዚህ ጣል", + "prompt.dropzone.file.label": "ወደ @mention ፋይል ጣል", + "prompt.slash.badge.custom": "ብጁ", + "prompt.slash.badge.skill": "ችሎታ", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "ንቁ", + "prompt.context.includeActiveFile": "ገቢር ፋይልን ያካትቱ", + "prompt.context.removeActiveFile": "ገቢር ፋይልን ከአውድ አስወግድ", + "prompt.context.removeFile": "ፋይሉን ከአውድ አስወግድ", + "prompt.action.attachFile": "ፋይሎችን አክል", + "prompt.menu.addImagesAndFiles": "ፋይሎችን አክል እና ተጨማሪ", + "prompt.menu.imagesAndFiles": "ምስሎች እና ፋይሎች", + "prompt.menu.commands": "ትዕዛዞች", + "prompt.menu.context": "አውድ", + "prompt.menu.shellCommand": "የሼል ትዕዛዝ", + "prompt.attachment.remove": "ዓባሪን አስወግድ", + "prompt.action.send": "ላክ", + "prompt.action.stop": "አቁም", + "prompt.toast.pasteUnsupported.title": "የማይደገፍ ዓባሪ", + "prompt.toast.pasteUnsupported.description": "ምስሎች፣ ፒዲኤፎች ወይም የጽሑፍ ፋይሎች ብቻ እዚህ ጋር ሊጣመሩ ይችላሉ።", + "prompt.toast.attachmentDuplicate.title": "ይህ ፋይል አስቀድሞ ተሰቅሏል", + "prompt.toast.modelAgentRequired.title": "ወኪል እና ሞዴል ይምረጡ", + "prompt.toast.modelAgentRequired.description": "ፕሮምፕት ከመላክዎ በፊት ወኪል እና ሞዴል ይምረጡ።", + "prompt.toast.worktreeCreateFailed.title": "Git worktree መፍጠር አልተሳካም", + "prompt.toast.sessionCreateFailed.title": "ክፍለ ጊዜ መፍጠር አልተሳካም", + "prompt.toast.shellSendFailed.title": "የሼል ትዕዛዝ መላክ አልተሳካም", + "prompt.toast.commandSendFailed.title": "ትዕዛዝ መላክ አልተሳካም", + "prompt.toast.promptSendFailed.title": "ፕሮምፕት መላክ አልተሳካም", + "prompt.toast.promptSendFailed.description": "ክፍለ ጊዜን ሰርስሮ ማውጣት አልተቻለም", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} ከ{{total}} ነቅቷል", + "dialog.mcp.empty": "ምንም MCPs አልተዋቀረም", + "dialog.lsp.empty": "LSPs በራስ-ሰር ከፋይል አይነቶች የተገኘ", + "dialog.plugins.empty": "plugins በ opencode.json የተዋቀሩ", + "mcp.status.connected": "የተገናኘ", + "mcp.status.failed": "ያልተሳካ", + "mcp.status.needs_auth": "auth ያስፈልገዋል", + "mcp.status.disabled": "የተሰናከለ", + "mcp.auth.clickToAuthenticate": "ለማረጋገጥ ጠቅ ያድርጉ", + "dialog.fork.empty": "አዲስ ቅርንጫፍ ለመፍጠር መልዕክት የለም", + "dialog.directory.search.placeholder": "የአቃፊዎችን ፍለጋ", + "dialog.directory.empty": "ምንም አቃፊዎች አልተገኙም", + "dialog.directory.action.selectFile": "ፋይል ምረጥ", + "dialog.directory.action.selectFolder": "አቃፊን ምረጥ", + "dialog.directory.root": "ሥር", + "dialog.directory.parent": "ወላጅ", + "dialog.directory.readError": "ይህን አቃፊ ማንበብ አልተቻለም", + "app.server.unreachable": "{{server}}ን መድረስ አልተቻለም", + "app.server.retrying": "በራስ ሰር እንደገና በመሞከር ላይ...", + "app.server.otherServers": "ሌሎች አገልጋዮች", + "dialog.server.title": "አገልጋዮች", + "dialog.server.description": "ይህ መተግበሪያ ከየትኛው OpenCode አገልጋይ ጋር እንደሚገናኝ ይቀይሩ።", + "dialog.server.search.placeholder": "የፍለጋ አገልጋዮች", + "dialog.server.empty": "ገና ምንም አገልጋይ የለም", + "dialog.server.add.title": "አገልጋይ አክል", + "dialog.server.add.url": "የአገልጋይ አድራሻ", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "ከአገልጋይ ጋር መገናኘት አልተቻለም", + "dialog.server.add.checking": "በማጣራት ላይ...", + "dialog.server.add.button": "አገልጋይ አክል", + "dialog.server.add.name": "የአገልጋይ ስም (አማራጭ)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "የተጠቃሚ ስም (አማራጭ)", + "dialog.server.add.usernamePlaceholder": "የተጠቃሚ ስም", + "dialog.server.add.password": "የይለፍ ቃል (አማራጭ)", + "dialog.server.add.passwordPlaceholder": "የይለፍ ቃል", + "dialog.server.edit.title": "አገልጋይ አርትዕ", + "dialog.server.default.title": "ነባሪ አገልጋይ", + "dialog.server.default.description": "አካባቢያዊ አገልጋይን ከመጀመር ይልቅ ከዚህ አገልጋይ ጋር ይገናኙ። እንደገና መጀመር ያስፈልገዋል።", + "dialog.server.default.none": "ምንም አገልጋይ አልተመረጠም", + "dialog.server.default.set": "የአሁኑን አገልጋይ እንደ ነባሪ አዘጋጅ", + "dialog.server.default.clear": "ግልጽ", + "dialog.server.action.remove": "አገልጋይ አስወግድ", + "dialog.server.menu.edit": "አርትዕ", + "dialog.server.menu.default": "ነባሪ አዘጋጅ", + "dialog.server.menu.defaultRemove": "ነባሪውን አስወግድ", + "dialog.server.menu.delete": "ሰርዝ", + "dialog.server.current": "የአሁኑ አገልጋይ", + "dialog.server.status.default": "ነባሪ", + "wsl.server.add": "WSL አገልጋይ አክል", + "wsl.server.addShort": "WSL አክል", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL አገልጋይ", + "wsl.server.retryStart": "እንደገና ጀምር", + "wsl.server.updating": "በማዘመን ላይ...", + "wsl.onboarding.step.distro": "distro ምረጥ", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "መፈተሽ WSL...", + "wsl.onboarding.restartRequired": "Windows WSLን መጫኑን ለመጨረስ ዳግም መጀመር ያስፈልገዋል።", + "wsl.onboarding.ready": "WSL ዝግጁ ነው።", + "wsl.onboarding.required": "WSLለመቀጠል ያስፈልጋል።", + "wsl.onboarding.checkingDistros": "Distros በመፈተሽ ላይ...", + "wsl.onboarding.installingDistro": "በመጫን ላይ {{distro}}...", + "wsl.onboarding.checkingDistro": "መፈተሽ {{distro}}...", + "wsl.onboarding.listingDistros": "የዝርዝር ዲስትሮስ...", + "wsl.onboarding.distroReady": "{{distro}} ዝግጁ ነው።", + "wsl.onboarding.distroNotInstalled": "{{distro}} ገና አልተጫነም።", + "wsl.onboarding.openDistroOnce": "ማዋቀሩን ለመጨረስ {{distro}} አንድ ጊዜ ይክፈቱ።", + "wsl.onboarding.finishingDistro": "ማዋቀሩን ለ{{distro}} በማጠናቀቅ ላይ።", + "wsl.onboarding.pickDistro": "distro ይምረጡ ወይም ከታች ይጫኑ።", + "wsl.onboarding.checkingOpencode": "መፈተሽ OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "OpenCodeን በ{{distro}} ውስጥ በመፈተሽ ላይ...", + "wsl.onboarding.updatingOpencode": "በማዘመን ላይ OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "በማዘመን ላይ OpenCode በ{{distro}}...", + "wsl.onboarding.updateOpencodeIn": "አዘምን OpenCode በ{{distro}}።", + "wsl.onboarding.updateOpencode": "አዘምን OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode በ{{distro}} ውስጥ ዝግጁ ነው።", + "wsl.onboarding.opencodeReady": "OpenCode ዝግጁ ነው።", + "wsl.onboarding.installOpencodeIn": "መጫን OpenCode በ{{distro}} ውስጥ።", + "wsl.onboarding.installOpencode": "ጫን OpenCode", + "wsl.onboarding.chooseDistroFirst": "መጀመሪያ ዳይስትሮ ይምረጡ።", + "wsl.onboarding.loadFailed": "WSL ሁኔታን መጫን አልተሳካም።", + "wsl.onboarding.loading": "በመጫን ላይ...", + "wsl.onboarding.installedDistros": "የተጫነው distros", + "wsl.onboarding.checkAgain": "እንደገና ያረጋግጡ", + "wsl.onboarding.distroStatus.ready": "ዝግጁ", + "wsl.onboarding.distroStatus.checking": "በማጣራት ላይ...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCodeአልተጫነም", + "wsl.onboarding.distroStatus.missingTools": "የጠፋ bash፣ curl", + "wsl.onboarding.distroStatus.unsupported": "የማይደገፍ · WSL 2] ተጠቀም", + "wsl.onboarding.needAnotherDistro": "ሌላ አስተላላፊ ይፈልጋሉ?", + "wsl.onboarding.needAnotherDistroHint": "Linux ስርጭት ከWSL ካታሎግ ጫን", + "wsl.onboarding.wslNotInstalled.title": "WSLአልተጫነም", + "wsl.onboarding.wslNotInstalled.description": "WSL (Windowsንዑስ ስርዓት ለLinux) OpenCode WSL አገልጋይ ከመጨመራቸው በፊት ያስፈልጋል", + "wsl.onboarding.wslUnavailable.title": "WSL አይገኝም", + "wsl.onboarding.wslUnavailable.description": "OpenCode በዚህ ማሽን ላይ WSLን ማረጋገጥ አልቻለም።", + "wsl.onboarding.installWsl": "ጫን WSL", + "wsl.onboarding.windowsRestartRequired": "WSLን መጫኑን ለመጨረስ Windows እንደገና ያስጀምሩ እና ከዚያ እንደገና ይክፈቱ OpenCode።", + "wsl.onboarding.next": "ቀጣይ", + "wsl.onboarding.refresh": "አድስ", + "wsl.onboarding.allDistrosAdded": "ሁሉም የተጫኑ ዳይስትሮዎች ቀድሞ ተጨምረዋል።", + "wsl.onboarding.noDistros": "ገና ምንም distros አልተገኘም።", + "wsl.onboarding.install": "ጫን", + "wsl.onboarding.installing": "በመጫን ላይ...", + "wsl.onboarding.installDistro": "distro ጫን", + "wsl.onboarding.searchDistros": "የፍለጋ distros", + "wsl.onboarding.wsl2Required": "WSL 2 ያስፈልጋል።", + "wsl.onboarding.toolsRequired": "ይህ ዲስትሮ bash እና curl ያስፈልገዋል።", + "wsl.onboarding.openTerminal": "ተርሚናል ክፈት", + "wsl.onboarding.path": "መንገድ፡ {{path}}", + "wsl.onboarding.notFound": "አልተገኘም", + "wsl.onboarding.version": "ስሪት፡ {{version}}", + "wsl.onboarding.unknown": "ያልታወቀ", + "wsl.onboarding.desktopVersion": "ዴስክቶፕ {{version}}", + "wsl.onboarding.versionMismatch": "የተጫነው ሥሪት ከዴስክቶፕ መተግበሪያ ሥሪት ጋር አይዛመድም።", + "wsl.onboarding.adding": "በማከል ላይ...", + "help.tabs.toast.ariaLabel": "ትሮችን በማስተዋወቅ ላይ። ስራዎን እና ንቁ ክፍለ ጊዜዎን በትሮች ያደራጁ", + "help.tabs.toast.dismiss": "የትርኮችን መረጃ አሰናብት", + "help.tabs.title": "ትሮችን በማስተዋወቅ ላይ", + "help.tabs.description": "ሥራዎን እና ንቁ ክፍለ ጊዜዎችን በትሮች ያደራጁ", + "help.tabs.date": "ሐምሌ 14", + "help.tabs.introduction": "OpenCode ዴስክቶፕ አሁን በትሮች ዙሪያ ነው የተሰራው።", + "help.tabs.sessions": + "አዲስ ክፍለ ጊዜ በትሩ ውስጥ ጀምር፣ወይም ከማንኛቸውም ፕሮጄክቶችህ ነባር ክፍለ ጊዜ ክፈት። አዲስ ነገር ሲጀምሩ አዲስ ትር ይክፈቱ፣ እና ሲጨርሱ ይዝጉት።", + "help.tabs.organize": "ትንሽ ትሮችን መክፈት ንቁ ክፍለ ጊዜዎችን ማደራጀት ቀላል ያደርገዋል። ትሮችን በአካባቢያቸው ለማቆየት ካሰቡ የማይረሳ ነገርን እንደገና ይሰይሙ።", + "help.tabs.home": "ሁሉንም ክፍለ ጊዜዎችዎን እና ፕሮጀክቶችዎን በአዲሱ የመነሻ ማያ ገጽ ላይ ያገኛሉ። ክፍለ ጊዜ መምረጥ በትር ውስጥ ይከፍታል።", + "help.tabs.persistence": "መተግበሪያውን እንደገና ሲከፍቱት ትሮችዎ አሁንም ክፍት ናቸው።", + "help.tabs.worktrees": + "አዲሱ ንድፍ እስካሁን Git Worktreesን አይደግፍም፣ በቅርቡ ይመጣል። ስለዚህ የቀደመውን አቀማመጥ በመጠቀም መቀጠል ከፈለግክ በቅንብሮች ውስጥ በአቀማመጦች መካከል መቀያየር ትችላለህ። አዲሱ አቀማመጥ በጥቂት ሳምንታት ውስጥ ዘላቂ እንደሚሆን ብቻ ያስታውሱ።", + "server.row.noUsername": "የተጠቃሚ ስም የለም", + "dialog.project.edit.title": "ፕሮጀክት አርትዕ", + "dialog.project.edit.name": "ስም", + "dialog.project.edit.icon": "አዶ", + "dialog.project.edit.icon.alt": "የፕሮጀክት አዶ", + "dialog.project.edit.icon.hint": "ምስሉን ጠቅ ያድርጉ ወይም ይጎትቱ", + "dialog.project.edit.icon.recommended": "የሚመከር፡ 128x128 ፒክስል", + "dialog.project.edit.color": "ቀለም", + "dialog.project.edit.color.select": "{{color}} ቀለም ይምረጡ", + "dialog.project.edit.worktree.startup": "የworkspace ማስጀመሪያ ስክሪፕት", + "dialog.project.edit.worktree.startup.description": "አዲስ workspace (Git worktree) ከፈጠረ በኋላ ይሰራል", + "dialog.project.edit.worktree.startup.placeholder": "ለምሳሌ፡. ጥቅል", + "dialog.releaseNotes.action.getStarted": "ጀምር", + "dialog.releaseNotes.action.next": "ቀጣይ", + "dialog.releaseNotes.action.hideFuture": "እነዚህን ወደፊት አታሳይ", + "dialog.releaseNotes.media.alt": "የልቀት ቅድመ እይታ", + "context.breakdown.title": "የአውድ ዝርዝር መግለጫ", + "context.breakdown.note": 'የግቤት ቶከኖች ግምታዊ ብልሽት። "ሌላ" የመሳሪያ ትርጓሜዎችን እና ከላይ ያለውን ያካትታል።', + "context.breakdown.system": "ስርዓት", + "context.breakdown.user": "ተጠቃሚ", + "context.breakdown.assistant": "ረዳት", + "context.breakdown.tool": "የመሳሪያ ጥሪዎች", + "context.breakdown.other": "ሌላ", + "context.systemPrompt.title": "የስርዓት ፕሮምፕት", + "context.rawMessages.title": "ጥሬ መልዕክቶች", + "context.export.session": "ክፍለ ጊዜን ወደ ውጭ ላክ", + "context.stats.session": "ክፍለ ጊዜ", + "context.stats.messages": "መልእክቶች", + "context.stats.provider": "አቅራቢ", + "context.stats.model": "ሞዴል", + "context.stats.limit": "የአውድ ገደብ", + "context.stats.totalTokens": "አጠቃላይ ቶከኖች", + "context.stats.usage": "አጠቃቀም", + "context.stats.inputTokens": "የግቤት ቶከኖች", + "context.stats.outputTokens": "የውጤት ቶከኖች", + "context.stats.reasoningTokens": "የማመዛዘኛ ቶከኖች", + "context.stats.cacheTokens": "መሸጎጫ ቶከኖች (ማንበብ/መጻፍ)", + "context.stats.userMessages": "የተጠቃሚ መልዕክቶች", + "context.stats.assistantMessages": "የረዳት መልዕክቶች", + "context.stats.totalCost": "ጠቅላላ ወጪ", + "context.stats.sessionCreated": "ክፍለ ተፈጠረ", + "context.stats.lastActivity": "የመጨረሻው እንቅስቃሴ", + "context.usage.tokens": "ቶከኖች", + "context.usage.usage": "አጠቃቀም", + "context.usage.cost": "ዋጋ", + "context.usage.clickToView": "አውድ ለማየት ጠቅ ያድርጉ", + "context.usage.view": "የአውድ አጠቃቀምን ይመልከቱ", + "language.en": "እንግሊዝኛ", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "ዳንስክ", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (ብራሲል)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "ቋንቋ", + "toast.language.description": "ወደ {{language}} ተቀይሯል", + "toast.theme.title": "ገጽታ ተቀይሯል", + "toast.scheme.title": "የቀለም ንድፍ", + "toast.workspace.enabled.title": "Workspace-ዎች ነቅተዋል", + "toast.workspace.enabled.description": "በርካታ Git worktree-ዎች በጎን አሞሌ ላይ አሁን ይታያሉ", + "toast.workspace.disabled.title": "Workspace-ዎች ተሰናክለዋል", + "toast.workspace.disabled.description": "በጎን አሞሌው ላይ ዋናው Git worktree ብቻ ነው የሚታየው", + "toast.permissions.autoaccept.on.title": "በራስ-ተቀባይ ፍቃዶች", + "toast.permissions.autoaccept.on.description": "የፈቃድ ጥያቄዎች በራስ-ሰር ይጸድቃሉ", + "toast.permissions.autoaccept.off.title": "በራስ-መቀበል ፍቃዶችን አቁሟል", + "toast.permissions.autoaccept.off.description": "የፈቃድ ጥያቄዎች መጽደቅ ያስፈልጋቸዋል", + "toast.model.none.title": "ምንም ሞዴል አልተመረጠም", + "toast.model.none.description": "ይህን ክፍለ ጊዜ ለማጠቃለል አቅራቢን ያገናኙ", + "toast.file.loadFailed.title": "ፋይሉን መጫን አልተቻለም", + "toast.file.listFailed.title": "ፋይሎችን መዘርዘር አልተሳካም", + "toast.context.noLineSelection.title": "የመስመር ምርጫ የለም", + "toast.context.noLineSelection.description": "በመጀመሪያ በፋይል ትር ውስጥ የመስመር ክልልን ይምረጡ።", + "toast.session.share.copyFailed.title": "URLን ወደ ቅንጥብ ሰሌዳ መቅዳት አልተሳካም", + "toast.session.share.success.title": "ክፍል የተጋራ", + "toast.session.share.success.description": "አጋራ URL ወደ ቅንጥብ ሰሌዳ ተቀድቷል!", + "toast.session.share.failed.title": "ክፍለ ጊዜን ማጋራት አልተሳካም", + "toast.session.share.failed.description": "ክፍለ-ጊዜውን በማጋራት ላይ ስህተት ተፈጥሯል", + "toast.session.unshare.success.title": "ክፍል ያልተጋራ", + "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": "ዝማኔ አለ", + "toast.update.description": "አዲሱ የOpenCode ({{version}}) ስሪት አሁን ለመጫን አለ።", + "toast.update.action.installRestart": "ጫን እና እንደገና አስጀምር", + "toast.update.action.notYet": "ገና", + "error.page.title": "የሆነ ችግር ተፈጥሯል", + "error.page.description": "መተግበሪያውን በመጫን ላይ ሳለ ስህተት ተፈጥሯል።", + "error.page.description.localServerStartup": "የአገር ውስጥ አገልጋይ ሲጀመር ስህተት ተፈጥሯል።", + "error.page.details.label": "የስህተት ዝርዝሮች", + "error.page.action.restart": "ዳግም አስጀምር", + "error.page.action.report": "ስህተት ሪፖርት አድርግ", + "error.page.action.reported": "ስህተት ሪፖርት ተደርጓል", + "error.page.action.exportLogs": "የመላክ ምዝግብ ማስታወሻዎች", + "error.page.action.checking": "በማጣራት ላይ...", + "error.page.action.checkUpdates": "ዝማኔዎችን ይመልከቱ", + "error.page.action.updateTo": "ወደ {{version}} አዘምን", + "error.page.circular": "[Circular]", + "error.page.report.prefix": "እባክዎ ይህንን ስህተት ለOpenCode ቡድን ያሳውቁ", + "error.page.report.discord": "በDiscord", + "error.page.version": "ስሪት፡ {{version}}", + "error.dev.rootNotFound": "ሥርወ አካል አልተገኘም። ወደ የእርስዎ index.html ማከልን ረስተዋል? ወይም የመታወቂያ ባህሪው የተሳሳተ ፊደል ተጽፎ ሊሆን ይችላል?", + "error.serverSync.connectFailed": "ከአገልጋይ ጋር መገናኘት አልተቻለም። `{{url}}` ላይ የሚሰራ አገልጋይ አለ?", + "error.serverSDK.noServerAvailable": "ምንም አገልጋይ የለም", + "error.serverSDK.serverNotAvailable": "አገልጋይ አይገኝም", + "error.childStore.persistedCacheCreateFailed": "ቋሚ መሸጎጫ መፍጠር አልተሳካም", + "error.childStore.persistedProjectMetadataCreateFailed": "የቀጠለ የፕሮጀክት ዲበ ውሂብ መፍጠር አልተሳካም", + "error.childStore.persistedProjectIconCreateFailed": "የቀጠለ የፕሮጀክት አዶ መፍጠር አልተሳካም", + "error.childStore.storeCreateFailed": "መደብር መፍጠር አልተሳካም", + "directory.error.invalidUrl": "ልክ ያልሆነ ማውጫ በURL ውስጥ።", + "error.chain.unknown": "ያልታወቀ ስህተት", + "error.server.invalidConfiguration": "ልክ ያልሆነ ውቅር", + "error.chain.causedBy": "የተከሰተ፡", + "error.chain.apiError": "APIስህተት", + "error.chain.status": "ሁኔታ፡ {{status}}", + "error.chain.retryable": "እንደገና ሊሞከር የሚችል፡ {{retryable}}", + "error.chain.responseBody": "የምላሽ አካል፡\n{{body}}", + "error.chain.didYouMean": "{{suggestions}}] ማለትዎ ነው", + "error.chain.modelNotFound": "ሞዴል አልተገኘም፦ {{provider}}/{{model}}", + "error.chain.checkConfig": "የእርስዎን ውቅር (opencode.json) አቅራቢ/የሞዴል ስሞችን ያረጋግጡ", + "error.chain.mcpFailed": 'MCP አገልጋይ "{{name}}" አልተሳካም። ማስታወሻ፣ OpenCode እስካሁን MCP ማረጋገጫን አይደግፍም።', + "error.chain.providerAuthFailed": "የአቅራቢውን ማረጋገጥ አልተሳካም ({{provider}}): {{message}}", + "error.chain.providerInitFailed": 'አቅራቢውን "{{provider}}" ማስጀመር አልተሳካም። ምስክርነቶችን እና ውቅረትን ያረጋግጡ።', + "error.chain.configJsonInvalid": "ማዋቀር በ{{path}} ላይ የሚሰራ አይደለም JSON(C)", + "error.chain.configJsonInvalidWithMessage": "ማዋቀር በ{{path}} ላይ የሚሰራ አይደለም JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'ማውጫ "{{dir}}" በ{{path}} ውስጥ የሚሰራ አይደለም። ማውጫውን ወደ «{{suggestion}}» እንደገና ይሰይሙት ወይም ያስወግዱት። ይህ የተለመደ የትየባ ነው።', + "error.chain.configFrontmatterError": "የፊት ጉዳይን በ{{path}} ውስጥ መተንተን አልተሳካም፦\n{{message}}", + "error.chain.configInvalid": "ማዋቀር ፋይል በ{{path}} ልክ ያልሆነ ነው", + "error.chain.configInvalidWithMessage": "የማዋቀር ፋይል በ{{path}} ልክ ያልሆነ ነው፡ {{message}}", + "notification.permission.title": "ፈቃድ ያስፈልጋል", + "notification.permission.description": "{{sessionTitle}} በ{{projectName}} ውስጥ ፈቃድ ያስፈልገዋል", + "notification.question.title": "ጥያቄ", + "notification.question.description": "{{sessionTitle}} በ{{projectName}} ውስጥ ጥያቄ አለው", + "notification.action.goToSession": "ወደ ክፍለ-ጊዜ ሂድ", + "notification.session.responseReady.title": "ምላሽ ዝግጁ", + "notification.session.error.title": "የክፍለ ጊዜ ስህተት", + "notification.session.error.fallbackDescription": "ስህተት ተፈጥሯል", + "home.recentProjects": "የቅርብ ጊዜ ፕሮጀክቶች", + "home.empty.title": "ምንም የቅርብ ጊዜ ፕሮጀክቶች የሉም", + "home.empty.description": "የአካባቢውን ፕሮጀክት በመክፈት ጀምር", + "home.title": "ቤት", + "home.projects": "ፕሮጀክቶች", + "home.project.add": "ፕሮጀክት አክል", + "home.recentlyClosed": "በቅርቡ የተዘጋ", + "home.server.collapse": "የአገልጋይ ፕሮጀክቶችን ሰብስብ", + "home.server.expand": "የአገልጋይ ፕሮጀክቶችን ዘርጋ", + "home.sessions.search.placeholder": "የፍለጋ ክፍለ-ጊዜዎች", + "home.sessions.search.placeholder.scoped": "የፍለጋ ክፍለ ጊዜዎች በ{{scope}}", + "home.sessions.search.sessions": "ክፍለ-ጊዜዎች", + "home.sessions.search.noResults": "ለ{{query}}] ምንም ክፍለ ጊዜዎች አልተገኙም", + "home.sessions.empty": "አሁን ምንም እዚህ የለም", + "home.sessions.empty.description": "ለመጀመር ክፍለ ጊዜ ፍጠር", + "home.sessions.group.today": "ዛሬ", + "home.sessions.group.yesterday": "ትላንት", + "home.sessions.group.older": "የቆየ", + "home.providerTip": "Claude፣ GPT፣ Gemini፣ ወዘተ] ጨምሮ ሌሎች ሞዴሎችን ለመጠቀም ከ75+ አቅራቢዎች ጋር ይገናኙ", + "session.tab.session": "ክፍለ ጊዜ", + "session.tab.review": "ግምገማ", + "session.tab.context": "አውድ", + "session.tab.unknown": "ያልታወቀ ክፍለ ጊዜ", + "session.panel.reviewAndFiles": "ግምገማ እና ፋይሎች", + "session.error.notFound": "ይህ ክፍለ ጊዜ ሊገኝ አይችልም", + "session.error.notFound.description": "ይህ ትር የሚያመለክተው በዚህ አገልጋይ ላይ ወደሌለው ክፍለ ጊዜ ነው።", + "session.error.notFound.closeTab": "ትርን ዝጋ", + "session.error.serverConnection": "ከዚህ አገልጋይ ጋር መገናኘት አልተቻለም", + "session.review.filesChanged": "የተቀየሩ ፋይሎች {{count}}", + "session.review.change.one": "ለውጥ", + "session.review.change.other": "ለውጦች", + "session.review.loadingChanges": "ለውጦችን በመጫን ላይ...", + "session.review.empty": "በዚህ ክፍለ ጊዜ ምንም ለውጦች የሉም", + "session.review.noVcs": "ምንም Git የስሪት ቁጥጥር ስርዓት አልተገኘም፣ ለውጦች አይታዩም", + "session.review.noVcs.createGit.title": "Git ማከማቻ ፍጠር", + "session.review.noVcs.createGit.description": "በዚህ ፕሮጀክት ላይ ለውጦችን ይከታተሉ፣ ይገምግሙ እና ይቀልብሱ", + "session.review.noVcs.createGit.actionLoading": "Git ማከማቻ በመፍጠር ላይ...", + "session.review.noVcs.createGit.action": "Git ማከማቻ ፍጠር", + "session.review.noSnapshot": "የቅጽበታዊ ገጽ እይታን መከታተል በውቅረት ውስጥ ተሰናክሏል፣ስለዚህ የክፍለ-ጊዜ ለውጦች አይገኙም", + "session.review.noChanges": "ምንም ለውጦች የሉም", + "session.review.noUncommittedChanges": "ገና ምንም ያልተደረጉ ለውጦች የሉም", + "session.review.noBranchChanges": "ገና ምንም ቅርንጫፍ ምንም ለውጥ የለም", + "session.files.selectToOpen": "ለመክፈት ፋይል ምረጥ", + "session.files.all": "ሁሉም ፋይሎች", + "session.files.empty": "ምንም ፋይሎች", + "session.files.binaryContent": "ሁለትዮሽ ፋይል (ይዘት ሊታይ አይችልም)", + "session.messages.renderEarlier": "ቀደምት መልዕክቶችን ይስሩ", + "session.messages.loadingEarlier": "የቀድሞ መልዕክቶችን በመጫን ላይ...", + "session.messages.loadEarlier": "የቀድሞ መልዕክቶችን ጫን", + "session.messages.loading": "መልእክቶችን በመጫን ላይ...", + "session.messages.jumpToLatest": "ወደ የቅርብ ጊዜ ዝለል", + "session.context.addToContext": "{{selection}}ን ወደ አውድ አክል", + "session.todo.title": "የሚደረጉ ሥራዎች", + "session.todo.collapse": "ስብስብ", + "session.todo.expand": "ዘርጋ", + "session.todo.progress": "{{done}} ከ{{total}} ስራ ተጠናቋል", + "session.question.progress": "{{current}} ከ{{total}} ጥያቄዎች", + "session.question.minimize": "ጥያቄን አሳንስ", + "session.question.restore": "ጥያቄን ወደነበረበት መልስ", + "session.question.pending.one": "{{count}} በመጠባበቅ ላይ ያለ ጥያቄ", + "session.question.pending.other": "{{count}} በመጠባበቅ ላይ ያሉ ጥያቄዎች", + "session.followupDock.summary.one": "{{count}}የተሰለፈ መልእክት", + "session.followupDock.summary.other": "{{count}}የተሰለፉ መልዕክቶች", + "session.followupDock.sendNow": "አሁን ላክ", + "session.followupDock.edit": "አርትዕ", + "session.followupDock.collapse": "የተሰለፉ መልዕክቶችን ሰብስብ", + "session.followupDock.expand": "የተሰለፉ መልዕክቶችን ዘርጋ", + "session.revertDock.summary.one": "{{count}}የተመለሰ መልዕክት", + "session.revertDock.summary.other": "{{count}}የተመለሱ መልዕክቶች", + "session.revertDock.collapse": "የተመለሱ መልዕክቶችን ሰብስብ", + "session.revertDock.expand": "የተጠቀለሉ መልዕክቶችን ዘርጋ", + "session.revertDock.restore": "መልዕክት እነበረበት መልስ", + "session.new.title": "ማንኛውም ነገር ገንባ", + "session.new.project.new": "አዲስ ፕሮጀክት", + "session.new.project.search": "የፍለጋ ፕሮጀክቶች", + "session.new.project.add": "ፕሮጀክት አክል", + "session.new.worktree.main": "ዋና ቅርንጫፍ", + "session.new.worktree.mainWithBranch": "ዋና ቅርንጫፍ ({{branch}})", + "session.new.worktree.create": "አዲስ Git worktree ፍጠር", + "session.new.workspace.runIn": "ክፍለ ጊዜን በ ውስጥ ያሂዱ", + "session.new.workspace.triggerLocal": "አካባቢ", + "session.new.workspace.local": "አካባቢያዊ ማከማቻ", + "session.new.workspace.existing": "Workspace…", + "session.new.git.none": "ያለ Git", + "session.new.lastModified": "መጨረሻ የተሻሻለው", + "session.header.search.placeholder": "ፍለጋ {{project}}", + "session.header.searchFiles": "ፋይሎችን ይፈልጉ", + "session.header.openIn": "ክፍት ውስጥ", + "session.header.open.action": "ክፈት {{app}}", + "session.header.open.ariaLabel": "በ{{app}} ውስጥ ክፈት", + "session.header.open.menu": "ክፍት አማራጮች", + "session.header.open.copyPath": "መገልበጥ መንገድ", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "ፋይል አሳሽ", + "session.header.open.fileManager": "ፋይል አስተዳዳሪ", + "session.header.reveal.finder": "መገለጥ በFinder", + "session.header.reveal.fileExplorer": "ፋይል ኤክስፕሎረር ውስጥ ይገለጣል", + "session.header.reveal.containingFolder": "አቃፊን የያዘ ክፈት", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "ተርሚናል", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "ሁኔታ", + "status.popover.ariaLabel": "የአገልጋይ ውቅሮች", + "status.popover.tab.servers": "አገልጋዮች", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "ፕለጊኖች", + "status.popover.action.manageServers": "አገልጋዮችን አስተዳድር", + "session.share.popover.title": "በድር ላይ አትም", + "session.share.popover.description.shared": "ይህ ክፍለ ጊዜ በድር ላይ ይፋዊ ነው። አገናኙ ላለው ለማንኛውም ሰው ተደራሽ ነው።", + "session.share.popover.description.unshared": "ክፍለጊዜውን በይፋ በድሩ ላይ አጋራ። አገናኙ ላለው ለማንኛውም ሰው ተደራሽ ይሆናል።", + "session.share.action.share": "አጋራ", + "session.share.action.publish": "አትም", + "session.share.action.publishing": "በህትመት ላይ...", + "session.share.action.unpublish": "አትታተም", + "session.share.action.unpublishing": "ያለመታተም...", + "session.share.action.view": "እይታ", + "session.share.copy.copied": "የተገለበጠ", + "session.share.copy.copyLink": "መገልበጥ አገናኝ", + "lsp.tooltip.none": "ምንም LSP አገልጋዮች", + "lsp.label.connected": "{{count}}LSP", + "prompt.loading": "ፕሮምፕትን በመጫን ላይ...", + "terminal.loading": "በመጫን ላይ ተርሚናል...", + "terminal.title": "ተርሚናል", + "terminal.title.numbered": "ተርሚናል {{number}}", + "terminal.close": "ተርሚናል ዝጋ", + "terminal.connectionLost.title": "ግንኙነቱ ተቋርጧል", + "terminal.connectionLost.abnormalClose": "WebSocket ባልተለመደ ሁኔታ ተዘግቷል፡ {{code}}", + "terminal.connectionLost.description": "የተርሚናል ግንኙነቱ ተቋርጧል። ይህ አገልጋዩ እንደገና ሲጀምር ሊከሰት ይችላል።", + "terminal.connectTicket.csrfError": "PTY ትኬት ማገናኘት በመነሻ ወይም በCSRF ቼኮች ውድቅ ተደርጓል። አገልጋዩን ይመልከቱ CORS ውቅር", + "terminal.connectTicket.statusError": "PTY የግንኙነት ቲኬት ከ{{status}}] ጋር አልተሳካም", + "titlebar.update": "አዘምን", + "titlebar.updateVersion": "አዘምን {{version}}", + "common.closeTab": "ትርፉን ዝጋ", + "common.dismiss": "አሰናብት", + "common.moreCountSuffix": " (+{{count}} ተጨማሪ)", + "common.requestFailed": "ጥያቄው አልተሳካም", + "common.moreOptions": "ተጨማሪ አማራጮች", + "common.learnMore": "ተጨማሪ ለመረዳት", + "common.rename": "ዳግም ስም", + "common.export": "ላክ", + "common.reset": "ዳግም አስጀምር", + "common.archive": "በማህደር አስቀምጥ", + "common.delete": "ሰርዝ", + "common.close": "ዝጋ", + "common.edit": "አርትዕ", + "common.loadMore": "ተጨማሪ ጫን", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "ያልታወቀ", + "common.time.justNow": "አሁን", + "common.time.minutesAgo.short": "{{count}}ደቂቃ በፊት", + "common.time.hoursAgo.short": "{{count}}ከሰዓ በፊት", + "common.time.daysAgo.short": "ከ{{count}} ቀን በፊት", + "sidebar.menu.toggle": "ሜኑ ቀያይር", + "sidebar.nav.projectsAndSessions": "ፕሮጀክቶች እና ክፍለ-ጊዜዎች", + "sidebar.settings": "ቅንጅቶች", + "sidebar.help": "እገዛ", + "sidebar.workspaces.enable": "Workspace-ዎችን አንቃ", + "sidebar.workspaces.disable": "Workspace-ዎችን አሰናክል", + "sidebar.gettingStarted.title": "መጀመር", + "sidebar.gettingStarted.line1": "OpenCode ነፃ ሞዴሎችን ያካትታል ስለዚህ ወዲያውኑ መጀመር ይችላሉ።", + "sidebar.gettingStarted.line2": "ሞዴሎችን ለመጠቀም ማንኛውንም አቅራቢ ያገናኙ፣ Inc. Claude፣ GPT፣ Gemini ወዘተ.", + "sidebar.project.recentSessions": "የቅርብ ጊዜ ክፍለ ጊዜዎች", + "sidebar.project.viewAllSessions": "ሁሉንም ክፍለ ጊዜዎች ይመልከቱ", + "sidebar.project.clearNotifications": "ማሳወቂያዎችን አጽዳ", + "sidebar.empty.title": "ምንም ክፍት ፕሮጀክቶች የሉም", + "sidebar.empty.description": "ለመጀመር ፕሮጀክት ይክፈቱ", + "debugBar.ariaLabel": "የልማት አፈጻጸም ምርመራዎች", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": "የክፍለ ጊዜ ገጽን የነካው የመጨረሻው የተጠናቀቀ የመንገድ ሽግግር፣ ከራውተር ጅምር እስከ ከተረጋጋ በኋላ የመጀመሪያው ስዕል ድረስ የሚለካ።", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "ባለፉት 5 ሰከንዶች ውስጥ የሚሽከረከሩ ክፈፎች በሰከንድ።", + "debugBar.frame.label": "ፍሬም", + "debugBar.frame.tip": "ባለፉት 5 ሰከንዶች ውስጥ በጣም መጥፎው የፍሬም ጊዜ።", + "debugBar.jank.label": "መንቀጥቀጥ", + "debugBar.jank.tip": "ባለፉት 5 ሰከንዶች ውስጥ ከ32ሚሴ በላይ ክፈፎች።", + "debugBar.long.label": "ረጅም", + "debugBar.long.tip": "የታገደው ጊዜ እና የረዥም ጊዜ ብዛት በመጨረሻዎቹ 5 ሰከንዶች ውስጥ። ከፍተኛው ተግባር፡ {{max}}", + "debugBar.delay.label": "መዘግየት", + "debugBar.delay.tip": "በመጨረሻዎቹ 5 ሰከንዶች ውስጥ በጣም መጥፎው የታየ የግቤት መዘግየት።", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "በመጨረሻዎቹ 5 ሰከንዶች ውስጥ ግምታዊ የግንኙነቶች ቆይታ። ይህ INP የሚመስል ነው፣ ይፋዊው የድር ቪታሎች INP አይደለም።", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "ለአሁኑ መተግበሪያ የህይወት ዘመን ድምር አቀማመጥ ለውጥ።", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "ያገለገለ JS ክምር vs ክምር ገደብ። Chromium ብቻ።", + "debugBar.mem.tip": "ያገለገለ JS ክምር vs ክምር ገደብ። {{used}} ከ {{limit}}", + "debugBar.focus.label": "ትኩረት", + "debugBar.focus.tip": "በሁሉም በይነተገናኝ አካላት ላይ የማተኮር ቅጦችን አስገድድ", + "debugBar.focus.on": "በርቷል", + "debugBar.focus.off": "ጠፍቷል", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "የተመረጠውን ቋንቋ ሳይቀይሩ ሙሉውን የመተግበሪያ አቀማመጥ አቅጣጫ ያስገድዱ", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode ዴስክቶፕ", + "settings.section.desktop": "ዴስክቶፕ", + "settings.section.server": "አገልጋይ", + "settings.tab.general": "አጠቃላይ", + "settings.tab.shortcuts": "አቋራጮች", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL ውህደት", + "settings.desktop.wsl.description": "የOpenCode አገልጋዩን በWSL ውስጥ በWindows ላይ ያሂዱ።", + "settings.general.section.appearance": "መልክ", + "settings.general.section.advanced": "የላቀ", + "settings.general.section.notifications": "የስርዓት ማሳወቂያዎች", + "settings.general.section.updates": "ዝማኔዎች", + "settings.general.section.sounds": "የድምጽ ተጽዕኖዎች", + "settings.general.section.feed": "ምግብ", + "settings.general.section.display": "ማሳያ", + "settings.general.row.language.title": "ቋንቋ", + "settings.general.row.language.description": "የማሳያ ቋንቋውን ለOpenCode", + "settings.general.row.shell.title": "ተርሚናል ሼል", + "settings.general.row.shell.description": "ሼል በተርሚናል እና ወኪል መሳሪያዎች ጥቅም ላይ የዋለ", + "settings.general.row.shell.autoDefault": "ራስ-ሰር (ነባሪ)", + "settings.general.row.shell.terminalOnly": "ተርሚናል ብቻ", + "settings.general.row.appearance.title": "መልክ", + "settings.general.row.appearance.description": "OpenCodeበመሣሪያዎ ላይ እንዴት እንደሚታይ ብጁ ያድርጉ", + "settings.general.row.colorScheme.title": "የቀለም ንድፍ", + "settings.general.row.colorScheme.description": "OpenCode ስርዓቱን፣ ብርሃንን ወይም ጨለማውን ገጽታ የሚከተል እንደሆነ ምረጥ", + "settings.general.row.theme.title": "ገጽታ", + "settings.general.row.theme.description": "OpenCode ገጽታ እንዴት እንደሆነ አብጅ።", + "settings.general.row.font.title": "የኮድ ቅርጸ ቁምፊ", + "settings.general.row.font.description": "በኮድ ብሎኮች ውስጥ ጥቅም ላይ የዋለውን ቅርጸ-ቁምፊ አብጅ", + "settings.general.row.terminalFont.title": "ተርሚናል ፊደል", + "settings.general.row.terminalFont.description": "በተርሚናል ውስጥ ጥቅም ላይ የዋለውን ቅርጸ-ቁምፊ አብጅ", + "settings.general.row.uiFont.title": "የUI ቅርጸ ቁምፊ", + "settings.general.row.uiFont.description": "በመገናኛው ሁሉ ጥቅም ላይ የዋለውን ቅርጸ-ቁምፊ አብጅ", + "settings.general.row.followup.title": "መከታተያ ባህሪ", + "settings.general.row.followup.description": "ክትትል የሚጠይቅ ከሆነ ወዲያውኑ ይመራ እንደሆነ ይምረጡ ወይም በሰልፍ ይጠብቁ", + "settings.general.row.followup.option.queue": "ወረፋ", + "settings.general.row.followup.option.steer": "መሪ", + "settings.general.row.showFileTree.title": "ፋይል ዛፍ", + "settings.general.row.showFileTree.description": "የፋይል ዛፍ ፓነልን በክፍሎች ውስጥ አሳይ", + "settings.general.row.showNavigation.title": "የአሰሳ መቆጣጠሪያዎች", + "settings.general.row.showNavigation.description": "በዴስክቶፕ የርዕስ አሞሌ ላይ የኋላ እና ወደፊት ቁልፎችን አሳይ", + "settings.general.row.showSearch.title": "የትእዛዝ ቤተ-ስዕል", + "settings.general.row.showSearch.description": "በርዕስ አሞሌው ላይ የፍለጋ እና የትእዛዝ ቤተ-ስዕል ቁልፍ አሳይ", + "settings.general.row.showTerminal.title": "ተርሚናል", + "settings.general.row.showTerminal.description": "የተርሚናል አዝራሩን በዴስክቶፕ ርዕስ አሞሌ ላይ አሳይ", + "settings.general.row.showStatus.title": "የአገልጋይ ሁኔታ", + "settings.general.row.showStatus.description": "የአገልጋዩን ሁኔታ ቁልፍ በርዕስ አሞሌው ላይ አሳይ", + "settings.general.row.mobileTitlebarBottom.title": "የታችኛው ዳሰሳ", + "settings.general.row.mobileTitlebarBottom.description": "የርዕስ አሞሌውን እና የክፍለ ጊዜ ትሮችን በማያ ገጹ ግርጌ በሞባይል ላይ ያስቀምጡ", + "settings.general.row.showCustomAgents.title": "ወኪሉን አሳይ", + "settings.general.row.showCustomAgents.description": "በአቀናባሪው ውስጥ ባሉ ወኪሎች መካከል ይቀያይሩ። ሲደበቅ፣ ለግንባታ ወኪል ነባሪዎች ይሆናሉ።", + "settings.general.row.reasoningSummaries.title": "የምክንያት ማጠቃለያዎችን አሳይ", + "settings.general.row.reasoningSummaries.description": "የሞዴል ማመዛዘኛ ማጠቃለያዎችን በጊዜ መስመር አሳይ", + "settings.general.row.shellToolPartsExpanded.title": "የሼል መሣሪያ ክፍሎችን ዘርጋ", + "settings.general.row.shellToolPartsExpanded.description": "የሼል መሣሪያ ክፍሎችን በጊዜ መስመር በነባሪነት ያሳዩ", + "settings.general.row.editToolPartsExpanded.title": "የመሳሪያ ክፍሎችን ዘርጋ", + "settings.general.row.editToolPartsExpanded.description": "በጊዜ መስመር በነባሪነት የተዘረጉትን የመሳሪያ ክፍሎችን አርትዕ፣ ጻፍ እና ጠጋኝ አሳይ", + "settings.general.row.newInterface.title": "አዲስ አቀማመጥ", + "settings.general.row.newInterface.badge": "አዲስ", + "settings.general.row.newInterface.description": "አዲሱን ትሮች እና የቤት አቀማመጥ ይጠቀሙ። ለተወሰነ ጊዜ በአቀማመጦች መካከል ይቀያይሩ።", + "settings.general.row.newInterfaceNotice.title": "አሁን አዲስ አቀማመጥ እየተጠቀሙ ነው", + "settings.general.row.newInterfaceNotice.description": "የቀድሞው አቀማመጥ ከአሁን በኋላ አይገኝም", + "settings.general.row.newInterfaceNotice.dismiss": "አሰናብት", + "settings.general.row.pinchZoom.title": "ለማጉላት ቆንጥጦ", + "settings.general.row.pinchZoom.description": "ትራክፓድ መቆንጠጥ እና የCtrl-ማሸብለል ምልክቶችን እንዲያሳድጉ ፍቀድ", + "settings.general.row.wayland.title": "ቤተኛ Wayland", + "settings.general.row.wayland.description": "X11መመለስን በWayland አሰናክል። እንደገና መጀመር ያስፈልገዋል።", + "settings.general.row.wayland.tooltip": "በLinux ላይ በተደባለቀ የማደስ-ተመን ማሳያዎች፣ ቤተኛ Wayland የበለጠ የተረጋጋ ሊሆን ይችላል።", + "settings.general.row.releaseNotes.title": "የልቀት ማስታወሻዎች", + "settings.general.row.releaseNotes.description": "ከዝማኔዎች በኋላ ምን አዲስ ብቅ-ባዮችን አሳይ", + "settings.updates.row.startup.title": "በጅማሬ ላይ ዝማኔዎችን ይመልከቱ", + "settings.updates.row.startup.description": "OpenCode ሲጀመር ዝማኔዎችን በራስ-ሰር ያረጋግጡ", + "settings.updates.row.check.title": "ዝማኔዎችን ይመልከቱ", + "settings.updates.row.check.description": "ዝማኔዎችን ለማግኘት በእጅ ያረጋግጡ እና ካለ ይጫኑ", + "settings.updates.action.checkNow": "አሁን ያረጋግጡ", + "settings.updates.action.checking": "በማጣራት ላይ...", + "settings.updates.action.downloading": "በማውረድ ላይ...", + "settings.updates.action.installing": "በመጫን ላይ...", + "settings.updates.toast.latest.title": "ዘመኑን ጠብቀዋል", + "settings.updates.toast.latest.description": "የቅርብ ጊዜውን የOpenCode እትም እያሄዱ ነው።", + "sound.option.none": "ምንም", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "ወኪል", + "settings.general.notifications.agent.description": "ወኪሉ ሲጠናቀቅ ወይም ትኩረት ሲፈልግ የስርዓት ማሳወቂያን አሳይ", + "settings.general.notifications.permissions.title": "ፍቃዶች", + "settings.general.notifications.permissions.description": "ፍቃድ በሚያስፈልግበት ጊዜ የስርዓት ማሳወቂያን አሳይ", + "settings.general.notifications.errors.title": "ስህተት", + "settings.general.notifications.errors.description": "ስህተት ሲከሰት የስርዓት ማሳወቂያን አሳይ", + "settings.general.sounds.agent.title": "ወኪል", + "settings.general.sounds.agent.description": "ወኪሉ ሲጠናቀቅ ወይም ትኩረት ሲፈልግ ድምጽ ያጫውቱ", + "settings.general.sounds.permissions.title": "ፍቃዶች", + "settings.general.sounds.permissions.description": "ፍቃድ በሚያስፈልግበት ጊዜ ድምጽ ያጫውቱ", + "settings.general.sounds.errors.title": "ስህተት", + "settings.general.sounds.errors.description": "ስህተት ሲከሰት ድምጽ ያጫውቱ", + "settings.shortcuts.title": "የቁልፍ ሰሌዳ አቋራጮች", + "settings.shortcuts.reset.button": "ወደ ነባሪዎች ዳግም አስጀምር", + "settings.shortcuts.reset.toast.title": "የአቋራጮች ዳግም ማስጀመር", + "settings.shortcuts.reset.toast.description": "የቁልፍ ሰሌዳ አቋራጮች ወደ ነባሪዎች ተቀይረዋል።", + "settings.shortcuts.conflict.title": "አቋራጭ አስቀድሞ ጥቅም ላይ ውሏል", + "settings.shortcuts.conflict.description": "{{keybind}} አስቀድሞ ለ{{titles}} ተመድቧል።", + "settings.shortcuts.unassigned": "ያልተመደበ", + "settings.shortcuts.pressKeys": "ቁልፎችን ይጫኑ", + "settings.shortcuts.search.placeholder": "አቋራጮችን ይፈልጉ", + "settings.shortcuts.search.empty": "ምንም አቋራጮች አልተገኙም", + "settings.shortcuts.group.general": "አጠቃላይ", + "settings.shortcuts.group.session": "ክፍለ ጊዜ", + "settings.shortcuts.group.navigation": "ዳሰሳ", + "settings.shortcuts.group.modelAndAgent": "ሞዴል እና ወኪል", + "settings.shortcuts.group.terminal": "ተርሚናል", + "settings.shortcuts.group.prompt": "ፕሮምፕት", + "settings.providers.title": "አቅራቢዎች", + "settings.providers.description": "የአቅራቢዎች መቼቶች እዚህ ሊዋቀሩ ይችላሉ።", + "settings.providers.section.connected": "የተገናኙ አቅራቢዎች", + "settings.providers.connected.empty": "ምንም የተገናኙ አቅራቢዎች የሉም", + "settings.providers.connected.environmentDescription": "ከአካባቢዎ ተለዋዋጮች ጋር የተገናኘ", + "settings.providers.section.popular": "ታዋቂ አቅራቢዎች", + "settings.providers.custom.description": "OpenAI-ተኳሃኝ አቅራቢን በመሠረት URL ያክሉ።", + "settings.providers.tag.environment": "አካባቢ", + "settings.providers.tag.config": "ውቅር", + "settings.providers.tag.custom": "ብጁ", + "settings.providers.tag.other": "ሌላ", + "settings.models.title": "ሞዴሎች", + "settings.models.description": "የሞዴል ቅንብሮች እዚህ ሊዋቀሩ ይችላሉ።", + "settings.agents.title": "ወኪሎች", + "settings.agents.description": "የወኪል ቅንብሮች እዚህ ይዋቀራሉ።", + "settings.commands.title": "ትዕዛዞች", + "settings.commands.description": "የትእዛዝ ቅንብሮች እዚህ ይዋቀራሉ።", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCPቅንብሮች እዚህ ይዋቀራሉ።", + "settings.permissions.title": "ፍቃዶች", + "settings.permissions.description": "አገልጋዩ በነባሪነት ምን ዓይነት መሳሪያዎችን መጠቀም እንደሚችል ይቆጣጠሩ።", + "settings.permissions.section.tools": "መሳሪያዎች", + "settings.permissions.toast.updateFailed.title": "ፍቃዶችን ማዘመን አልተሳካም", + "settings.permissions.action.allow": "ፍቀድ", + "settings.permissions.action.ask": "ጥያቄ", + "settings.permissions.action.deny": "ክድ", + "settings.permissions.tool.read.title": "አንብብ", + "settings.permissions.tool.read.description": "ፋይል ማንበብ (ከፋይል ዱካ ጋር ይዛመዳል)", + "settings.permissions.tool.edit.title": "አርትዕ", + "settings.permissions.tool.edit.description": "አርትዖቶችን፣ ጽሁፎችን እና መጠገኛዎችን ጨምሮ ፋይሎችን ይቀይሩ", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "የግሎብ ቅጦችን በመጠቀም ፋይሎችን አዛምድ", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "መደበኛ መግለጫዎችን በመጠቀም የፋይል ይዘቶችን ይፈልጉ", + "settings.permissions.tool.list.title": "ዝርዝር", + "settings.permissions.tool.list.description": "ፋይሎችን በማውጫ ውስጥ ይዘርዝሩ", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "የሼል ትዕዛዞችን አሂድ", + "settings.permissions.tool.task.title": "ተግባር", + "settings.permissions.tool.task.description": "ንዑስ ወኪሎችን አስጀምር", + "settings.permissions.tool.skill.title": "ክህሎት", + "settings.permissions.tool.skill.description": "ችሎታ በስም ጫን", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "የቋንቋ አገልጋይ ጥያቄዎችን አሂድ", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "የሚከናወኑ ሥራዎችን ዝርዝር ያዘምኑ", + "settings.permissions.tool.webfetch.title": "ድር ፈልጎ", + "settings.permissions.tool.webfetch.description": "ይዘትን ከURL አምጣ", + "settings.permissions.tool.websearch.title": "ድር ፍለጋ", + "settings.permissions.tool.websearch.description": "ድሩን ይፈልጉ", + "settings.permissions.tool.external_directory.title": "የውጭ ማውጫ", + "settings.permissions.tool.external_directory.description": "ፋይሎችን ከፕሮጀክት ማውጫ ውጭ ይድረሱ", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "ተመሳሳይ ግብዓት ያላቸው ተደጋጋሚ የመሳሪያ ጥሪዎችን አግኝ", + "session.delete.failed.title": "ክፍለ-ጊዜን መሰረዝ አልተሳካም", + "session.delete.title": "ክፍለ ጊዜን ሰርዝ", + "session.delete.confirm": 'ክፍለ-ጊዜውን ይሰርዙ"{{name}}?', + "session.delete.button": "ክፍለ ጊዜን ሰርዝ", + "workspace.new": "አዲስ workspace", + "workspace.type.local": "አካባቢ", + "workspace.type.sandbox": "ማጠሪያ", + "workspace.create.failed.title": "Workspace መፍጠር አልተሳካም", + "workspace.delete.failed.title": "Workspace መሰረዝ አልተሳካም", + "workspace.resetting.title": "Workspaceን ዳግም በማስጀመር ላይ", + "workspace.resetting.description": "ይህ አንድ ደቂቃ ሊወስድ ይችላል።", + "workspace.reset.failed.title": "Workspaceን ዳግም ማስጀመር አልተቻለም", + "workspace.reset.success.title": "Workspace ዳግም ተጀምሯል", + "workspace.reset.success.description": "Workspace አሁን ከነባሪው ቅርንጫፍ ጋር ይዛመዳል።", + "workspace.error.stillPreparing": "Workspace አሁንም በመዘጋጀት ላይ ነው", + "workspace.status.checking": "ያልተጣመሩ ለውጦችን በመፈተሽ ላይ...", + "workspace.status.error": "git ሁኔታን ማረጋገጥ አልተቻለም።", + "workspace.status.clean": "ምንም ያልተዋሃዱ ለውጦች አልተገኙም።", + "workspace.status.dirty": "ያልተቀላቀሉ ለውጦች በዚህ workspace ላይ ተገኝተዋል።", + "workspace.delete.title": "Workspaceን ሰርዝ", + "workspace.delete.confirm": 'Workspace "{{name}}" ይሰረዝ?', + "workspace.delete.button": "Workspaceን ሰርዝ", + "workspace.reset.title": "Workspaceን ዳግም አስጀምር", + "workspace.reset.confirm": 'Workspace "{{name}}" ዳግም ይጀመር?', + "workspace.reset.button": "Workspaceን ዳግም አስጀምር", + "workspace.reset.archived.none": "ምንም ንቁ ክፍለ ጊዜዎች አይቀመጡም።", + "workspace.reset.archived.one": "1 ክፍለ ጊዜ በማህደር ይቀመጣል።", + "workspace.reset.archived.many": "{{count}} ክፍለ-ጊዜዎች በማህደር ይቀመጣሉ።", + "workspace.reset.note": "ይህ workspaceን ከነባሪው ቅርንጫፍ ጋር እንዲመሳሰል ዳግም ያስጀምረዋል።", + "dialog.usageExceeded.dontShowAgain": "እንደገና አታሳይ", +} diff --git a/packages/app/src/i18n/az.ts b/packages/app/src/i18n/az.ts new file mode 100644 index 0000000000000000000000000000000000000000..d818ae57fd6c4f065a256f1a2783c12654932d03 --- /dev/null +++ b/packages/app/src/i18n/az.ts @@ -0,0 +1,1168 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Fayl", + "desktop.menu.edit": "Redaktə", + "desktop.menu.view": "Görünüş", + "desktop.menu.go": "Keçid", + "desktop.menu.window": "Pəncərə", + "desktop.menu.help": "Kömək", + "desktop.menu.checkForUpdates": "Yeniləmələri yoxla...", + "desktop.menu.settings": "Tənzimləmələr", + "desktop.menu.reloadWebview": "Webview-u yenidən yüklə", + "desktop.menu.restart": "Yenidən başlat", + "desktop.menu.exportLogs": "Jurnalları ixrac et...", + "desktop.menu.newSession": "Yeni sessiya", + "desktop.menu.openProject": "Layihəni aç...", + "desktop.menu.newWindow": "Yeni pəncərə", + "desktop.menu.closeWindow": "Pəncərəni bağla", + "desktop.menu.undo": "Geri al", + "desktop.menu.redo": "Yenidən et", + "desktop.menu.cut": "Kəs", + "desktop.menu.copy": "Kopyala", + "desktop.menu.paste": "Yapışdır", + "desktop.menu.delete": "Sil", + "desktop.menu.selectAll": "Hamısını seç", + "desktop.menu.toggleSidebar": "Yan paneli aç/bağla", + "desktop.menu.toggleTerminal": "Terminalı aç/bağla", + "desktop.menu.toggleFileTree": "Fayl ağacını aç/bağla", + "desktop.menu.reload": "Yenidən yüklə", + "desktop.menu.toggleDeveloperTools": "Tərtibatçı alətlərini aç/bağla", + "desktop.menu.actualSize": "Faktiki ölçü", + "desktop.menu.zoomIn": "Böyüt", + "desktop.menu.zoomOut": "Kiçilt", + "desktop.menu.toggleFullScreen": "Tam ekran rejimini aç/bağla", + "desktop.menu.back": "Geri", + "desktop.menu.forward": "İrəli", + "desktop.menu.previousSession": "Əvvəlki sessiya", + "desktop.menu.nextSession": "Növbəti sessiya", + "desktop.menu.previousProject": "Əvvəlki layihə", + "desktop.menu.nextProject": "Növbəti layihə", + "desktop.menu.minimize": "Kiçilt", + "desktop.menu.maximize": "Böyüt", + "desktop.menu.documentation": "OpenCode sənədləri", + "desktop.menu.supportForum": "Dəstək forumu", + "desktop.menu.shareFeedback": "Rəy bildir", + "desktop.menu.reportBug": "Xəta bildir", + "desktop.menu.ariaLabel": "OpenCode menyusu", + "desktop.updater.dialog.checkFailed.message": "Yeniləmələri yoxlamaq mümkün olmadı.", + "desktop.updater.dialog.checkFailed.title": "Yeniləmə xətası", + "desktop.updater.dialog.upToDate.message": "Ən son versiyadan istifadə edirsiniz.", + "desktop.updater.dialog.upToDate.title": "Yeniləmə yoxdur", + "desktop.updater.dialog.ready.message": "{{version}} yeniləməsi endirildi. İndi yenidən başlatmaq istəyirsiniz?", + "desktop.updater.dialog.ready.title": "Yeniləmə hazırdır", + "desktop.updater.dialog.restart": "Yenidən başlat", + "desktop.updater.dialog.later": "Daha sonra", + "desktop.recovery.action.relaunch": "Yenidən işə sal", + "desktop.recovery.action.exportLogs": "Jurnalları ixrac et", + "desktop.recovery.action.keepWaiting": "Gözləməyə davam et", + "desktop.recovery.action.quit": "Çıx", + "desktop.recovery.loadFailed": "OpenCode yüklənə bilmədi", + "desktop.recovery.terminated": "OpenCode pəncərəsinin işi gözlənilmədən dayandı", + "desktop.recovery.unresponsive": "OpenCode cavab vermir", + "desktop.recovery.unresponsive.detail": + "Tətbiqi yenidən işə sala, jurnalları aça və ya gözləməyə davam edə bilərsiniz.", + "desktop.recovery.loadFailed.detail": "Pəncərə: {{window}}\nURL: {{url}}\nXəta: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Pəncərə: {{window}}\nSəbəb: {{reason}}\nKod: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Qovluq seçin", + "desktop.dialog.chooseFile": "Fayl seçin", + "desktop.dialog.saveFile": "Faylı saxla", + "desktop.dialog.files": "Fayllar", + "desktop.server.local": "Yerli server", + "desktop.wsl.error.windowsOnly": "WSL yalnız Windows-da əlçatandır", + "desktop.wsl.error.unavailable": "WSL əlçatan deyil", + "desktop.wsl.error.listInstalled": "Quraşdırılmış WSL distribütivlərini siyahılamaq mümkün olmadı", + "desktop.wsl.error.listOnline": "Onlayn WSL distribütivlərini siyahılamaq mümkün olmadı", + "desktop.wsl.error.executeDistro": "Distribütivdə əmrləri icra etmək mümkün deyil", + "desktop.wsl.error.installWsl": "WSL quraşdırılması uğursuz oldu", + "desktop.wsl.error.installDistro": "Distribütivi quraşdırmaq mümkün olmadı: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode quraşdırılması uğursuz oldu", + "desktop.wsl.error.alreadyAdded": "{{distro}} artıq əlavə edilib", + "desktop.wsl.error.opencodeMissing": "Bu distribütivdə opencode quraşdırılmayıb", + "desktop.wsl.error.opencodeCannotRun": "opencode quraşdırılıb, lakin onu işə salmaq mümkün olmadı", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode {{distro}} distribütivində quraşdırılmayıb", + "desktop.wsl.error.updateVersion": + "OpenCode yeniləməsi tamamlandı, lakin {{distro}} hələ də {{installed}} versiyasını bildirir; gözlənilən versiya: {{expected}}", + "desktop.wsl.error.noVersion": "versiya yoxdur", + "desktop.wsl.error.serverExited": "WSL serveri başladıqdan sonra dayandı (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL serveri işlək vəziyyətə gəlməzdən əvvəl dayandı (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "{{distro}} üçün sidecar sağlamlıq yoxlamasının {{timeout}}ms gözləmə müddəti bitdi", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} üçün {{timeout}}ms gözləmə müddəti bitdi", + "desktop.wsl.error.failedPort": "Portu əldə etmək mümkün olmadı", + "desktop.picker.error.notSelected": "Seçim vasitəsində fayl seçilməyib", + "desktop.picker.error.sizeLimit": "Seçilmiş əlavələr {{limit}} MB limitini aşır", + "command.category.suggested": "Təklif olunan", + "command.category.view": "Görünüş", + "command.category.project": "Layihə", + "command.category.provider": "Provayder", + "command.category.server": "Server", + "command.category.session": "Sessiya", + "command.category.theme": "Mövzu", + "command.category.language": "Dil", + "command.category.file": "Fayl", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "İcazələr", + "command.category.workspace": "İş sahəsi", + "command.category.settings": "Tənzimləmələr", + "command.logs.export": "Jurnalları ixrac et", + "theme.scheme.system": "Sistem", + "theme.scheme.light": "Açıq", + "theme.scheme.dark": "Tünd", + "command.sidebar.toggle": "Yan paneli aç/bağla", + "command.project.open": "Layihəni aç", + "command.project.previous": "Əvvəlki layihə", + "command.project.next": "Növbəti layihə", + "command.project.index": "{{index}} nömrəli layihəyə keç", + "command.provider.connect": "Provayderi qoş", + "command.server.switch": "Serveri dəyiş", + "command.settings.open": "Tənzimləmələri aç", + "command.session.previous": "Əvvəlki sessiya", + "command.session.next": "Növbəti sessiya", + "command.session.previous.unseen": "Əvvəlki oxunmamış sessiya", + "command.session.next.unseen": "Növbəti oxunmamış sessiya", + "command.session.archive": "Sessiyanı arxivlə", + "command.palette": "Əmr paneli", + "command.theme.cycle": "Mövzunu dəyiş", + "command.theme.set": "Mövzudan istifadə et: {{theme}}", + "command.theme.scheme.cycle": "Rəng sxemini dəyiş", + "command.theme.scheme.set": "Rəng sxemindən istifadə et: {{scheme}}", + "command.language.cycle": "Dili dəyiş", + "command.language.set": "Dildən istifadə et: {{language}}", + "command.session.new": "Yeni sessiya", + "command.file.open": "Faylı aç", + "command.tab.close": "Tabı bağla", + "command.tab.reopenClosed": "Bağlanmış tabı yenidən aç", + "command.context.addSelection": "Seçimi kontekstə əlavə et", + "command.context.addSelection.description": "Cari fayldan seçilmiş sətirləri əlavə et", + "command.input.focus": "Giriş sahəsinə fokusla", + "command.terminal.toggle": "Terminalı aç/bağla", + "command.fileTree.toggle": "Fayl ağacını aç/bağla", + "command.review.toggle": "İcmalı aç/bağla", + "command.terminal.new": "Yeni terminal", + "command.terminal.new.description": "Yeni terminal tabı yarat", + "command.steps.toggle": "Addımları aç/bağla", + "command.steps.toggle.description": "Cari mesaj üçün addımları göstər və ya gizlət", + "command.message.previous": "Əvvəlki mesaj", + "command.message.previous.description": "Əvvəlki istifadəçi mesajına keç", + "command.message.next": "Növbəti mesaj", + "command.message.next.description": "Növbəti istifadəçi mesajına keç", + "command.model.choose": "Model seç", + "command.model.choose.description": "Fərqli model seç", + "command.mcp.toggle": "MCP-ləri aç/bağla", + "command.mcp.toggle.description": "MCP-ləri aç/bağla", + "command.agent.cycle": "Agenti dəyiş", + "command.agent.cycle.description": "Növbəti agentə keç", + "command.agent.cycle.reverse": "Agenti geri dəyiş", + "command.agent.cycle.reverse.description": "Əvvəlki agentə keç", + "command.model.variant.cycle": "Düşünmə səviyyəsini dəyiş", + "command.model.variant.cycle.description": "Növbəti səviyyəyə keç", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "İcazələri avtomatik qəbul et", + "command.permissions.autoaccept.disable": "İcazələrin avtomatik qəbulunu dayandır", + "command.workspace.toggle": "İş sahələrini aç/bağla", + "command.workspace.toggle.description": "Yan paneldə birdən çox iş sahəsini aktiv və ya deaktiv et", + "command.session.undo": "Geri al", + "command.session.undo.description": "Son mesajı geri al", + "command.session.redo": "Yenidən et", + "command.session.redo.description": "Son geri alınmış mesajı yenidən et", + "command.session.compact": "Sessiyanı sıxlaşdır", + "command.session.compact.description": "Kontekst həcmini azaltmaq üçün sessiyanı xülasə et", + "command.session.fork": "Mesajdan fork et", + "command.session.fork.description": "Əvvəlki mesajdan yeni sessiya yarat", + "command.session.share": "Sessiyanı paylaş", + "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ı", + "palette.group.commands": "Əmrlər", + "palette.group.files": "Fayllar", + "dialog.provider.search.placeholder": "Provayderləri axtar", + "dialog.provider.empty": "Provayder tapılmadı", + "dialog.provider.group.popular": "Populyar", + "dialog.provider.group.other": "Digər", + "dialog.provider.custom.label": "Xüsusi OpenAI-a uyğun provayder", + "dialog.provider.tag.recommended": "Tövsiyə olunan", + "dialog.provider.opencode.note": "Claude, GPT, Gemini və daha çoxu daxil olmaqla seçilmiş modellər", + "dialog.provider.opencode.tagline": "Etibarlı optimallaşdırılmış modellər", + "dialog.provider.opencodeGo.tagline": "Hamı üçün aşağı qiymətli abunəlik", + "dialog.provider.anthropic.note": "Pro və Max daxil olmaqla Claude modellərinə birbaşa giriş", + "dialog.provider.copilot.note": "GitHub Copilot vasitəsilə kodlaşdırma yardımı üçün AI modelləri", + "dialog.provider.openai.note": "Sürətli və bacarıqlı ümumi AI tapşırıqları üçün GPT modelləri", + "dialog.provider.google.note": "Sürətli, strukturlaşdırılmış cavablar üçün Gemini modelləri", + "dialog.provider.openrouter.note": "Bir provayderdən bütün dəstəklənən modellərə giriş", + "dialog.provider.vercel.note": "Ağıllı yönləndirmə ilə AI modellərinə vahid giriş", + "dialog.model.select.title": "Model seç", + "dialog.model.search.placeholder": "Modelləri axtar", + "dialog.model.empty": "Model nəticəsi yoxdur", + "dialog.model.manage": "Modelləri idarə et", + "dialog.model.manage.description": "Model seçicisində hansı modellərin görünəcəyini fərdiləşdir.", + "dialog.model.manage.provider.toggle": "Bütün {{provider}} modellərini aç/bağla", + "dialog.model.unpaid.freeModels.title": "OpenCode tərəfindən təqdim olunan pulsuz modellər", + "dialog.model.unpaid.addMore.title": "Populyar provayderlərdən daha çox model əlavə edin", + "dialog.model.unpaid.viewMoreProviders": "Daha 70+ provayderə baxın", + "dialog.provider.viewAll": "Daha çox provayder göstər", + "provider.connect.title": "{{provider}} provayderini qoş", + "provider.connect.title.anthropicProMax": "Claude Pro/Max ilə daxil ol", + "provider.connect.selectMethod": "{{provider}} üçün giriş üsulunu seçin.", + "provider.connect.method.apiKey": "API açarı", + "provider.connect.method.browser": "Brauzer", + "provider.connect.method.headless": "Başsız", + "provider.connect.status.inProgress": "Avtorizasiya davam edir...", + "provider.connect.status.waiting": "Avtorizasiya gözlənilir...", + "provider.connect.status.failed": "Avtorizasiya uğursuz oldu: {{error}}", + "provider.connect.apiKey.description": + "Hesabınızı qoşmaq və OpenCode-da {{provider}} modellərindən istifadə etmək üçün {{provider}} API açarınızı daxil edin.", + "provider.connect.apiKey.label": "{{provider}} API açarı", + "provider.connect.apiKey.placeholder": "API açarı", + "provider.connect.apiKey.required": "API açarı tələb olunur", + "provider.connect.opencodeZen.line1": + "OpenCode Zen sizə kodlaşdırma agentləri üçün etibarlı optimallaşdırılmış seçilmiş modellər dəstinə giriş verir.", + "provider.connect.opencodeZen.line2": + "Tək bir API açarı ilə Claude, GPT, Gemini, GLM və daha çox modellərə giriş əldə edəcəksiniz.", + "provider.connect.opencodeZen.visit.prefix": "", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " səhifəsinə daxil olub API açarınızı əldə edin.", + "provider.connect.oauth.code.visit.prefix": "", + "provider.connect.oauth.code.visit.link": "Bu linkə", + "provider.connect.oauth.code.visit.suffix": + " daxil olub hesabınızı qoşmaq və OpenCode-da {{provider}} modellərindən istifadə etmək üçün avtorizasiya kodunuzu əldə edin.", + "provider.connect.oauth.code.label": "{{method}} avtorizasiya kodu", + "provider.connect.oauth.code.placeholder": "Avtorizasiya kodu", + "provider.connect.oauth.code.required": "Avtorizasiya kodu tələb olunur", + "provider.connect.oauth.code.invalid": "Yanlış avtorizasiya kodu", + "provider.connect.oauth.auto.visit.prefix": "", + "provider.connect.oauth.auto.visit.link": "Bu linkə", + "provider.connect.oauth.auto.visit.suffix": + " daxil olub aşağıdakı kodu daxil edərək hesabınızı qoşun və OpenCode-da {{provider}} modellərindən istifadə edin.", + "provider.connect.oauth.auto.confirmationCode": "Təsdiq kodu", + "provider.connect.toast.connected.title": "{{provider}} qoşuldu", + "provider.connect.toast.connected.description": "{{provider}} modelləri artıq istifadə üçün mövcuddur.", + "provider.custom.title": "Fərdi provayder", + "provider.custom.unavailable": "Fərdi provayderlər bu serverdə əlçatan deyil", + "provider.custom.description.prefix": "OpenAI ilə uyğun provayderi konfiqurasiya edin. ", + "provider.custom.description.link": "Provayder konfiqurasiyası sənədləri", + "provider.custom.description.suffix": "nə baxın.", + "provider.custom.field.providerID.label": "Provayder ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Kiçik hərflər, rəqəmlər, tire və ya alt xətt", + "provider.custom.field.name.label": "Görünən ad", + "provider.custom.field.name.placeholder": "Mənim AI Provayderim", + "provider.custom.field.baseURL.label": "Əsas URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API açarı", + "provider.custom.field.apiKey.placeholder": "API açarı", + "provider.custom.field.apiKey.description": + "İstəyə bağlıdır. Autentifikasiyanı başlıqlar vasitəsilə idarə edirsinizsə, boş buraxın.", + "provider.custom.models.label": "Modellər", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Ad", + "provider.custom.models.name.placeholder": "Görünən ad", + "provider.custom.models.remove": "Modeli sil", + "provider.custom.models.add": "Model əlavə et", + "provider.custom.headers.label": "Başlıqlar (istəyə bağlı)", + "provider.custom.headers.key.label": "Başlıq", + "provider.custom.headers.key.placeholder": "Başlıq-Adı", + "provider.custom.headers.value.label": "Dəyər", + "provider.custom.headers.value.placeholder": "dəyər", + "provider.custom.headers.remove": "Başlığı sil", + "provider.custom.headers.add": "Başlıq əlavə et", + "provider.custom.error.providerID.required": "Provayder ID tələb olunur", + "provider.custom.error.providerID.format": "Kiçik hərflər, rəqəmlər, tire və ya alt xətt istifadə edin", + "provider.custom.error.providerID.exists": "Bu provayder ID artıq mövcuddur", + "provider.custom.error.name.required": "Görünən ad tələb olunur", + "provider.custom.error.baseURL.required": "Əsas URL tələb olunur", + "provider.custom.error.baseURL.format": "http:// və ya https:// ilə başlamalıdır", + "provider.custom.error.required": "Tələb olunur", + "provider.custom.error.duplicate": "Təkrar", + "provider.disconnect.toast.disconnected.title": "{{provider}} ayrıldı", + "provider.disconnect.toast.disconnected.description": "{{provider}} modelləri artıq mövcud deyil.", + "model.tag.free": "Pulsuz", + "model.tag.latest": "Ən son", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "mətn", + "model.input.image": "şəkil", + "model.input.audio": "səs", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Dəstəkləyir: {{inputs}}", + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Girişlər", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Provayder", + "model.tooltip.reasoning": "Düşünmə", + "model.tooltip.reasoning.allowed": "Düşünməni dəstəkləyir", + "model.tooltip.reasoning.none": "Düşünmə yoxdur", + "model.tooltip.context": "Kontekst limiti {{limit}}", + "common.search.placeholder": "Axtar", + "common.goBack": "Geri keç", + "common.goForward": "İrəli keç", + "common.loading": "Yüklənir", + "common.loading.ellipsis": "...", + "common.cancel": "Ləğv et", + "common.clear": "Təmizlə", + "common.open": "Aç", + "common.connect": "Qoş", + "common.disconnect": "Ayır", + "common.continue": "Davam et", + "common.submit": "Göndər", + "common.save": "Saxla", + "common.saving": "Saxlanılır...", + "common.default": "Standart", + "common.attachment": "əlavə", + "prompt.placeholder.shell": "Qabıq əmrini daxil edin... {{example}}", + "prompt.placeholder.normal": 'Nə istəsəniz soruşun... "{{example}}"', + "prompt.placeholder.simple": "Nə istəsəniz soruşun...", + "prompt.placeholder.summarizeComments": "Şərhləri xülasə et…", + "prompt.placeholder.summarizeComment": "Şərhi xülasə et…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "çıxmaq üçün esc", + "session.child.promptDisabled": "Alt agent sessiyalarına prompt göndərmək olmaz.", + "session.child.backToParent": "Əsas sessiyaya qayıt.", + "prompt.example.1": "Kod bazasında TODO-nu həll et", + "prompt.example.2": "Bu layihənin texnologiya yığını nədir?", + "prompt.example.3": "Xətalı testləri düzəlt", + "prompt.example.4": "Autentifikasiyanın necə işlədiyini izah et", + "prompt.example.5": "Təhlükəsizlik boşluqlarını tap və düzəlt", + "prompt.example.6": "İstifadəçi xidməti üçün vahid testləri əlavə et", + "prompt.example.7": "Bu funksiyanı daha oxunaqlı hala gətir", + "prompt.example.8": "Bu xəta nə deməkdir?", + "prompt.example.9": "Bu problemi sazlamağa kömək et", + "prompt.example.10": "API sənədləri yarat", + "prompt.example.11": "Verilənlər bazası sorğularını optimallaşdır", + "prompt.example.12": "Giriş doğrulaması əlavə et", + "prompt.example.13": "Bunun üçün yeni komponent yarat...", + "prompt.example.14": "Bu layihəni necə yerləşdirə bilərəm?", + "prompt.example.15": "Kodumu qabaqcıl təcrübələr baxımından nəzərdən keçir", + "prompt.example.16": "Bu funksiyaya xəta idarəetməsi əlavə et", + "prompt.example.17": "Bu regex nümunəsini izah et", + "prompt.example.18": "Bunu TypeScript-ə çevir", + "prompt.example.19": "Bütün kod bazasına jurnallaşdırma əlavə et", + "prompt.example.20": "Hansı asılılıqlar köhnəlib?", + "prompt.example.21": "Miqrasiya skripti yazmağıma kömək et", + "prompt.example.22": "Bu endpoint üçün keşləmə tətbiq et", + "prompt.example.23": "Bu siyahıya səhifələmə əlavə et", + "prompt.example.24": "Bunun üçün CLI əmri yarat...", + "prompt.example.25": "Burada mühit dəyişənləri necə işləyir?", + "prompt.popover.emptyResults": "Uyğun nəticə yoxdur", + "prompt.popover.emptyCommands": "Uyğun əmr yoxdur", + "prompt.dropzone.label": "Şəkilləri, PDF-ləri və ya mətn fayllarını bura buraxın", + "prompt.dropzone.file.label": "Faylı @ ilə qeyd etmək üçün buraxın", + "prompt.slash.badge.custom": "fərdi", + "prompt.slash.badge.skill": "bacarıq", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktiv", + "prompt.context.includeActiveFile": "Aktiv faylı daxil et", + "prompt.context.removeActiveFile": "Aktiv faylı kontekstdən sil", + "prompt.context.removeFile": "Faylı kontekstdən sil", + "prompt.action.attachFile": "Fayllar əlavə et", + "prompt.menu.addImagesAndFiles": "Fayllar və daha çox əlavə edin", + "prompt.menu.imagesAndFiles": "Şəkillər və fayllar", + "prompt.menu.commands": "Əmrlər", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Shell əmri", + "prompt.attachment.remove": "Əlavəni sil", + "prompt.action.send": "Göndər", + "prompt.action.stop": "Dayandır", + "prompt.toast.pasteUnsupported.title": "Dəstəklənməyən əlavə", + "prompt.toast.pasteUnsupported.description": "Buraya yalnız şəkillər, PDF-lər və ya mətn faylları əlavə edilə bilər.", + "prompt.toast.attachmentDuplicate.title": "Bu fayl artıq yüklənib", + "prompt.toast.modelAgentRequired.title": "Agent və model seçin", + "prompt.toast.modelAgentRequired.description": "Prompt göndərməzdən əvvəl agent və model seçin.", + "prompt.toast.worktreeCreateFailed.title": "Worktree yaradıla bilmədi", + "prompt.toast.sessionCreateFailed.title": "Sessiya yaradıla bilmədi", + "prompt.toast.shellSendFailed.title": "Shell əmri göndərilə bilmədi", + "prompt.toast.commandSendFailed.title": "Əmr göndərilə bilmədi", + "prompt.toast.promptSendFailed.title": "Prompt göndərilə bilmədi", + "prompt.toast.promptSendFailed.description": "Sessiya əldə edilə bilmədi", + "dialog.mcp.title": "MCP-lər", + "dialog.mcp.description": "{{total}}-dən {{enabled}} aktiv", + "dialog.mcp.empty": "MCP konfiqurasiya edilməyib", + "dialog.lsp.empty": "LSP-lər fayl növlərindən avtomatik aşkarlanır", + "dialog.plugins.empty": "Plaginlər opencode.json-da konfiqurasiya edilib", + "mcp.status.connected": "qoşulub", + "mcp.status.failed": "uğursuz", + "mcp.status.needs_auth": "avtorizasiya lazımdır", + "mcp.status.disabled": "deaktiv", + "mcp.auth.clickToAuthenticate": "Autentifikasiya üçün klikləyin", + "dialog.fork.empty": "Yeni sessiyaya ayırmaq üçün mesaj yoxdur", + "dialog.directory.search.placeholder": "Qovluqları axtar", + "dialog.directory.empty": "Qovluq tapılmadı", + "dialog.directory.action.selectFile": "Fayl seçin", + "dialog.directory.action.selectFolder": "Qovluğu seçin", + "dialog.directory.root": "Kök", + "dialog.directory.parent": "Üst qovluq", + "dialog.directory.readError": "Bu qovluğu oxumaq mümkün deyil", + "app.server.unreachable": "{{server}}-ə çatmaq mümkün olmadı", + "app.server.retrying": "Avtomatik olaraq yenidən cəhd edilir...", + "app.server.otherServers": "Digər serverlər", + "dialog.server.title": "Serverlər", + "dialog.server.description": "Bu tətbiqin qoşulduğu OpenCode serverini dəyişdirin.", + "dialog.server.search.placeholder": "Serverləri axtar", + "dialog.server.empty": "Hələ server yoxdur", + "dialog.server.add.title": "Server əlavə et", + "dialog.server.add.url": "Server ünvanı", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Serverə qoşulmaq mümkün olmadı", + "dialog.server.add.checking": "Yoxlanılır...", + "dialog.server.add.button": "Server əlavə et", + "dialog.server.add.name": "Server adı (istəyə bağlı)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "İstifadəçi adı (istəyə bağlı)", + "dialog.server.add.usernamePlaceholder": "istifadəçi adı", + "dialog.server.add.password": "Şifrə (istəyə bağlı)", + "dialog.server.add.passwordPlaceholder": "parol", + "dialog.server.edit.title": "Serveri redaktə et", + "dialog.server.default.title": "Standart server", + "dialog.server.default.description": + "Tətbiq açıldıqda yerli server başlatmaq əvəzinə bu serverə qoşulun. Yenidən başlatma tələb olunur.", + "dialog.server.default.none": "Server seçilməyib", + "dialog.server.default.set": "Cari serveri standart olaraq təyin et", + "dialog.server.default.clear": "Təmizlə", + "dialog.server.action.remove": "Serveri sil", + "dialog.server.menu.edit": "Redaktə et", + "dialog.server.menu.default": "Standart olaraq təyin et", + "dialog.server.menu.defaultRemove": "Standartı sil", + "dialog.server.menu.delete": "Sil", + "dialog.server.current": "Cari server", + "dialog.server.status.default": "Standart", + "wsl.server.add": "WSL server əlavə edin", + "wsl.server.addShort": "WSL əlavə edin", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL server", + "wsl.server.retryStart": "Yenidən başlamağa cəhd edin", + "wsl.server.updating": "Yenilənir...", + "wsl.onboarding.step.distro": "Distribütiv seçin", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL yoxlanılır...", + "wsl.onboarding.restartRequired": + "WSL-nin quraşdırılmasını başa çatdırmaq üçün Windows-un yenidən başlaması lazımdır.", + "wsl.onboarding.ready": "WSL hazırdır.", + "wsl.onboarding.required": "Davam etmək üçün WSL tələb olunur.", + "wsl.onboarding.checkingDistros": "Distribütivlər yoxlanılır...", + "wsl.onboarding.installingDistro": "{{distro}} quraşdırılır...", + "wsl.onboarding.checkingDistro": "{{distro}} yoxlanılır...", + "wsl.onboarding.listingDistros": "Distribütivlər siyahılanır...", + "wsl.onboarding.distroReady": "{{distro}} hazırdır.", + "wsl.onboarding.distroNotInstalled": "{{distro}} hələ quraşdırılmayıb.", + "wsl.onboarding.openDistroOnce": "Quraşdırmanı bitirmək üçün {{distro}}-ni bir dəfə açın.", + "wsl.onboarding.finishingDistro": "{{distro}} üçün quraşdırma tamamlanır.", + "wsl.onboarding.pickDistro": "Distribütiv seçin və ya aşağıdakılardan birini quraşdırın.", + "wsl.onboarding.checkingOpencode": "OpenCode yoxlanılır...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}}-də OpenCode yoxlanılır...", + "wsl.onboarding.updatingOpencode": "OpenCode yenilənir...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}}-də OpenCode yenilənir...", + "wsl.onboarding.updateOpencodeIn": "{{distro}}-də OpenCode-u yeniləyin.", + "wsl.onboarding.updateOpencode": "OpenCode-u yeniləyin", + "wsl.onboarding.opencodeReadyIn": "OpenCode {{distro}}-də hazırdır.", + "wsl.onboarding.opencodeReady": "OpenCode hazırdır.", + "wsl.onboarding.installOpencodeIn": "OpenCode-u {{distro}}-də quraşdırın.", + "wsl.onboarding.installOpencode": "OpenCode quraşdırın", + "wsl.onboarding.chooseDistroFirst": "Əvvəlcə distribütiv seçin.", + "wsl.onboarding.loadFailed": "WSL vəziyyətini yükləmək alınmadı.", + "wsl.onboarding.loading": "Yüklənir...", + "wsl.onboarding.installedDistros": "Quraşdırılmış distribütivlər", + "wsl.onboarding.checkAgain": "Yenidən yoxlayın", + "wsl.onboarding.distroStatus.ready": "Hazır", + "wsl.onboarding.distroStatus.checking": "Yoxlanır...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode quraşdırılmayıb", + "wsl.onboarding.distroStatus.missingTools": "Çatışmayan bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Dəstəklənmir · WSL 2 istifadə edin", + "wsl.onboarding.needAnotherDistro": "Başqa distribütiv lazımdır?", + "wsl.onboarding.needAnotherDistroHint": "WSL kataloqundan Linux paylamasını quraşdırın", + "wsl.onboarding.wslNotInstalled.title": "WSL quraşdırılmayıb", + "wsl.onboarding.wslNotInstalled.description": + "OpenCode WSL serveri əlavə etməzdən əvvəl WSL (Linux üçün Windows alt sistemi) tələb olunur", + "wsl.onboarding.wslUnavailable.title": "WSL mövcud deyil", + "wsl.onboarding.wslUnavailable.description": "OpenCode bu maşında WSL-i doğrulaya bilmədi.", + "wsl.onboarding.installWsl": "WSL quraşdırın", + "wsl.onboarding.windowsRestartRequired": + "WSL-nin quraşdırılmasını başa çatdırmaq üçün Windows-u yenidən başladın, sonra OpenCode-u yenidən açın.", + "wsl.onboarding.next": "Növbəti", + "wsl.onboarding.refresh": "Yeniləyin", + "wsl.onboarding.allDistrosAdded": "Bütün quraşdırılmış distribütivlər artıq əlavə edilib.", + "wsl.onboarding.noDistros": "Hələ distribütiv aşkarlanmayıb.", + "wsl.onboarding.install": "Quraşdırın", + "wsl.onboarding.installing": "Quraşdırılır...", + "wsl.onboarding.installDistro": "Distribütiv quraşdırın", + "wsl.onboarding.searchDistros": "Distribütivləri axtar", + "wsl.onboarding.wsl2Required": "WSL 2 tələb olunur.", + "wsl.onboarding.toolsRequired": "Bu distribütiv üçün bash və curl tələb olunur.", + "wsl.onboarding.openTerminal": "Terminalı aç", + "wsl.onboarding.path": "Yol: {{path}}", + "wsl.onboarding.notFound": "tapılmadı", + "wsl.onboarding.version": "Versiya: {{version}}", + "wsl.onboarding.unknown": "naməlum", + "wsl.onboarding.desktopVersion": "masaüstü {{version}}", + "wsl.onboarding.versionMismatch": "Quraşdırılmış versiya masaüstü proqram versiyasına uyğun gəlmir.", + "wsl.onboarding.adding": "Əlavə edilir...", + "help.tabs.toast.ariaLabel": "Tablarla tanışlıq. İşinizi və aktiv sessiyalarınızı tablarla təşkil edin", + "help.tabs.toast.dismiss": "Tablar haqqında məlumatı bağla", + "help.tabs.title": "Tablarla tanışlıq", + "help.tabs.description": "İşinizi və aktiv sessiyalarınızı tablarla təşkil edin", + "help.tabs.date": "14 iyul", + "help.tabs.introduction": "OpenCode Desktop indi tablar əsasında qurulub.", + "help.tabs.sessions": + "Yeni sessiyanı tabda başladın və ya layihələrinizdən mövcud sessiyanı açın. Yeni işə başlayanda yeni tab açın, işi bitirdikdə isə onu bağlayın.", + "help.tabs.organize": + "Bir neçə tabı açıq saxlamaq aktiv sessiyalarınızı təşkil etməyi asanlaşdırır. Onları saxlamağı planlaşdırırsınızsa, tablara yadda qalan adlar verin.", + "help.tabs.home": + "Bütün sessiya və layihələrinizi yeni Ana səhifədə tapa bilərsiniz. Sessiyanı seçdikdə o, tabda açılır.", + "help.tabs.persistence": "Tətbiqi yenidən açdıqda tablarınız açıq qalır.", + "help.tabs.worktrees": + "Yeni dizayn hələ Git Worktrees-i dəstəkləmir, lakin bu imkan tezliklə əlavə olunacaq. Əvvəlki tərtibatdan istifadə etməyə davam etmək istəyirsinizsə, Tənzimləmələrdə tərtibatlar arasında keçid edə bilərsiniz. Nəzərə alın ki, yeni tərtibat bir neçə həftədən sonra daimi olacaq.", + "server.row.noUsername": "istifadəçi adı yoxdur", + "dialog.project.edit.title": "Layihəni redaktə et", + "dialog.project.edit.name": "Ad", + "dialog.project.edit.icon": "İkon", + "dialog.project.edit.icon.alt": "Layihə ikonu", + "dialog.project.edit.icon.hint": "Şəkilə klikləyin və ya onu sürükləyin", + "dialog.project.edit.icon.recommended": "Tövsiyə: 128x128px", + "dialog.project.edit.color": "Rəng", + "dialog.project.edit.color.select": "{{color}} rəngini seçin", + "dialog.project.edit.worktree.startup": "İş sahəsi başlanğıc skripti", + "dialog.project.edit.worktree.startup.description": "Yeni iş sahəsi (worktree) yaradıldıqdan sonra işləyir.", + "dialog.project.edit.worktree.startup.placeholder": "məs. bun install", + "dialog.releaseNotes.action.getStarted": "Başla", + "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", + "context.breakdown.user": "İstifadəçi", + "context.breakdown.assistant": "Köməkçi", + "context.breakdown.tool": "Alət çağırışları", + "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", + "context.stats.model": "Model", + "context.stats.limit": "Kontekst limiti", + "context.stats.totalTokens": "Ümumi tokenlər", + "context.stats.usage": "İstifadə", + "context.stats.inputTokens": "Giriş tokenləri", + "context.stats.outputTokens": "Çıxış tokenləri", + "context.stats.reasoningTokens": "Düşünmə tokenləri", + "context.stats.cacheTokens": "Keş tokenləri (oxuma/yazma)", + "context.stats.userMessages": "İstifadəçi mesajları", + "context.stats.assistantMessages": "Köməkçi mesajları", + "context.stats.totalCost": "Ümumi xərc", + "context.stats.sessionCreated": "Sessiya yaradıldı", + "context.stats.lastActivity": "Son fəaliyyət", + "context.usage.tokens": "Tokenlər", + "context.usage.usage": "İstifadə", + "context.usage.cost": "Xərc", + "context.usage.clickToView": "Kontekstə baxmaq üçün klikləyin", + "context.usage.view": "Kontekst istifadəsinə bax", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Dil", + "toast.language.description": "{{language}} dilinə keçildi", + "toast.theme.title": "Mövzu dəyişdirildi", + "toast.scheme.title": "Rəng sxemi", + "toast.workspace.enabled.title": "İş sahələri aktivləşdirildi", + "toast.workspace.enabled.description": "Birdən çox worktree indi yan paneldə göstərilir", + "toast.workspace.disabled.title": "İş sahələri deaktiv edildi", + "toast.workspace.disabled.description": "Yan paneldə yalnız əsas worktree göstərilir", + "toast.permissions.autoaccept.on.title": "İcazələr avtomatik qəbul edilir", + "toast.permissions.autoaccept.on.description": "İcazə sorğuları avtomatik təsdiqlənəcək", + "toast.permissions.autoaccept.off.title": "İcazələrin avtomatik qəbulu dayandırıldı", + "toast.permissions.autoaccept.off.description": "İcazə sorğuları təsdiq tələb edəcək", + "toast.model.none.title": "Model seçilməyib", + "toast.model.none.description": "Bu sessiyanı xülasə etmək üçün provayder qoşun", + "toast.file.loadFailed.title": "Fayl yüklənə bilmədi", + "toast.file.listFailed.title": "Fayllar siyahılana bilmədi", + "toast.context.noLineSelection.title": "Sətir seçimi yoxdur", + "toast.context.noLineSelection.description": "Əvvəlcə fayl tabında sətir aralığı seçin.", + "toast.session.share.copyFailed.title": "URL buferə kopyalana bilmədi", + "toast.session.share.success.title": "Sessiya paylaşıldı", + "toast.session.share.success.description": "Paylaşma URL-si buferə kopyalandı!", + "toast.session.share.failed.title": "Sessiya paylaşıla bilmədi", + "toast.session.share.failed.description": "Sessiyanı paylaşarkən xəta baş verdi", + "toast.session.unshare.success.title": "Sessiyanın paylaşımı dayandırıldı", + "toast.session.unshare.success.description": "Sessiyanın paylaşımı uğurla dayandırıldı!", + "toast.session.unshare.failed.title": "Sessiyanın paylaşımı dayandırıla bilmədi", + "toast.session.unshare.failed.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", + "toast.update.description": "OpenCode-un yeni versiyası ({{version}}) quraşdırma üçün mövcuddur.", + "toast.update.action.installRestart": "Quraşdır və yenidən başlat", + "toast.update.action.notYet": "Hələ yox", + "error.page.title": "Xəta baş verdi", + "error.page.description": "Tətbiq yüklənərkən xəta baş verdi.", + "error.page.description.localServerStartup": "Lokal serveri işə salarkən xəta baş verdi.", + "error.page.details.label": "Xəta detalları", + "error.page.action.restart": "Yenidən başlat", + "error.page.action.report": "Xəta bildirin", + "error.page.action.reported": "Xəta bildirildi", + "error.page.action.exportLogs": "Qeydləri ixrac edin", + "error.page.action.checking": "Yoxlanılır...", + "error.page.action.checkUpdates": "Yeniləmələri yoxla", + "error.page.action.updateTo": "{{version}} versiyasına yenilə", + "error.page.circular": "[Dairəvi]", + "error.page.report.prefix": "Bu xətanı OpenCode komandasına", + "error.page.report.discord": "Discord-da bildirin", + "error.page.version": "Versiya: {{version}}", + "error.dev.rootNotFound": + "Kök element tapılmadı. index.html-ə əlavə etməyi unutmusunuz? Yoxsa id atributu səhv yazılıb?", + "error.serverSync.connectFailed": "Serverə qoşulmaq mümkün olmadı. `{{url}}`-də işləyən server varmı?", + "error.serverSDK.noServerAvailable": "Əlçatan server yoxdur", + "error.serverSDK.serverNotAvailable": "Server mövcud deyil", + "error.childStore.persistedCacheCreateFailed": "Qalıcı keş yaradıla bilmədi", + "error.childStore.persistedProjectMetadataCreateFailed": "Qalıcı layihə metadatası yaradıla bilmədi", + "error.childStore.persistedProjectIconCreateFailed": "Qalıcı layihə nişanı yaradıla bilmədi", + "error.childStore.storeCreateFailed": "Saxlanc yaradıla bilmədi", + "directory.error.invalidUrl": "URL-də yanlış qovluq.", + "error.chain.unknown": "Naməlum xəta", + "error.server.invalidConfiguration": "Yanlış konfiqurasiya", + "error.chain.causedBy": "Səbəb:", + "error.chain.apiError": "API xətası", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Təkrar cəhd mümkündür: {{retryable}}", + "error.chain.responseBody": "Cavab gövdəsi:\n{{body}}", + "error.chain.didYouMean": "Bunu nəzərdə tuturdunuz: {{suggestions}}", + "error.chain.modelNotFound": "Model tapılmadı: {{provider}}/{{model}}", + "error.chain.checkConfig": "Konfiqurasiyanızdakı (opencode.json) provayder/model adlarını yoxlayın", + "error.chain.mcpFailed": + 'MCP server "{{name}}" uğursuz oldu. Qeyd: OpenCode hələ MCP autentifikasiyasını dəstəkləmir.', + "error.chain.providerAuthFailed": "Provayder autentifikasiyası uğursuz oldu ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + '"{{provider}}" provayderinin işə salınması uğursuz oldu. Giriş məlumatlarını və konfiqurasiyanı yoxlayın.', + "error.chain.configJsonInvalid": "{{path}} ünvanındakı konfiqurasiya faylı etibarlı JSON(C) deyil", + "error.chain.configJsonInvalidWithMessage": + "{{path}} ünvanındakı konfiqurasiya faylı etibarlı JSON(C) deyil: {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} içindəki "{{dir}}" qovluğu etibarlı deyil. Qovluğu "{{suggestion}}" adına dəyişdirin və ya silin. Bu ümumi bir yazım xətasıdır.', + "error.chain.configFrontmatterError": "{{path}} faylındakı frontmatter təhlil edilə bilmədi:\n{{message}}", + "error.chain.configInvalid": "{{path}} ünvanındakı konfiqurasiya faylı etibarsızdır", + "error.chain.configInvalidWithMessage": "{{path}} ünvanındakı konfiqurasiya faylı etibarsızdır: {{message}}", + "notification.permission.title": "İcazə tələb olunur", + "notification.permission.description": "{{projectName}} layihəsində {{sessionTitle}} icazə tələb edir", + "notification.question.title": "Sual", + "notification.question.description": "{{projectName}} layihəsində {{sessionTitle}} sualı var", + "notification.action.goToSession": "Sessiyaya keç", + "notification.session.responseReady.title": "Cavab hazırdır", + "notification.session.error.title": "Sessiya xətası", + "notification.session.error.fallbackDescription": "Xəta baş verdi", + "home.recentProjects": "Son layihələr", + "home.empty.title": "Son layihə yoxdur", + "home.empty.description": "Yerli layihə açaraq başlayın", + "home.title": "Ana səhifə", + "home.projects": "Layihələr", + "home.project.add": "Layihə əlavə edin", + "home.recentlyClosed": "Bu yaxınlarda bağlandı", + "home.server.collapse": "Server layihələrini yığışdırın", + "home.server.expand": "Server layihələrini genişləndirin", + "home.sessions.search.placeholder": "Sessiyaları axtar", + "home.sessions.search.placeholder.scoped": "{{scope}}-də sessiyaları axtarın", + "home.sessions.search.sessions": "Sessiyalar", + "home.sessions.search.noResults": "{{query}} üçün sessiya tapılmadı", + "home.sessions.empty": "Burada hələ heç nə yoxdur", + "home.sessions.empty.description": "Başlamaq üçün sessiya yaradın", + "home.sessions.group.today": "Bu gün", + "home.sessions.group.yesterday": "Dünən", + "home.sessions.group.older": "Daha əvvəl", + "home.providerTip": + "Claude, GPT, Gemini və s. daxil olmaqla, digər modellərdən istifadə etmək üçün 75+ provayderə qoşulun", + "session.tab.session": "Sessiya", + "session.tab.review": "İcmal", + "session.tab.context": "Kontekst", + "session.tab.unknown": "Naməlum sessiya", + "session.panel.reviewAndFiles": "İcmal və fayllar", + "session.error.notFound": "Bu sessiyanı tapmaq mümkün deyil", + "session.error.notFound.description": "Bu tab artıq bu serverdə olmayan sessiyaya işarə edir.", + "session.error.notFound.closeTab": "Tabı bağla", + "session.error.serverConnection": "Bu serverə qoşulmaq mümkün deyil", + "session.review.filesChanged": "{{count}} fayl dəyişdi", + "session.review.change.one": "Dəyişiklik", + "session.review.change.other": "Dəyişikliklər", + "session.review.loadingChanges": "Dəyişikliklər yüklənir...", + "session.review.empty": "Bu sessiyada hələ dəyişiklik yoxdur", + "session.review.noVcs": "Git versiyaya nəzarət sistemi aşkarlanmadı; sessiya dəyişiklikləri göstərilməyəcək", + "session.review.noVcs.createGit.title": "Git repozitoriyası yaradın", + "session.review.noVcs.createGit.description": "Bu layihədə dəyişiklikləri izləyin, nəzərdən keçirin və ləğv edin", + "session.review.noVcs.createGit.actionLoading": "Git repozitoriyası yaradılır...", + "session.review.noVcs.createGit.action": "Git repozitoriyası yaradın", + "session.review.noSnapshot": + "Snapshot izləmə konfiqurasiyada deaktiv edilib, ona görə də sessiya dəyişiklikləri əlçatan deyil", + "session.review.noChanges": "Dəyişiklik yoxdur", + "session.review.noUncommittedChanges": "Hələ commit edilməmiş dəyişiklik yoxdur", + "session.review.noBranchChanges": "Hələ branch dəyişikliyi yoxdur", + "session.files.selectToOpen": "Açmaq üçün fayl seçin", + "session.files.all": "Bütün fayllar", + "session.files.empty": "Fayl yoxdur", + "session.files.binaryContent": "İkili fayl (məzmun göstərilə bilmir)", + "session.messages.renderEarlier": "Əvvəlki mesajları göstər", + "session.messages.loadingEarlier": "Əvvəlki mesajlar yüklənir...", + "session.messages.loadEarlier": "Əvvəlki mesajları yüklə", + "session.messages.loading": "Mesajlar yüklənir...", + "session.messages.jumpToLatest": "Ən sonuncuya keç", + "session.context.addToContext": "{{selection}} kontekstə əlavə et", + "session.todo.title": "Tapşırıqlar", + "session.todo.collapse": "Yığ", + "session.todo.expand": "Genişlət", + "session.todo.progress": "{{total}} tapşırıqdan {{done}}-i tamamlandı", + "session.question.progress": "{{total}} sualdan {{current}}-i", + "session.question.minimize": "Sualı kiçilt", + "session.question.restore": "Sualı bərpa edin", + "session.question.pending.one": "{{count}} cavab gözləyən sual", + "session.question.pending.other": "{{count}} cavab gözləyən sual", + "session.followupDock.summary.one": "{{count}} növbəyə qoyulmuş mesaj", + "session.followupDock.summary.other": "{{count}} növbəyə qoyulmuş mesajlar", + "session.followupDock.sendNow": "İndi göndər", + "session.followupDock.edit": "Redaktə et", + "session.followupDock.collapse": "Növbəyə qoyulmuş mesajları yığcamlaşdırın", + "session.followupDock.expand": "Növbəyə qoyulmuş mesajları genişləndirin", + "session.revertDock.summary.one": "{{count}} geri alınmış mesaj", + "session.revertDock.summary.other": "{{count}} geri alınmış mesaj", + "session.revertDock.collapse": "Geri alınmış mesajları yığcamlaşdırın", + "session.revertDock.expand": "Geri qaytarılmış mesajları genişləndirin", + "session.revertDock.restore": "Mesajı bərpa edin", + "session.new.title": "İstədiyinizi qurun", + "session.new.project.new": "Yeni layihə", + "session.new.project.search": "Layihələri axtarın", + "session.new.project.add": "Layihə əlavə edin", + "session.new.worktree.main": "Əsas branch", + "session.new.worktree.mainWithBranch": "Əsas branch ({{branch}})", + "session.new.worktree.create": "Yeni worktree yarat", + "session.new.workspace.runIn": "Sessiyanı burada işə sal", + "session.new.workspace.triggerLocal": "Yerli", + "session.new.workspace.local": "Yerli depo", + "session.new.workspace.existing": "İş sahəsi…", + "session.new.git.none": "Git yoxdur", + "session.new.lastModified": "Son dəyişiklik", + "session.header.search.placeholder": "{{project}} axtar", + "session.header.searchFiles": "Faylları axtar", + "session.header.openIn": "Burada aç:", + "session.header.open.action": "{{app}} tətbiqini aç", + "session.header.open.ariaLabel": "{{app}} tətbiqində aç", + "session.header.open.menu": "Seçimləri aç", + "session.header.open.copyPath": "Yolu kopyala", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "Fayl meneceri", + "session.header.reveal.finder": "Finder-də göstər", + "session.header.reveal.fileExplorer": "File Explorer-də göstər", + "session.header.reveal.containingFolder": "Tərkibindəki qovluğu açın", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Server konfiqurasiyaları", + "status.popover.tab.servers": "Serverlər", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plaginlər", + "status.popover.action.manageServers": "Serverləri idarə et", + "session.share.popover.title": "Vebdə dərc et", + "session.share.popover.description.shared": "Bu sessiya vebdə açıqdır. Linkə sahib olan hər kəs daxil ola bilər.", + "session.share.popover.description.unshared": + "Sessiyanı vebdə açıq paylaşın. Linkə sahib olan hər kəs daxil ola biləcək.", + "session.share.action.share": "Paylaş", + "session.share.action.publish": "Dərc et", + "session.share.action.publishing": "Dərc edilir...", + "session.share.action.unpublish": "Dərcdən çıxar", + "session.share.action.unpublishing": "Dərcdən çıxarılır...", + "session.share.action.view": "Bax", + "session.share.copy.copied": "Kopyalandı", + "session.share.copy.copyLink": "Linki kopyala", + "lsp.tooltip.none": "LSP server yoxdur", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Prompt yüklənir...", + "terminal.loading": "Terminal yüklənir...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Terminalı bağla", + "terminal.connectionLost.title": "Bağlantı kəsildi", + "terminal.connectionLost.abnormalClose": "WebSocket anormal bağlandı: {{code}}", + "terminal.connectionLost.description": "Terminal bağlantısı kəsildi. Bu server yenidən başladıqda baş verə bilər.", + "terminal.connectTicket.csrfError": + "PTY bağlantı bileti mənşə və ya CSRF yoxlaması zamanı rədd edildi. Serverin CORS konfiqurasiyasını yoxlayın.", + "terminal.connectTicket.statusError": "PTY bağlantı bileti {{status}} statusu ilə uğursuz oldu", + "titlebar.update": "Yenilə", + "titlebar.updateVersion": "Yenilə {{version}}", + "common.closeTab": "Tabı bağla", + "common.dismiss": "Bağla", + "common.moreCountSuffix": " (+{{count}} daha)", + "common.requestFailed": "Sorğu uğursuz oldu", + "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", + "common.close": "Bağla", + "common.edit": "Redaktə et", + "common.loadMore": "Daha çox yüklə", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Boşluq", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Sil", + "common.key.home": "Home", + "common.key.end": "Son", + "common.key.pageUp": "Səhifə Yuxarı", + "common.key.pageDown": "Səhifə Aşağı", + "common.key.insert": "Daxil et", + "common.unknown": "naməlum", + "common.time.justNow": "İndicə", + "common.time.minutesAgo.short": "{{count}}d əvvəl", + "common.time.hoursAgo.short": "{{count}}s əvvəl", + "common.time.daysAgo.short": "{{count}}g əvvəl", + "sidebar.menu.toggle": "Menyunu aç/bağla", + "sidebar.nav.projectsAndSessions": "Layihələr və sessiyalar", + "sidebar.settings": "Tənzimləmələr", + "sidebar.help": "Kömək", + "sidebar.workspaces.enable": "İş sahələrini aktivləşdir", + "sidebar.workspaces.disable": "İş sahələrini deaktiv et", + "sidebar.gettingStarted.title": "Başlanğıc", + "sidebar.gettingStarted.line1": "OpenCode-a pulsuz modellər daxildir, buna görə dərhal başlaya bilərsiniz.", + "sidebar.gettingStarted.line2": + "Claude, GPT, Gemini və s. kimi modellərdən istifadə etmək üçün istənilən provayderi qoşun.", + "sidebar.project.recentSessions": "Son sessiyalar", + "sidebar.project.viewAllSessions": "Bütün sessiyalara bax", + "sidebar.project.clearNotifications": "Bildirişləri təmizlə", + "sidebar.empty.title": "Heç bir layihə açılmayıb", + "sidebar.empty.description": "Başlamaq üçün bir layihə açın", + "debugBar.ariaLabel": "Tərtibatçı performans diaqnostikası", + "debugBar.na": "yox", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Sessiya səhifəsinə aid son tamamlanmış marşrut keçidi; marşrutlaşdırıcının başladığı andan keçid tamamlandıqdan sonrakı ilk göstərilməyədək ölçülür.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Son 5 saniyə üzrə saniyədə kadrların hərəkətli orta qiyməti.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Son 5 saniyədə ən uzun kadr müddəti.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Son 5 saniyədə 32 ms-dən uzun çəkən kadrlar.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Son 5 saniyədə bloklanma müddəti və uzun tapşırıqların sayı. Ən uzun tapşırıq: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Son 5 saniyədə müşahidə olunan ən uzun daxiletmə gecikməsi.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Son 5 saniyədə təxmini qarşılıqlı əlaqə müddəti. Bu, rəsmi Web Vitals INP göstəricisi deyil, ona bənzər göstəricidir.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Tətbiqin cari işləmə müddəti üzrə məcmu tərtibat sürüşməsi.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "İstifadə olunmuş JS yığını və yığın limiti. Yalnız Chromium.", + "debugBar.mem.tip": "İstifadə olunmuş JS yığını və yığın limiti. {{used}} və {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Bütün interaktiv elementlərə fokus üslublarını məcburi tətbiq et", + "debugBar.focus.on": "AÇIQ", + "debugBar.focus.off": "QAPALI", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Seçilmiş dili dəyişmədən bütün tətbiq üçün tərtibat istiqamətini məcburi təyin et", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Masaüstü", + "settings.section.server": "Server", + "settings.tab.general": "Ümumi", + "settings.tab.shortcuts": "Qısa yollar", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL inteqrasiyası", + "settings.desktop.wsl.description": "Windows-da WSL daxilində OpenCode serverini işlət.", + "settings.general.section.appearance": "Görünüş", + "settings.general.section.advanced": "Qabaqcıl", + "settings.general.section.notifications": "Sistem bildirişləri", + "settings.general.section.updates": "Yeniləmələr", + "settings.general.section.sounds": "Səs effektləri", + "settings.general.section.feed": "Lenta", + "settings.general.section.display": "Ekran", + "settings.general.row.language.title": "Dil", + "settings.general.row.language.description": "OpenCode üçün ekran dilini dəyişdirin", + "settings.general.row.shell.title": "Terminal qabığı", + "settings.general.row.shell.description": "Terminal və agent alətləri tərəfindən istifadə edilən qabıq", + "settings.general.row.shell.autoDefault": "Avtomatik (Standart)", + "settings.general.row.shell.terminalOnly": "yalnız terminal", + "settings.general.row.appearance.title": "Görünüş", + "settings.general.row.appearance.description": "OpenCode-un cihazınızda necə göründüyünü fərdiləşdirin", + "settings.general.row.colorScheme.title": "Rəng sxemi", + "settings.general.row.colorScheme.description": + "OpenCode-un sistem, açıq və ya tünd mövzudan istifadə etməsini seçin", + "settings.general.row.theme.title": "Mövzu", + "settings.general.row.theme.description": "OpenCode-un mövzusunu fərdiləşdirin.", + "settings.general.row.font.title": "Kod şrifti", + "settings.general.row.font.description": "Kod bloklarında istifadə olunan mono şrifti fərdiləşdirin", + "settings.general.row.terminalFont.title": "Terminal şrifti", + "settings.general.row.terminalFont.description": "Terminalda istifadə olunan şrifti fərdiləşdirin", + "settings.general.row.uiFont.title": "İnterfeys şrifti", + "settings.general.row.uiFont.description": "Bütün interfeysdə istifadə olunan şrifti fərdiləşdirin", + "settings.general.row.followup.title": "Sonrakı sorğuların davranışı", + "settings.general.row.followup.description": + "Sonrakı sorğuların dərhal yönləndirilməsini və ya növbədə gözləməsini seçin", + "settings.general.row.followup.option.queue": "Növbə", + "settings.general.row.followup.option.steer": "Yönləndir", + "settings.general.row.showFileTree.title": "Fayl ağacı", + "settings.general.row.showFileTree.description": "Seanslarda fayl ağacı panelini göstərin", + "settings.general.row.showNavigation.title": "Naviqasiya nəzarətləri", + "settings.general.row.showNavigation.description": + "Masaüstü tətbiqin başlıq zolağında geri və irəli düymələrini göstərin", + "settings.general.row.showSearch.title": "Komanda palitrası", + "settings.general.row.showSearch.description": "Başlıq çubuğunda axtarış və əmr palitrası düyməsini göstərin", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Masaüstü tətbiqin başlıq zolağında terminal düyməsini göstərin", + "settings.general.row.showStatus.title": "Server statusu", + "settings.general.row.showStatus.description": "Başlıq çubuğunda server statusu düyməsini göstərin", + "settings.general.row.mobileTitlebarBottom.title": "Aşağı naviqasiya", + "settings.general.row.mobileTitlebarBottom.description": + "Başlıq çubuğunu və sessiya tablarını mobil cihazda ekranın aşağısına yerləşdirin", + "settings.general.row.showCustomAgents.title": "Agenti göstər", + "settings.general.row.showCustomAgents.description": + "Mesaj sahəsində agentlər arasında keçid edin. Gizlədildikdə Build agenti seçilir.", + "settings.general.row.reasoningSummaries.title": "Düşünmə xülasələrini göstər", + "settings.general.row.reasoningSummaries.description": "Zaman xəttində modelin düşünmə xülasələrini göstər", + "settings.general.row.shellToolPartsExpanded.title": "Shell alət hissələrini genişlət", + "settings.general.row.shellToolPartsExpanded.description": + "Zaman xəttində shell alət hissələrini standart olaraq genişlədilmiş göstər", + "settings.general.row.editToolPartsExpanded.title": "Redaktə alət hissələrini genişlət", + "settings.general.row.editToolPartsExpanded.description": + "Zaman xəttində redaktə, yazma və patch alət hissələrini standart olaraq genişlədilmiş göstər", + "settings.general.row.newInterface.title": "Yeni tərtibat", + "settings.general.row.newInterface.badge": "Yeni", + "settings.general.row.newInterface.description": + "Yeni tab və ana səhifə tərtibatından istifadə edin. Məhdud müddət ərzində tərtibatlar arasında keçid edin.", + "settings.general.row.newInterfaceNotice.title": "İndi yeni tərtibatdan istifadə edirsiniz", + "settings.general.row.newInterfaceNotice.description": "Əvvəlki tərtibat artıq mövcud deyil", + "settings.general.row.newInterfaceNotice.dismiss": "Rədd et", + "settings.general.row.pinchZoom.title": "Böyütmək üçün çimdikləyin", + "settings.general.row.pinchZoom.description": + "Trekpadda çimdikləmə və Ctrl-sürüşdürmə jestləri ilə miqyaslamağa icazə verin", + "settings.general.row.wayland.title": "Nativ Wayland-dan istifadə et", + "settings.general.row.wayland.description": "Wayland-da X11 geri dönüşünü deaktiv et. Yenidən başlatma tələb olunur.", + "settings.general.row.wayland.tooltip": + "Qarışıq yeniləmə tezlikli monitorlu Linux-da nativ Wayland daha sabit ola bilər.", + "settings.general.row.releaseNotes.title": "Buraxılış qeydləri", + "settings.general.row.releaseNotes.description": "Yeniləmələrdən sonra yenilikləri göstər", + "settings.updates.row.startup.title": "Başlanğıcda yeniləmələri yoxla", + "settings.updates.row.startup.description": "OpenCode açıldıqda avtomatik yeniləmələri yoxla", + "settings.updates.row.check.title": "Yeniləmələri yoxla", + "settings.updates.row.check.description": "Əl ilə yeniləmələri yoxla və mövcud olduqda quraşdır", + "settings.updates.action.checkNow": "İndi yoxla", + "settings.updates.action.checking": "Yoxlanılır...", + "settings.updates.action.downloading": "Yüklənir...", + "settings.updates.action.installing": "Quraşdırılır...", + "settings.updates.toast.latest.title": "Ən son versiyadan istifadə edirsiniz", + "settings.updates.toast.latest.description": "OpenCode-un ən son versiyasından istifadə edirsiniz.", + "sound.option.none": "Yoxdur", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Agent tamamlandıqda və ya diqqət tələb etdikdə sistem bildirişi göstər", + "settings.general.notifications.permissions.title": "İcazələr", + "settings.general.notifications.permissions.description": "İcazə tələb olunduqda sistem bildirişi göstər", + "settings.general.notifications.errors.title": "Xətalar", + "settings.general.notifications.errors.description": "Xəta baş verdikdə sistem bildirişi göstər", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Agent tamamlandıqda və ya diqqət tələb etdikdə səs çal", + "settings.general.sounds.permissions.title": "İcazələr", + "settings.general.sounds.permissions.description": "İcazə tələb olunduqda səs çal", + "settings.general.sounds.errors.title": "Xətalar", + "settings.general.sounds.errors.description": "Xəta baş verdikdə səs çal", + "settings.shortcuts.title": "Klaviatura qısa yolları", + "settings.shortcuts.reset.button": "Standartlara sıfırla", + "settings.shortcuts.reset.toast.title": "Qısa yollar sıfırlandı", + "settings.shortcuts.reset.toast.description": "Klaviatura qısa yolları standartlara sıfırlandı.", + "settings.shortcuts.conflict.title": "Qısa yol artıq istifadə olunur", + "settings.shortcuts.conflict.description": "{{keybind}} artıq {{titles}} üçün təyin edilib.", + "settings.shortcuts.unassigned": "Təyin edilməyib", + "settings.shortcuts.pressKeys": "Düymələrə basın", + "settings.shortcuts.search.placeholder": "Qısa yolları axtar", + "settings.shortcuts.search.empty": "Qısa yol tapılmadı", + "settings.shortcuts.group.general": "Ümumi", + "settings.shortcuts.group.session": "Sessiya", + "settings.shortcuts.group.navigation": "Naviqasiya", + "settings.shortcuts.group.modelAndAgent": "Model və agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + "settings.providers.title": "Provayderlər", + "settings.providers.description": "Provayder tənzimləmələri burada konfiqurasiya edilə biləcək.", + "settings.providers.section.connected": "Qoşulmuş provayderlər", + "settings.providers.connected.empty": "Qoşulmuş provayder yoxdur", + "settings.providers.connected.environmentDescription": "Mühit dəyişənlərinizdən qoşulub", + "settings.providers.section.popular": "Populyar provayderlər", + "settings.providers.custom.description": "Əsas URL ilə OpenAI-uyğun provayder əlavə edin.", + "settings.providers.tag.environment": "Mühit", + "settings.providers.tag.config": "Konfiqurasiya", + "settings.providers.tag.custom": "Fərdi", + "settings.providers.tag.other": "Digər", + "settings.models.title": "Modellər", + "settings.models.description": "Model tənzimləmələri burada konfiqurasiya edilə biləcək.", + "settings.agents.title": "Agentlər", + "settings.agents.description": "Agent tənzimləmələri burada konfiqurasiya edilə biləcək.", + "settings.commands.title": "Əmrlər", + "settings.commands.description": "Əmr tənzimləmələri burada konfiqurasiya edilə biləcək.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP tənzimləmələri burada konfiqurasiya edilə biləcək.", + "settings.permissions.title": "İcazələr", + "settings.permissions.description": "Serverin standart olaraq hansı alətləri istifadə edə biləcəyini idarə edin.", + "settings.permissions.section.tools": "Alətlər", + "settings.permissions.toast.updateFailed.title": "İcazələr yenilənə bilmədi", + "settings.permissions.action.allow": "İcazə ver", + "settings.permissions.action.ask": "Soruş", + "settings.permissions.action.deny": "Rədd et", + "settings.permissions.tool.read.title": "Oxu", + "settings.permissions.tool.read.description": "Fayl oxuma (fayl yoluna uyğun gəlir)", + "settings.permissions.tool.edit.title": "Redaktə", + "settings.permissions.tool.edit.description": "Redaktə, yazma və yamaq tətbiqi daxil olmaqla faylları dəyişdir", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Glob nümunələri ilə faylları uyğunlaşdır", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Müntəzəm ifadələrlə fayl məzmununu axtar", + "settings.permissions.tool.list.title": "Siyahı", + "settings.permissions.tool.list.description": "Qovluq daxilindəki faylları siyahıla", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Shell əmrlərini işlət", + "settings.permissions.tool.task.title": "Tapşırıq", + "settings.permissions.tool.task.description": "Alt agentləri işə sal", + "settings.permissions.tool.skill.title": "Bacarıq", + "settings.permissions.tool.skill.description": "Adla bacarıq yüklə", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Dil server sorğularını işlət", + "settings.permissions.tool.todowrite.title": "Tapşırıq yaz", + "settings.permissions.tool.todowrite.description": "Tapşırıq siyahısını yenilə", + "settings.permissions.tool.webfetch.title": "Veb əldə et", + "settings.permissions.tool.webfetch.description": "URL-dən məzmun əldə et", + "settings.permissions.tool.websearch.title": "Veb axtarış", + "settings.permissions.tool.websearch.description": "Vebdə axtar", + "settings.permissions.tool.external_directory.title": "Xarici qovluq", + "settings.permissions.tool.external_directory.description": "Layihə qovluğundan kənardakı fayllara daxil ol", + "settings.permissions.tool.doom_loop.title": "Sonsuz dövr", + "settings.permissions.tool.doom_loop.description": "Eyni girişlə təkrarlanan alət çağırışlarını aşkarla", + "session.delete.failed.title": "Sessiya silinə bilmədi", + "session.delete.title": "Sessiyanı sil", + "session.delete.confirm": '"{{name}}" sessiyasını silmək istəyirsiniz?', + "session.delete.button": "Sessiyanı sil", + "workspace.new": "Yeni iş sahəsi", + "workspace.type.local": "yerli", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "İş sahəsi yaradıla bilmədi", + "workspace.delete.failed.title": "İş sahəsi silinə bilmədi", + "workspace.resetting.title": "İş sahəsi sıfırlanır", + "workspace.resetting.description": "Bu bir dəqiqə çəkə bilər.", + "workspace.reset.failed.title": "İş sahəsi sıfırlana bilmədi", + "workspace.reset.success.title": "İş sahəsi sıfırlandı", + "workspace.reset.success.description": "İş sahəsi indi standart branch ilə uyğunlaşır.", + "workspace.error.stillPreparing": "İş sahəsi hələ hazırlanır", + "workspace.status.checking": "Birləşdirilməmiş dəyişikliklər yoxlanılır...", + "workspace.status.error": "Git statusu doğrulana bilmədi.", + "workspace.status.clean": "Birləşdirilməmiş dəyişiklik aşkarlanmadı.", + "workspace.status.dirty": "Bu iş sahəsində birləşdirilməmiş dəyişikliklər aşkarlandı.", + "workspace.delete.title": "İş sahəsini sil", + "workspace.delete.confirm": '"{{name}}" iş sahəsini silmək istəyirsiniz?', + "workspace.delete.button": "İş sahəsini sil", + "workspace.reset.title": "İş sahəsini sıfırla", + "workspace.reset.confirm": '"{{name}}" iş sahəsini sıfırlamaq istəyirsiniz?', + "workspace.reset.button": "İş sahəsini sıfırla", + "workspace.reset.archived.none": "Heç bir aktiv sessiya arxivlənməyəcək.", + "workspace.reset.archived.one": "1 sessiya arxivlənəcək.", + "workspace.reset.archived.many": "{{count}} sessiya arxivlənəcək.", + "workspace.reset.note": "Bu iş sahəsini standart branch ilə uyğunlaşdırmaq üçün sıfırlayacaq.", +} diff --git a/packages/app/src/i18n/bg.ts b/packages/app/src/i18n/bg.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f63d1fa4b3ee32cc417f2c2ca161fd63d013e71 --- /dev/null +++ b/packages/app/src/i18n/bg.ts @@ -0,0 +1,1170 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Файл", + "desktop.menu.edit": "Редактиране", + "desktop.menu.view": "Преглед", + "desktop.menu.go": "върви", + "desktop.menu.window": "прозорец", + "desktop.menu.help": "Помощ", + "desktop.menu.checkForUpdates": "Проверете за актуализации...", + "desktop.menu.settings": "Настройки", + "desktop.menu.reloadWebview": "Презареди Webview", + "desktop.menu.restart": "Рестартирайте", + "desktop.menu.exportLogs": "Експортиране на регистрационни файлове...", + "desktop.menu.newSession": "Нова сесия", + "desktop.menu.openProject": "Отворете проекта...", + "desktop.menu.newWindow": "Нов прозорец", + "desktop.menu.closeWindow": "Затваряне на прозореца", + "desktop.menu.undo": "Отмяна", + "desktop.menu.redo": "Повторете", + "desktop.menu.cut": "Изрежете", + "desktop.menu.copy": "копие", + "desktop.menu.paste": "Залепете", + "desktop.menu.delete": "Изтриване", + "desktop.menu.selectAll": "Изберете Всички", + "desktop.menu.toggleSidebar": "Превключване на страничната лента", + "desktop.menu.toggleTerminal": "Превключване на терминала", + "desktop.menu.toggleFileTree": "Превключване на файловото дърво", + "desktop.menu.reload": "Презареди", + "desktop.menu.toggleDeveloperTools": "Превключване на инструментите за разработчици", + "desktop.menu.actualSize": "Действителен размер", + "desktop.menu.zoomIn": "Увеличете", + "desktop.menu.zoomOut": "Намаляване", + "desktop.menu.toggleFullScreen": "Превключване на цял екран", + "desktop.menu.back": "Назад", + "desktop.menu.forward": "Напред", + "desktop.menu.previousSession": "Предишна сесия", + "desktop.menu.nextSession": "Следваща сесия", + "desktop.menu.previousProject": "Предишен проект", + "desktop.menu.nextProject": "Следващ проект", + "desktop.menu.minimize": "Минимизиране", + "desktop.menu.maximize": "Увеличете максимално", + "desktop.menu.documentation": "OpenCode Документация", + "desktop.menu.supportForum": "Форум за поддръжка", + "desktop.menu.shareFeedback": "Споделете обратна връзка", + "desktop.menu.reportBug": "Докладване за грешка", + "desktop.menu.ariaLabel": "OpenCode меню", + "desktop.updater.dialog.checkFailed.message": "Проверката на актуализацията е неуспешна.", + "desktop.updater.dialog.checkFailed.title": "Грешка при актуализиране", + "desktop.updater.dialog.upToDate.message": "Вие сте в крак с времето.", + "desktop.updater.dialog.upToDate.title": "Няма актуализации", + "desktop.updater.dialog.ready.message": "Актуализацията {{version}} е изтеглена. Рестартиране сега?", + "desktop.updater.dialog.ready.title": "Актуализацията е готова", + "desktop.updater.dialog.restart": "Рестартирайте", + "desktop.updater.dialog.later": "по-късно", + "desktop.recovery.action.relaunch": "Рестартирайте", + "desktop.recovery.action.exportLogs": "Експортиране на регистрационни файлове", + "desktop.recovery.action.keepWaiting": "Продължете да чакате", + "desktop.recovery.action.quit": "Откажете се", + "desktop.recovery.loadFailed": "OpenCode не успя да се зареди", + "desktop.recovery.terminated": "Прозорецът OpenCode приключи неочаквано", + "desktop.recovery.unresponsive": "OpenCode не отговаря", + "desktop.recovery.unresponsive.detail": + "Можете да рестартирате приложението, да отворите регистрационните файлове или да продължите да чакате.", + "desktop.recovery.loadFailed.detail": "Прозорец: {{window}}\nURL: {{url}}\nГрешка: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Прозорец: {{window}}\nПричина: {{reason}}\nКод: {{code}}", + "desktop.recovery.unknown": "<неизвестен>", + "desktop.dialog.chooseFolder": "Изберете папка", + "desktop.dialog.chooseFile": "Изберете файл", + "desktop.dialog.saveFile": "Запазете файла", + "desktop.dialog.files": "файлове", + "desktop.server.local": "Локален сървър", + "desktop.wsl.error.windowsOnly": "WSL се предлага само на Windows", + "desktop.wsl.error.unavailable": "WSL не е наличен", + "desktop.wsl.error.listInstalled": "Неуспешно показване на инсталирани WSL дистрибуции", + "desktop.wsl.error.listOnline": "Неуспешно изброяване на онлайн WSL дистрибуции", + "desktop.wsl.error.executeDistro": "Не може да изпълнява команди в дистрибуцията", + "desktop.wsl.error.installWsl": "Инсталирането на WSL е неуспешно", + "desktop.wsl.error.installDistro": "Неуспешно инсталиране на дистрибуция: {{distro}}", + "desktop.wsl.error.installOpencode": "Инсталирането на OpenCode е неуспешно", + "desktop.wsl.error.alreadyAdded": "{{distro}} вече е добавен", + "desktop.wsl.error.opencodeMissing": "opencode не е инсталиран в тази дистрибуция", + "desktop.wsl.error.opencodeCannotRun": "opencode е инсталиран, но не може да стартира", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode не е инсталиран в {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode актуализацията приключи, но {{distro}} все още съобщава {{installed}}; очаквано {{expected}}", + "desktop.wsl.error.noVersion": "няма версия", + "desktop.wsl.error.serverExited": "WSL сървърът излезе след стартиране (код={{code}} сигнал={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL сървърът излезе, преди да стане здрав (код={{code}} сигнал={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Времето за изчакване на допълнителната кола за {{distro}} проверка на здравето изтече след {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} времето за изчакване изтече след {{timeout}}ms", + "desktop.wsl.error.failedPort": "Неуспешно получаване на порт", + "desktop.picker.error.notSelected": "Файлът не е избран от инструмента за избор", + "desktop.picker.error.sizeLimit": "Избраните прикачени файлове надхвърлят ограничението от {{limit}} MB", + "command.category.suggested": "Предложено", + "command.category.view": "Преглед", + "command.category.project": "Проект", + "command.category.provider": "Доставчик", + "command.category.server": "сървър", + "command.category.session": "Сесия", + "command.category.theme": "Тема", + "command.category.language": "език", + "command.category.file": "Файл", + "command.category.context": "Контекст", + "command.category.terminal": "Терминал", + "command.category.model": "Модел", + "command.category.mcp": "MCP", + "command.category.agent": "агент", + "command.category.permissions": "Разрешения", + "command.category.workspace": "Работно пространство", + "command.category.settings": "Настройки", + "command.logs.export": "Експортиране на регистрационни файлове", + "theme.scheme.system": "система", + "theme.scheme.light": "светлина", + "theme.scheme.dark": "Тъмно", + "command.sidebar.toggle": "Превключване на страничната лента", + "command.project.open": "Отворен проект", + "command.project.previous": "Предишен проект", + "command.project.next": "Следващ проект", + "command.project.index": "Превключване към проект {{index}}", + "command.provider.connect": "Свържете доставчика", + "command.server.switch": "Превключете сървъра", + "command.settings.open": "Отворете настройките", + "command.session.previous": "Предишна сесия", + "command.session.next": "Следваща сесия", + "command.session.previous.unseen": "Предишна непрочетена сесия", + "command.session.next.unseen": "Следваща непрочетена сесия", + "command.session.archive": "Архивна сесия", + "command.palette": "Командна палитра", + "command.theme.cycle": "Тема за цикъл", + "command.theme.set": "Използвайте тема: {{theme}}", + "command.theme.scheme.cycle": "Цикъл на цветовата схема", + "command.theme.scheme.set": "Използвайте цветова схема: {{scheme}}", + "command.language.cycle": "Цикличен език", + "command.language.set": "Използвайте език: {{language}}", + "command.session.new": "Нова сесия", + "command.file.open": "Отворете файла", + "command.tab.close": "Затваряне на раздела", + "command.tab.reopenClosed": "Повторно отваряне на затворен раздел", + "command.context.addSelection": "Добавете селекция към контекста", + "command.context.addSelection.description": "Добавяне на избрани редове от текущия файл", + "command.input.focus": "Въвеждане на фокус", + "command.terminal.toggle": "Превключване на терминала", + "command.fileTree.toggle": "Превключване на файловото дърво", + "command.review.toggle": "Превключване на прегледа", + "command.terminal.new": "Нов терминал", + "command.terminal.new.description": "Създайте нов раздел на терминала", + "command.steps.toggle": "Превключване на стъпки", + "command.steps.toggle.description": "Показване или скриване на стъпки за текущото съобщение", + "command.message.previous": "Предишно съобщение", + "command.message.previous.description": "Отидете на предишното потребителско съобщение", + "command.message.next": "Следващо съобщение", + "command.message.next.description": "Отидете на следващото потребителско съобщение", + "command.model.choose": "Изберете модел", + "command.model.choose.description": "Изберете друг модел", + "command.mcp.toggle": "Превключване на MCPs", + "command.mcp.toggle.description": "Превключване на MCPs", + "command.agent.cycle": "Следващ агент", + "command.agent.cycle.description": "Преминете към следващия агент", + "command.agent.cycle.reverse": "Предишен агент", + "command.agent.cycle.reverse.description": "Преминете към предишния агент", + "command.model.variant.cycle": "Следващо ниво на разсъждение", + "command.model.variant.cycle.description": "Преминете към следващото ниво на усилие", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "подкана", + "command.permissions.autoaccept.enable": "Автоматично приемане на разрешения", + "command.permissions.autoaccept.disable": "Спрете автоматичното приемане на разрешения", + "command.workspace.toggle": "Превключване на работните пространства", + "command.workspace.toggle.description": + "Активирайте или деактивирайте няколко работни пространства в страничната лента", + "command.session.undo": "Отмяна", + "command.session.undo.description": "Отмяна на последното съобщение", + "command.session.redo": "Повторете", + "command.session.redo.description": "Повторете последното отменено съобщение", + "command.session.compact": "Компактна сесия", + "command.session.compact.description": "Обобщете сесията, за да намалите размера на контекста", + "command.session.fork": "Разклонение от съобщение", + "command.session.fork.description": "Създайте нова сесия от предишно съобщение", + "command.session.share": "Споделяне на сесия", + "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": "Няма намерени резултати", + "palette.group.commands": "Команди", + "palette.group.files": "файлове", + "dialog.provider.search.placeholder": "Доставчици на търсене", + "dialog.provider.empty": "Няма намерени доставчици", + "dialog.provider.group.popular": "Популярни", + "dialog.provider.group.other": "други", + "dialog.provider.custom.label": "Персонализиран OpenAI-съвместим доставчик", + "dialog.provider.tag.recommended": "Препоръчва се", + "dialog.provider.opencode.note": "Подбрани модели, включително Claude, GPT, Gemini и други", + "dialog.provider.opencode.tagline": "Надеждни оптимизирани модели", + "dialog.provider.opencodeGo.tagline": "Абонамент на ниска цена за всеки", + "dialog.provider.anthropic.note": "Директен достъп до Claude модели, включително Pro и Max", + "dialog.provider.copilot.note": "AI модели за помощ при кодиране чрез GitHub Copilot", + "dialog.provider.openai.note": "GPT модели за бързи, способни общи задачи с изкуствен интелект", + "dialog.provider.google.note": "Gemini модели за бързи, структурирани отговори", + "dialog.provider.openrouter.note": "Достъп до всички поддържани модели от един доставчик", + "dialog.provider.vercel.note": "Унифициран достъп до AI модели с интелигентно маршрутизиране", + "dialog.model.select.title": "Изберете модел", + "dialog.model.search.placeholder": "Търсене на модели", + "dialog.model.empty": "Няма резултати за модели", + "dialog.model.manage": "Управление на модели", + "dialog.model.manage.description": "Персонализирайте кои модели да се показват в селектора на модели.", + "dialog.model.manage.provider.toggle": "Превключване на всички {{provider}} модели", + "dialog.model.unpaid.freeModels.title": "Безплатни модели, предоставени от OpenCode", + "dialog.model.unpaid.addMore.title": "Добавете още модели от популярни доставчици", + "dialog.model.unpaid.viewMoreProviders": "Вижте още 70+ доставчици", + "dialog.provider.viewAll": "Показване на още доставчици", + "provider.connect.title": "Свържете {{provider}}", + "provider.connect.title.anthropicProMax": "Влезте с Claude Pro/Max", + "provider.connect.selectMethod": "Изберете метод за влизане за {{provider}}.", + "provider.connect.method.apiKey": "API ключ", + "provider.connect.method.browser": "Браузър", + "provider.connect.method.headless": "Без глава", + "provider.connect.status.inProgress": "В ход е оторизация...", + "provider.connect.status.waiting": "Чака се оторизация...", + "provider.connect.status.failed": "Упълномощаването е неуспешно: {{error}}", + "provider.connect.apiKey.description": + "Въведете своя {{provider}} API ключ, за да свържете акаунта си и да използвате {{provider}} модели в OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API ключ", + "provider.connect.apiKey.placeholder": "API ключ", + "provider.connect.apiKey.required": "Изисква се ключ API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ви дава достъп до подбран набор от надеждни оптимизирани модели за кодиращи агенти.", + "provider.connect.opencodeZen.line2": + "С един ключ API ще получите достъп до модели като Claude, GPT, Gemini, GLM и други.", + "provider.connect.opencodeZen.visit.prefix": "Посетете", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "за да вземете своя ключ API.", + "provider.connect.oauth.code.visit.prefix": "Посетете", + "provider.connect.oauth.code.visit.link": "тази връзка", + "provider.connect.oauth.code.visit.suffix": + "за да вземете вашия код за оторизация, за да свържете акаунта си и да използвате {{provider}} модели в OpenCode.", + "provider.connect.oauth.code.label": "{{method}} код за оторизация", + "provider.connect.oauth.code.placeholder": "Код за оторизация", + "provider.connect.oauth.code.required": "Изисква се код за оторизация", + "provider.connect.oauth.code.invalid": "Невалиден код за оторизация", + "provider.connect.oauth.auto.visit.prefix": "Посетете", + "provider.connect.oauth.auto.visit.link": "тази връзка", + "provider.connect.oauth.auto.visit.suffix": + "и въведете кода по-долу, за да свържете акаунта си и да използвате {{provider}} модели в OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Код за потвърждение", + "provider.connect.toast.connected.title": "{{provider}} свързан", + "provider.connect.toast.connected.description": "{{provider}} модела вече са налични за използване.", + "provider.custom.title": "Персонализиран доставчик", + "provider.custom.unavailable": "Персонализираните доставчици не са достъпни на този сървър", + "provider.custom.description.prefix": "Конфигурирайте OpenAI-съвместим доставчик. Вижте", + "provider.custom.description.link": "документи за конфигурация на доставчика", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID на доставчика", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Малки букви, цифри, тирета или долни черти", + "provider.custom.field.name.label": "Екранно име", + "provider.custom.field.name.placeholder": "Моят доставчик на AI", + "provider.custom.field.baseURL.label": "Основа URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API ключ", + "provider.custom.field.apiKey.placeholder": "API ключ", + "provider.custom.field.apiKey.description": + "Не е задължително. Оставете празно, ако управлявате удостоверяването чрез заглавки.", + "provider.custom.models.label": "Модели", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "модел-id", + "provider.custom.models.name.label": "Име", + "provider.custom.models.name.placeholder": "Екранно име", + "provider.custom.models.remove": "Премахване на модела", + "provider.custom.models.add": "Добавете модел", + "provider.custom.headers.label": "Заглавки (по избор)", + "provider.custom.headers.key.label": "Заглавка", + "provider.custom.headers.key.placeholder": "Заглавие-Име", + "provider.custom.headers.value.label": "Стойност", + "provider.custom.headers.value.placeholder": "стойност", + "provider.custom.headers.remove": "Премахване на заглавката", + "provider.custom.headers.add": "Добавете заглавка", + "provider.custom.error.providerID.required": "Изисква се ID на доставчика", + "provider.custom.error.providerID.format": "Използвайте малки букви, цифри, тирета или долни черти", + "provider.custom.error.providerID.exists": "Този идентификатор на доставчик вече съществува", + "provider.custom.error.name.required": "Изисква се екранно име", + "provider.custom.error.baseURL.required": "Изисква се база URL", + "provider.custom.error.baseURL.format": "Трябва да започва с http:// или https://", + "provider.custom.error.required": "Задължително", + "provider.custom.error.duplicate": "Дубликат", + "provider.disconnect.toast.disconnected.title": "{{provider}} прекъснат", + "provider.disconnect.toast.disconnected.description": "{{provider}} модели вече не са налични.", + "model.tag.free": "безплатно", + "model.tag.latest": "Последни", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "текст", + "model.input.image": "изображение", + "model.input.audio": "аудио", + "model.input.video": "видео", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Позволява: {{inputs}}", + "model.tooltip.context.label": "Контекст", + "model.tooltip.inputs": "Входове", + "model.tooltip.model": "Модел", + "model.tooltip.provider": "Доставчик", + "model.tooltip.reasoning": "Разсъждение", + "model.tooltip.reasoning.allowed": "Позволява разсъждение", + "model.tooltip.reasoning.none": "Без разсъждения", + "model.tooltip.context": "Контекстно ограничение {{limit}}", + "common.search.placeholder": "Търсене", + "common.goBack": "Навигирайте назад", + "common.goForward": "Навигирайте напред", + "common.loading": "Зарежда се", + "common.loading.ellipsis": "...", + "common.cancel": "Отказ", + "common.clear": "ясно", + "common.open": "Отворете", + "common.connect": "Свържете се", + "common.disconnect": "Прекъснете връзката", + "common.continue": "Продължи", + "common.submit": "Изпратете", + "common.save": "Запазване", + "common.saving": "Запазва се...", + "common.default": "По подразбиране", + "common.attachment": "прикачен файл", + "prompt.placeholder.shell": "Въведете команда shell... {{example}}", + "prompt.placeholder.normal": 'Попитайте каквото и да е... "{{example}}"', + "prompt.placeholder.simple": "Питайте нещо...", + "prompt.placeholder.summarizeComments": "Обобщете коментарите...", + "prompt.placeholder.summarizeComment": "Обобщение на коментара...", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "подкана", + "prompt.mode.shell.exit": "esc за изход", + "session.child.promptDisabled": "Субагентните сесии не могат да бъдат подканени.", + "session.child.backToParent": "Назад към основната сесия.", + "prompt.example.1": "Поправете TODO в кодовата база", + "prompt.example.2": "Какъв е техническият стек на този проект?", + "prompt.example.3": "Поправете повредени тестове", + "prompt.example.4": "Обяснете как работи удостоверяването", + "prompt.example.5": "Намерете и поправете уязвимости в сигурността", + "prompt.example.6": "Добавяне на модулни тестове за потребителската услуга", + "prompt.example.7": "Преработете тази функция, за да бъде по-четима", + "prompt.example.8": "Какво означава тази грешка?", + "prompt.example.9": "Помогнете ми да коригирам този проблем", + "prompt.example.10": "Генерирайте API документация", + "prompt.example.11": "Оптимизирайте заявките към базата данни", + "prompt.example.12": "Добавете проверка на входа", + "prompt.example.13": "Създайте нов компонент за...", + "prompt.example.14": "Как да разположа този проект?", + "prompt.example.15": "Прегледайте моя код за най-добри практики", + "prompt.example.16": "Добавете обработка на грешки към тази функция", + "prompt.example.17": "Обяснете този модел на регулярен израз", + "prompt.example.18": "Преобразувайте това в TypeScript", + "prompt.example.19": "Добавете регистриране в цялата кодова база", + "prompt.example.20": "Какви зависимости са остарели?", + "prompt.example.21": "Помогнете ми да напиша скрипт за миграция", + "prompt.example.22": "Приложете кеширане за тази крайна точка", + "prompt.example.23": "Добавете пагинация към този списък", + "prompt.example.24": "Създайте команда CLI за...", + "prompt.example.25": "Как работят променливите на средата тук?", + "prompt.popover.emptyResults": "Няма съответстващи резултати", + "prompt.popover.emptyCommands": "Няма съответстващи команди", + "prompt.dropzone.label": "Пуснете тук изображения, PDF или текстови файлове", + "prompt.dropzone.file.label": "Пуснете до файла @mention", + "prompt.slash.badge.custom": "обичай", + "prompt.slash.badge.skill": "умение", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "активен", + "prompt.context.includeActiveFile": "Включете активен файл", + "prompt.context.removeActiveFile": "Премахнете активния файл от контекста", + "prompt.context.removeFile": "Премахване на файла от контекста", + "prompt.action.attachFile": "Добавете файлове", + "prompt.menu.addImagesAndFiles": "Добавете файлове и др", + "prompt.menu.imagesAndFiles": "Изображения и файлове", + "prompt.menu.commands": "Команди", + "prompt.menu.context": "Контекст", + "prompt.menu.shellCommand": "Shell команда", + "prompt.attachment.remove": "Премахване на прикачения файл", + "prompt.action.send": "Изпратете", + "prompt.action.stop": "Спрете", + "prompt.toast.pasteUnsupported.title": "Неподдържан прикачен файл", + "prompt.toast.pasteUnsupported.description": "Тук могат да се прикачват само изображения, PDF или текстови файлове.", + "prompt.toast.attachmentDuplicate.title": "Този файл вече е качен", + "prompt.toast.modelAgentRequired.title": "Изберете агент и модел", + "prompt.toast.modelAgentRequired.description": "Изберете агент и модел, преди да изпратите подкана.", + "prompt.toast.worktreeCreateFailed.title": "Неуспешно създаване на работно дърво", + "prompt.toast.sessionCreateFailed.title": "Неуспешно създаване на сесия", + "prompt.toast.shellSendFailed.title": "Неуспешно изпращане на команда shell", + "prompt.toast.commandSendFailed.title": "Неуспешно изпращане на команда", + "prompt.toast.promptSendFailed.title": "Неуспешно изпращане на подкана", + "prompt.toast.promptSendFailed.description": "Не може да се извлече сесия", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} от {{total}} активирани", + "dialog.mcp.empty": "Няма конфигурирани MCP", + "dialog.lsp.empty": "LSPs автоматично разпознати от типове файлове", + "dialog.plugins.empty": "Добавки, конфигурирани в opencode.json", + "mcp.status.connected": "свързан", + "mcp.status.failed": "неуспешно", + "mcp.status.needs_auth": "се нуждае от авт", + "mcp.status.disabled": "деактивиран", + "mcp.auth.clickToAuthenticate": "Щракнете за удостоверяване", + "dialog.fork.empty": "Няма съобщения за разклонение", + "dialog.directory.search.placeholder": "Папки за търсене", + "dialog.directory.empty": "Няма намерени папки", + "dialog.directory.action.selectFile": "Изберете файл", + "dialog.directory.action.selectFolder": "Изберете папка", + "dialog.directory.root": "корен", + "dialog.directory.parent": "Родител", + "dialog.directory.readError": "Тази папка не може да се прочете", + "app.server.unreachable": "Не може да се достигне до {{server}}", + "app.server.retrying": "Опитва се автоматично...", + "app.server.otherServers": "Други сървъри", + "dialog.server.title": "Сървъри", + "dialog.server.description": "Превключете към кой OpenCode сървър се свързва това приложение.", + "dialog.server.search.placeholder": "Сървъри за търсене", + "dialog.server.empty": "Все още няма сървъри", + "dialog.server.add.title": "Добавете сървър", + "dialog.server.add.url": "Адрес на сървъра", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Не може да се свърже със сървъра", + "dialog.server.add.checking": "Проверка...", + "dialog.server.add.button": "Добавете сървър", + "dialog.server.add.name": "Име на сървъра (по избор)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "потребителско име (по избор)", + "dialog.server.add.usernamePlaceholder": "потребителско име", + "dialog.server.add.password": "Парола (по избор)", + "dialog.server.add.passwordPlaceholder": "парола", + "dialog.server.edit.title": "Редактиране на сървъра", + "dialog.server.default.title": "Сървър по подразбиране", + "dialog.server.default.description": + "Свържете се с този сървър при стартиране на приложението, вместо да стартирате локален сървър. Изисква рестартиране.", + "dialog.server.default.none": "Няма избран сървър", + "dialog.server.default.set": "Задаване на текущия сървър по подразбиране", + "dialog.server.default.clear": "ясно", + "dialog.server.action.remove": "Премахване на сървъра", + "dialog.server.menu.edit": "Редактиране", + "dialog.server.menu.default": "Задайте по подразбиране", + "dialog.server.menu.defaultRemove": "Премахване на подразбиране", + "dialog.server.menu.delete": "Изтриване", + "dialog.server.current": "Текущ сървър", + "dialog.server.status.default": "По подразбиране", + "wsl.server.add": "Добавяне на WSL сървър", + "wsl.server.addShort": "Добавете WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL сървър", + "wsl.server.retryStart": "Повторен старт", + "wsl.server.updating": "Актуализиране...", + "wsl.onboarding.step.distro": "Изберете дистрибуция", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Проверява се WSL...", + "wsl.onboarding.restartRequired": "Windows се нуждае от рестартиране, за да завърши инсталирането на WSL.", + "wsl.onboarding.ready": "WSL е готов.", + "wsl.onboarding.required": "WSL се изисква, за да продължите.", + "wsl.onboarding.checkingDistros": "Дистрибуции се проверяват...", + "wsl.onboarding.installingDistro": "Инсталиране на {{distro}}...", + "wsl.onboarding.checkingDistro": "Проверява се {{distro}}...", + "wsl.onboarding.listingDistros": "Списък на дистрибуции...", + "wsl.onboarding.distroReady": "{{distro}} е готов.", + "wsl.onboarding.distroNotInstalled": "{{distro}} все още не е инсталиран.", + "wsl.onboarding.openDistroOnce": "Отворете {{distro}} веднъж, за да завършите настройката.", + "wsl.onboarding.finishingDistro": "Завършва настройката за {{distro}}.", + "wsl.onboarding.pickDistro": "Изберете дистрибуция или инсталирайте такава по-долу.", + "wsl.onboarding.checkingOpencode": "Проверява се OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Проверява се OpenCode в {{distro}}...", + "wsl.onboarding.updatingOpencode": "Актуализиране на OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Актуализиране на OpenCode в {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Актуализирайте OpenCode в {{distro}}.", + "wsl.onboarding.updateOpencode": "Актуализация OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode е готов в {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode е готов.", + "wsl.onboarding.installOpencodeIn": "Инсталирайте OpenCode в {{distro}}.", + "wsl.onboarding.installOpencode": "Инсталирайте OpenCode", + "wsl.onboarding.chooseDistroFirst": "Първо изберете дистрибуция.", + "wsl.onboarding.loadFailed": "Неуспешно зареждане на състояние WSL.", + "wsl.onboarding.loading": "Зареждане...", + "wsl.onboarding.installedDistros": "Инсталирани дистрибуции", + "wsl.onboarding.checkAgain": "Проверете отново", + "wsl.onboarding.distroStatus.ready": "Готови", + "wsl.onboarding.distroStatus.checking": "Проверка...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode не е инсталиран", + "wsl.onboarding.distroStatus.missingTools": "Липсват bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Не се поддържа · Използвайте WSL 2", + "wsl.onboarding.needAnotherDistro": "Нуждаете се от друга дистрибуция?", + "wsl.onboarding.needAnotherDistroHint": "Инсталирайте дистрибуция Linux от каталога WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL не е инсталиран", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) се изисква, преди OpenCode да може да добави WSL сървър", + "wsl.onboarding.wslUnavailable.title": "WSL не е наличен", + "wsl.onboarding.wslUnavailable.description": "OpenCode не можа да провери WSL на тази машина.", + "wsl.onboarding.installWsl": "Инсталирайте WSL", + "wsl.onboarding.windowsRestartRequired": + "Рестартирайте Windows, за да завършите инсталирането на WSL, след което отворете отново OpenCode.", + "wsl.onboarding.next": "Следваща", + "wsl.onboarding.refresh": "Опресняване", + "wsl.onboarding.allDistrosAdded": "Всички инсталирани дистрибуции вече са добавени.", + "wsl.onboarding.noDistros": "Все още няма открити дистрибуции.", + "wsl.onboarding.install": "Инсталирайте", + "wsl.onboarding.installing": "Инсталира се...", + "wsl.onboarding.installDistro": "Инсталирайте дистрибуция", + "wsl.onboarding.searchDistros": "Търсете дистрибуции", + "wsl.onboarding.wsl2Required": "WSL 2 е задължително.", + "wsl.onboarding.toolsRequired": "Тази дистрибуция се нуждае от bash и curl.", + "wsl.onboarding.openTerminal": "Отворете терминала", + "wsl.onboarding.path": "Път: {{path}}", + "wsl.onboarding.notFound": "не е намерено", + "wsl.onboarding.version": "Версия: {{version}}", + "wsl.onboarding.unknown": "неизвестен", + "wsl.onboarding.desktopVersion": "работен плот {{version}}", + "wsl.onboarding.versionMismatch": "Инсталираната версия не съответства на версията на настолното приложение.", + "wsl.onboarding.adding": "Добавяне...", + "help.tabs.toast.ariaLabel": "Представяме ви раздели. Организирайте работата и активните си сесии с раздели", + "help.tabs.toast.dismiss": "Отхвърляне на информация за раздели", + "help.tabs.title": "Представяме ви раздели", + "help.tabs.description": "Организирайте работата и активните си сесии с раздели", + "help.tabs.date": "14 юли", + "help.tabs.introduction": "OpenCode Desktop вече е изграден около раздели.", + "help.tabs.sessions": + "Започнете нова сесия в раздел или отворете съществуваща сесия от някой от вашите проекти. Отворете нов раздел, когато започвате нещо ново, и го затворете, когато сте готови.", + "help.tabs.organize": + "Поддържането на няколко раздела отворени улеснява организирането на вашите активни сесии. Преименувайте раздели на нещо запомнящо се, ако планирате да ги запазите.", + "help.tabs.home": + "Ще намерите всичките си сесии и проекти на новия начален екран. Избирането на сесия я отваря в раздел.", + "help.tabs.persistence": "Когато отворите отново приложението, вашите раздели са все още отворени.", + "help.tabs.worktrees": + "Новият дизайн все още не поддържа Git Worktrees, идва скоро. Така че, ако предпочитате да продължите да използвате предишното оформление, можете да превключвате между оформления в Настройки. Само имайте предвид, че новото оформление ще стане постоянно след няколко седмици.", + "server.row.noUsername": "няма потребителско име", + "dialog.project.edit.title": "Редактиране на проекта", + "dialog.project.edit.name": "Име", + "dialog.project.edit.icon": "Икона", + "dialog.project.edit.icon.alt": "Икона на проекта", + "dialog.project.edit.icon.hint": "Щракнете или плъзнете изображение", + "dialog.project.edit.icon.recommended": "Препоръчително: 128x128px", + "dialog.project.edit.color": "Цвят", + "dialog.project.edit.color.select": "Изберете цвят {{color}}", + "dialog.project.edit.worktree.startup": "Скрипт за стартиране на работното пространство", + "dialog.project.edit.worktree.startup.description": + "Изпълнява се след създаване на ново работно пространство (worktree).", + "dialog.project.edit.worktree.startup.placeholder": "напр. bun install", + "dialog.releaseNotes.action.getStarted": "Започнете", + "dialog.releaseNotes.action.next": "Следваща", + "dialog.releaseNotes.action.hideFuture": "Не ги показвайте в бъдеще", + "dialog.releaseNotes.media.alt": "Визуализация на изданието", + "context.breakdown.title": "Разбивка на контекста", + "context.breakdown.note": + "Приблизителна разбивка на входните токени. „Други“ включва дефиниции на инструменти и режийни разходи.", + "context.breakdown.system": "система", + "context.breakdown.user": "Потребител", + "context.breakdown.assistant": "Помощник", + "context.breakdown.tool": "Извиквания на инструменти", + "context.breakdown.other": "други", + "context.systemPrompt.title": "Системен ред", + "context.rawMessages.title": "Сурови съобщения", + "context.export.session": "Експортиране на сесия", + "context.stats.session": "Сесия", + "context.stats.messages": "Съобщения", + "context.stats.provider": "Доставчик", + "context.stats.model": "Модел", + "context.stats.limit": "Ограничение на контекста", + "context.stats.totalTokens": "Общо токени", + "context.stats.usage": "Използване", + "context.stats.inputTokens": "Входни токени", + "context.stats.outputTokens": "Изходни токени", + "context.stats.reasoningTokens": "Разсъждаващи токени", + "context.stats.cacheTokens": "Кеш токени (четене/запис)", + "context.stats.userMessages": "Потребителски съобщения", + "context.stats.assistantMessages": "Асистентски съобщения", + "context.stats.totalCost": "Обща цена", + "context.stats.sessionCreated": "Сесията е създадена", + "context.stats.lastActivity": "Последна активност", + "context.usage.tokens": "Токени", + "context.usage.usage": "Използване", + "context.usage.cost": "цена", + "context.usage.clickToView": "Кликнете, за да видите контекста", + "context.usage.view": "Преглед на използването на контекста", + "language.en": "английски", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Полски", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "португалски (Бразилия)", + "language.bs": "Босански", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "турски", + "toast.language.title": "език", + "toast.language.description": "Превключихте към {{language}}", + "toast.theme.title": "Темата е сменена", + "toast.scheme.title": "Цветова схема", + "toast.workspace.enabled.title": "Работните пространства са активирани", + "toast.workspace.enabled.description": "Множество работни дървета вече се показват в страничната лента", + "toast.workspace.disabled.title": "Работните пространства са деактивирани", + "toast.workspace.disabled.description": "В страничната лента се показва само основното работно дърво", + "toast.permissions.autoaccept.on.title": "Автоматично приемане на разрешения", + "toast.permissions.autoaccept.on.description": "Заявките за разрешение ще бъдат автоматично одобрени", + "toast.permissions.autoaccept.off.title": "Автоматичното приемане на разрешения е спряно", + "toast.permissions.autoaccept.off.description": "Исканията за разрешение ще изискват одобрение", + "toast.model.none.title": "Няма избран модел", + "toast.model.none.description": "Свържете доставчик, за да обобщите тази сесия", + "toast.file.loadFailed.title": "Неуспешно зареждане на файла", + "toast.file.listFailed.title": "Неуспешно изброяване на файлове", + "toast.context.noLineSelection.title": "Няма избор на линия", + "toast.context.noLineSelection.description": "Първо изберете диапазон от редове в раздел на файл.", + "toast.session.share.copyFailed.title": "Неуспешно копиране на URL в клипборда", + "toast.session.share.success.title": "Сесията е споделена", + "toast.session.share.success.description": "Споделяне на URL копирано в клипборда!", + "toast.session.share.failed.title": "Неуспешно споделяне на сесията", + "toast.session.share.failed.description": "Възникна грешка при споделяне на сесията", + "toast.session.unshare.success.title": "Сесията е прекратена", + "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": "Налична актуализация", + "toast.update.description": "Вече е достъпна за инсталиране нова версия на OpenCode ({{version}}).", + "toast.update.action.installRestart": "Инсталирайте и рестартирайте", + "toast.update.action.notYet": "още не", + "error.page.title": "Нещо се обърка", + "error.page.description": "Възникна грешка при зареждане на приложението.", + "error.page.description.localServerStartup": "Възникна грешка при стартиране на локалния сървър.", + "error.page.details.label": "Подробности за грешката", + "error.page.action.restart": "Рестартирайте", + "error.page.action.report": "Докладване за грешка", + "error.page.action.reported": "Съобщава се за грешка", + "error.page.action.exportLogs": "Експортиране на регистрационни файлове", + "error.page.action.checking": "Проверка...", + "error.page.action.checkUpdates": "Проверете за актуализации", + "error.page.action.updateTo": "Актуализиране до {{version}}", + "error.page.circular": "[Кръгло]", + "error.page.report.prefix": "Моля, докладвайте тази грешка на екипа на OpenCode", + "error.page.report.discord": "на Discord", + "error.page.version": "Версия: {{version}}", + "error.dev.rootNotFound": + "Основният елемент не е намерен. Забравихте ли да го добавите към вашия index.html? Или може би атрибутът id е изписан неправилно?", + "error.serverSync.connectFailed": "Не може да се свърже със сървъра. Има ли сървър, работещ на `{{url}}`?", + "error.serverSDK.noServerAvailable": "Няма наличен сървър", + "error.serverSDK.serverNotAvailable": "Сървърът не е наличен", + "error.childStore.persistedCacheCreateFailed": "Неуспешно създаване на постоянен кеш", + "error.childStore.persistedProjectMetadataCreateFailed": "Неуспешно създаване на постоянни метаданни на проекта", + "error.childStore.persistedProjectIconCreateFailed": "Неуспешно създаване на икона на постоянен проект", + "error.childStore.storeCreateFailed": "Неуспешно създаване на магазин", + "directory.error.invalidUrl": "Невалидна директория в URL.", + "error.chain.unknown": "Неизвестна грешка", + "error.server.invalidConfiguration": "Невалидна конфигурация", + "error.chain.causedBy": "Причинено от:", + "error.chain.apiError": "API грешка", + "error.chain.status": "Статус: {{status}}", + "error.chain.retryable": "Повторен опит: {{retryable}}", + "error.chain.responseBody": "Тяло на отговора:\n{{body}}", + "error.chain.didYouMean": "Може би имахте предвид: {{suggestions}}", + "error.chain.modelNotFound": "Моделът не е намерен: {{provider}}/{{model}}", + "error.chain.checkConfig": "Проверете имената на доставчика/модела на вашата конфигурация (opencode.json).", + "error.chain.mcpFailed": + 'MCP сървър "{{name}}" не успя. Имайте предвид, че OpenCode все още не поддържа MCP удостоверяване.', + "error.chain.providerAuthFailed": "Неуспешно удостоверяване на доставчика ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + "Неуспешно инициализиране на доставчика „{{provider}}“. Проверете идентификационните данни и конфигурацията.", + "error.chain.configJsonInvalid": "Конфигурационният файл на адрес {{path}} не е валиден JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Конфигурационният файл на {{path}} не е валиден JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Директория "{{dir}}" в {{path}} не е валидна. Преименувайте директорията на "{{suggestion}}" или я премахнете. Това е често срещана печатна грешка.', + "error.chain.configFrontmatterError": "Неуспешно анализиране на frontmatter в {{path}}:\n{{message}}", + "error.chain.configInvalid": "Конфигурационният файл в {{path}} е невалиден", + "error.chain.configInvalidWithMessage": "Конфигурационният файл в {{path}} е невалиден: {{message}}", + "notification.permission.title": "Изисква се разрешение", + "notification.permission.description": "{{sessionTitle}} в {{projectName}} се нуждае от разрешение", + "notification.question.title": "Въпрос", + "notification.question.description": "{{sessionTitle}} в {{projectName}} има въпрос", + "notification.action.goToSession": "Отидете на сесия", + "notification.session.responseReady.title": "Готов отговор", + "notification.session.error.title": "Грешка в сесията", + "notification.session.error.fallbackDescription": "Възникна грешка", + "home.recentProjects": "Последни проекти", + "home.empty.title": "Няма скорошни проекти", + "home.empty.description": "Започнете, като отворите местен проект", + "home.title": "Начало", + "home.projects": "проекти", + "home.project.add": "Добавете проект", + "home.recentlyClosed": "Наскоро затворен", + "home.server.collapse": "Свиване на сървърни проекти", + "home.server.expand": "Разширете сървърните проекти", + "home.sessions.search.placeholder": "Сесии за търсене", + "home.sessions.search.placeholder.scoped": "Търсене на сесии в {{scope}}", + "home.sessions.search.sessions": "Сесии", + "home.sessions.search.noResults": "Няма намерени сесии за {{query}}", + "home.sessions.empty": "Тук още няма нищо", + "home.sessions.empty.description": "Създайте сесия, за да започнете", + "home.sessions.group.today": "Днес", + "home.sessions.group.yesterday": "Вчера", + "home.sessions.group.older": "По-стари", + "home.providerTip": + "Свържете се с над 75 доставчици, за да използвате други модели, включително Claude, GPT, Gemini и т.н.", + "session.tab.session": "Сесия", + "session.tab.review": "Преглед", + "session.tab.context": "Контекст", + "session.tab.unknown": "Неизвестна сесия", + "session.panel.reviewAndFiles": "Преглед и файлове", + "session.error.notFound": "Тази сесия не може да бъде намерена", + "session.error.notFound.description": "Този раздел сочи към сесия, която вече не съществува на този сървър.", + "session.error.notFound.closeTab": "Затваряне на раздела", + "session.error.serverConnection": "Не може да се свърже с този сървър", + "session.review.filesChanged": "Променени файлове {{count}}", + "session.review.change.one": "промяна", + "session.review.change.other": "Промени", + "session.review.loadingChanges": "Промените се зареждат...", + "session.review.empty": "Все още няма промени в тази сесия", + "session.review.noVcs": "Не е открита Git Система за контрол на версиите, промените не се показват", + "session.review.noVcs.createGit.title": "Създайте Git хранилище", + "session.review.noVcs.createGit.description": "Проследявайте, преглеждайте и отменяйте промените в този проект", + "session.review.noVcs.createGit.actionLoading": "Създаване на Git хранилище...", + "session.review.noVcs.createGit.action": "Създайте Git хранилище", + "session.review.noSnapshot": + "Проследяването на моментна снимка е деактивирано в конфигурацията, така че промените в сесията не са налични", + "session.review.noChanges": "Без промени", + "session.review.noUncommittedChanges": "Все още няма незавършени промени", + "session.review.noBranchChanges": "Все още няма промени в клона", + "session.files.selectToOpen": "Изберете файл за отваряне", + "session.files.all": "Всички файлове", + "session.files.empty": "Няма файлове", + "session.files.binaryContent": "Двоичен файл (съдържанието не може да се покаже)", + "session.messages.renderEarlier": "Изобразяване на по-ранни съобщения", + "session.messages.loadingEarlier": "Зареждат се предишни съобщения...", + "session.messages.loadEarlier": "Зареди по-ранни съобщения", + "session.messages.loading": "Съобщенията се зареждат...", + "session.messages.jumpToLatest": "Преминете към най-новото", + "session.context.addToContext": "Добавете {{selection}} към контекста", + "session.todo.title": "Задачи", + "session.todo.collapse": "Свиване", + "session.todo.expand": "Разширяване", + "session.todo.progress": "{{done}} от {{total}} задачи са изпълнени", + "session.question.progress": "{{current}} от {{total}} въпроса", + "session.question.minimize": "Минимизиране на въпроса", + "session.question.restore": "Възстановете въпроса", + "session.question.pending.one": "{{count}} чакащ въпрос", + "session.question.pending.other": "{{count}} висящи въпроси", + "session.followupDock.summary.one": "{{count}} съобщение в опашка", + "session.followupDock.summary.other": "{{count}} съобщения в опашка", + "session.followupDock.sendNow": "Изпратете сега", + "session.followupDock.edit": "Редактиране", + "session.followupDock.collapse": "Свиване на съобщенията в опашката", + "session.followupDock.expand": "Разгъване на съобщенията в опашката", + "session.revertDock.summary.one": "{{count}} върнато съобщение", + "session.revertDock.summary.other": "{{count}} отменени съобщения", + "session.revertDock.collapse": "Свиване на върнатите съобщения", + "session.revertDock.expand": "Разгъване на върнатите съобщения", + "session.revertDock.restore": "Възстановяване на съобщението", + "session.new.title": "Изградете каквото и да било", + "session.new.project.new": "Нов проект", + "session.new.project.search": "Търсене на проекти", + "session.new.project.add": "Добавете проект", + "session.new.worktree.main": "Основен клон", + "session.new.worktree.mainWithBranch": "Основен клон ({{branch}})", + "session.new.worktree.create": "Създайте ново работно дърво", + "session.new.workspace.runIn": "Стартирайте сесията", + "session.new.workspace.triggerLocal": "Местен", + "session.new.workspace.local": "Локално хранилище", + "session.new.workspace.existing": "Работно пространство…", + "session.new.git.none": "Не Git", + "session.new.lastModified": "Последна промяна", + "session.header.search.placeholder": "Търсене {{project}}", + "session.header.searchFiles": "Търсене на файлове", + "session.header.openIn": "Отвори в", + "session.header.open.action": "Отворете {{app}}", + "session.header.open.ariaLabel": "Отворете в {{app}}", + "session.header.open.menu": "Отворени опции", + "session.header.open.copyPath": "Копиране на пътя", + "session.header.open.finder": "Търсач", + "session.header.open.fileExplorer": "Файлов мениджър", + "session.header.open.fileManager": "Файлов мениджър", + "session.header.reveal.finder": "Разкриване във Finder", + "session.header.reveal.fileExplorer": "Разкриване в File Explorer", + "session.header.reveal.containingFolder": "Отворете съдържащата папка", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Терминал", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Статус", + "status.popover.ariaLabel": "Сървърни конфигурации", + "status.popover.tab.servers": "Сървъри", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Плъгини", + "status.popover.action.manageServers": "Управление на сървъри", + "session.share.popover.title": "Публикувайте в мрежата", + "session.share.popover.description.shared": "Тази сесия е публична в мрежата. Достъпен е за всеки с връзката.", + "session.share.popover.description.unshared": + "Споделете сесията публично в мрежата. Тя ще бъде достъпна за всеки с връзката.", + "session.share.action.share": "Споделете", + "session.share.action.publish": "Публикувай", + "session.share.action.publishing": "Публикуване...", + "session.share.action.unpublish": "Отмяна на публикуването", + "session.share.action.unpublishing": "Отменя се публикуването...", + "session.share.action.view": "Преглед", + "session.share.copy.copied": "Копирано", + "session.share.copy.copyLink": "Копиране на връзката", + "lsp.tooltip.none": "Няма LSP сървъри", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Подканата се зарежда...", + "terminal.loading": "Терминалът се зарежда...", + "terminal.title": "Терминал", + "terminal.title.numbered": "Терминал {{number}}", + "terminal.close": "Затворете терминала", + "terminal.connectionLost.title": "Загубена връзка", + "terminal.connectionLost.abnormalClose": "WebSocket затворено необичайно: {{code}}", + "terminal.connectionLost.description": + "Терминалната връзка беше прекъсната. Това може да се случи, когато сървърът се рестартира.", + "terminal.connectTicket.csrfError": + "PTY билет за свързване е отхвърлен от произход или CSRF проверки. Проверете конфигурацията на сървъра CORS.", + "terminal.connectTicket.statusError": "PTY неуспешно свързване на билет с {{status}}", + "titlebar.update": "Актуализация", + "titlebar.updateVersion": "Актуализация {{version}}", + "common.closeTab": "Затваряне на раздела", + "common.dismiss": "Отхвърляне", + "common.moreCountSuffix": "(+{{count}} още)", + "common.requestFailed": "Неуспешна заявка", + "common.moreOptions": "Още опции", + "common.learnMore": "Научете повече", + "common.rename": "Преименуване", + "common.export": "Експортиране", + "common.reset": "Нулиране", + "common.archive": "Архив", + "common.delete": "Изтриване", + "common.close": "затвори", + "common.edit": "Редактиране", + "common.loadMore": "Заредете повече", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "неизвестен", + "common.time.justNow": "Току-що", + "common.time.minutesAgo.short": "преди {{count}}м", + "common.time.hoursAgo.short": "преди {{count}}ч", + "common.time.daysAgo.short": "преди {{count}} дни", + "sidebar.menu.toggle": "Превключване на менюто", + "sidebar.nav.projectsAndSessions": "Проекти и сесии", + "sidebar.settings": "Настройки", + "sidebar.help": "Помощ", + "sidebar.workspaces.enable": "Активиране на работни пространства", + "sidebar.workspaces.disable": "Деактивирайте работните пространства", + "sidebar.gettingStarted.title": "Първи стъпки", + "sidebar.gettingStarted.line1": "OpenCode включва безплатни модели, така че можете да започнете веднага.", + "sidebar.gettingStarted.line2": "Свържете всеки доставчик, за да използвате модели, вкл. Claude, GPT, Gemini и т.н.", + "sidebar.project.recentSessions": "Последни сесии", + "sidebar.project.viewAllSessions": "Вижте всички сесии", + "sidebar.project.clearNotifications": "Изчистване на известията", + "sidebar.empty.title": "Няма отворени проекти", + "sidebar.empty.description": "Отворете проект, за да започнете", + "debugBar.ariaLabel": "Диагностика на ефективността на разработката", + "debugBar.na": "няма", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Последен завършен преход на маршрута, докосващ страница на сесия, измерен от началото на рутера до първото боядисване след установяването му.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Подвижни кадри в секунда през последните 5 секунди.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Най-лошото време за кадър през последните 5 секунди.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Кадри над 32 ms през последните 5 секунди.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Блокирано време и брой дълги задачи през последните 5 секунди. Максимална задача: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Най-лошото наблюдавано забавяне на въвеждане през последните 5 секунди.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Приблизителна продължителност на взаимодействието през последните 5 секунди. Това е подобно на INP, а не на официалните уеб показатели INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Кумулативна промяна на оформлението за текущия живот на приложението.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Използван JS heap срещу ограничение на heap. само Chromium.", + "debugBar.mem.tip": "Използван JS heap срещу ограничение на heap. {{used}} от {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Принудително фокусиране на стилове върху всички интерактивни елементи", + "debugBar.focus.on": "ВКЛ", + "debugBar.focus.off": "ИЗКЛ", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": + "Принудително насочване на цялата посока на оформлението на приложението, без да променя избрания език", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Работен плот", + "settings.section.server": "сървър", + "settings.tab.general": "генерал", + "settings.tab.shortcuts": "Преки пътища", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL интеграция", + "settings.desktop.wsl.description": "Стартирайте сървъра OpenCode вътре в WSL на Windows.", + "settings.general.section.appearance": "Външен вид", + "settings.general.section.advanced": "Разширено", + "settings.general.section.notifications": "Системни известия", + "settings.general.section.updates": "Актуализации", + "settings.general.section.sounds": "Звукови ефекти", + "settings.general.section.feed": "Храна", + "settings.general.section.display": "Дисплей", + "settings.general.row.language.title": "език", + "settings.general.row.language.description": "Променете езика на дисплея за OpenCode", + "settings.general.row.shell.title": "Терминал shell", + "settings.general.row.shell.description": "Shell използвани от инструментите на терминала и агента", + "settings.general.row.shell.autoDefault": "Автоматично (по подразбиране)", + "settings.general.row.shell.terminalOnly": "само терминал", + "settings.general.row.appearance.title": "Външен вид", + "settings.general.row.appearance.description": "Персонализирайте как изглежда OpenCode на вашето устройство", + "settings.general.row.colorScheme.title": "Цветова схема", + "settings.general.row.colorScheme.description": "Изберете дали OpenCode следва системната, светла или тъмна тема", + "settings.general.row.theme.title": "Тема", + "settings.general.row.theme.description": "Персонализирайте как е тематично OpenCode.", + "settings.general.row.font.title": "Шрифт на кода", + "settings.general.row.font.description": "Персонализирайте шрифта, използван в кодовите блокове", + "settings.general.row.terminalFont.title": "Терминален шрифт", + "settings.general.row.terminalFont.description": "Персонализирайте шрифта, използван в терминала", + "settings.general.row.uiFont.title": "UI шрифт", + "settings.general.row.uiFont.description": "Персонализирайте шрифта, използван в целия интерфейс", + "settings.general.row.followup.title": "Последващо поведение", + "settings.general.row.followup.description": + "Изберете дали последващите подкани да се управляват незабавно или да чакат на опашка", + "settings.general.row.followup.option.queue": "Опашка", + "settings.general.row.followup.option.steer": "Насочвайте", + "settings.general.row.showFileTree.title": "Файлово дърво", + "settings.general.row.showFileTree.description": "Показване на панела на файловото дърво в сесии", + "settings.general.row.showNavigation.title": "Контроли за навигация", + "settings.general.row.showNavigation.description": + "Показване на бутоните за връщане назад и напред в заглавната лента на работния плот", + "settings.general.row.showSearch.title": "Командна палитра", + "settings.general.row.showSearch.description": + "Показване на бутона за палитра за търсене и команди в заглавната лента", + "settings.general.row.showTerminal.title": "Терминал", + "settings.general.row.showTerminal.description": "Покажете бутона на терминала в заглавната лента на работния плот", + "settings.general.row.showStatus.title": "Състояние на сървъра", + "settings.general.row.showStatus.description": "Показване на бутона за състояние на сървъра в заглавната лента", + "settings.general.row.mobileTitlebarBottom.title": "Долна навигация", + "settings.general.row.mobileTitlebarBottom.description": + "Поставете заглавната лента и разделите на сесиите в долната част на екрана на мобилно устройство", + "settings.general.row.showCustomAgents.title": "Показване на агента", + "settings.general.row.showCustomAgents.description": + "Превключвайте между агенти в композитора. Когато е скрит, по подразбиране е Build agent.", + "settings.general.row.reasoningSummaries.title": "Показване на обобщения на разсъжденията", + "settings.general.row.reasoningSummaries.description": + "Показване на обобщения на разсъжденията на модела във времевата линия", + "settings.general.row.shellToolPartsExpanded.title": "Разгънете shell части на инструмента", + "settings.general.row.shellToolPartsExpanded.description": + "Показване на shell части от инструмента, разширени по подразбиране във времевата линия", + "settings.general.row.editToolPartsExpanded.title": "Разгънете части за редактиране на инструмента", + "settings.general.row.editToolPartsExpanded.description": + "Показване на части от инструмента за редактиране, писане и корекция, разширени по подразбиране във времевата линия", + "settings.general.row.newInterface.title": "Ново оформление", + "settings.general.row.newInterface.badge": "Нов", + "settings.general.row.newInterface.description": + "Използвайте новите раздели и началното оформление. Превключвайте между оформления за ограничено време.", + "settings.general.row.newInterfaceNotice.title": "Вече използвате ново оформление", + "settings.general.row.newInterfaceNotice.description": "Предишното оформление вече не е налично", + "settings.general.row.newInterfaceNotice.dismiss": "Отхвърляне", + "settings.general.row.pinchZoom.title": "Щипнете, за да увеличите", + "settings.general.row.pinchZoom.description": + "Разрешаване на жестове с щипване и Ctrl-превъртане на тракпада за мащабиране", + "settings.general.row.wayland.title": "Използвайте естествен Wayland", + "settings.general.row.wayland.description": "Деактивирайте X11 резервен вариант на Wayland. Изисква рестартиране.", + "settings.general.row.wayland.tooltip": + "На Linux с монитори със смесена честота на опресняване естественият Wayland може да бъде по-стабилен.", + "settings.general.row.releaseNotes.title": "Бележки по изданието", + "settings.general.row.releaseNotes.description": "Показване на изскачащи прозорци с новости след актуализации", + "settings.updates.row.startup.title": "Проверете за актуализации при стартиране", + "settings.updates.row.startup.description": "Автоматично проверявайте за актуализации при стартиране на OpenCode", + "settings.updates.row.check.title": "Проверете за актуализации", + "settings.updates.row.check.description": "Проверете ръчно за актуализации и инсталирайте, ако има такива", + "settings.updates.action.checkNow": "Проверете сега", + "settings.updates.action.checking": "Проверка...", + "settings.updates.action.downloading": "Изтегля се...", + "settings.updates.action.installing": "Инсталира се...", + "settings.updates.toast.latest.title": "Вие сте в крак с времето", + "settings.updates.toast.latest.description": "Вие използвате най-новата версия на OpenCode.", + "sound.option.none": "Няма", + "sound.option.alert01": "Сигнал 01", + "sound.option.alert02": "Сигнал 02", + "sound.option.alert03": "Сигнал 03", + "sound.option.alert04": "Сигнал 04", + "sound.option.alert05": "Сигнал 05", + "sound.option.alert06": "Сигнал 06", + "sound.option.alert07": "Сигнал 07", + "sound.option.alert08": "Сигнал 08", + "sound.option.alert09": "Сигнал 09", + "sound.option.alert10": "Сигнал 10", + "sound.option.bipbop01": "Бип-боп 01", + "sound.option.bipbop02": "Бип-боп 02", + "sound.option.bipbop03": "Бип-боп 03", + "sound.option.bipbop04": "Бип-боп 04", + "sound.option.bipbop05": "Бип-боп 05", + "sound.option.bipbop06": "Бип-боп 06", + "sound.option.bipbop07": "Бип-боп 07", + "sound.option.bipbop08": "Бип-боп 08", + "sound.option.bipbop09": "Бип-боп 09", + "sound.option.bipbop10": "Бип-боп 10", + "sound.option.staplebops01": "Стейпълбопс 01", + "sound.option.staplebops02": "Стейпълбопс 02", + "sound.option.staplebops03": "Стейпълбопс 03", + "sound.option.staplebops04": "Стейпълбопс 04", + "sound.option.staplebops05": "Стейпълбопс 05", + "sound.option.staplebops06": "Стейпълбопс 06", + "sound.option.staplebops07": "Стейпълбопс 07", + "sound.option.nope01": "Не 01", + "sound.option.nope02": "Не 02", + "sound.option.nope03": "Не 03", + "sound.option.nope04": "Не 04", + "sound.option.nope05": "Не 05", + "sound.option.nope06": "Не 06", + "sound.option.nope07": "Не 07", + "sound.option.nope08": "Не 08", + "sound.option.nope09": "Не 09", + "sound.option.nope10": "Не 10", + "sound.option.nope11": "Не 11", + "sound.option.nope12": "Не 12", + "sound.option.yup01": "Да 01", + "sound.option.yup02": "Да 02", + "sound.option.yup03": "Да 03", + "sound.option.yup04": "Да 04", + "sound.option.yup05": "Да 05", + "sound.option.yup06": "Да 06", + "settings.general.notifications.agent.title": "агент", + "settings.general.notifications.agent.description": + "Показване на системно известие, когато агентът е завършен или се нуждае от внимание", + "settings.general.notifications.permissions.title": "Разрешения", + "settings.general.notifications.permissions.description": + "Показване на системно известие, когато е необходимо разрешение", + "settings.general.notifications.errors.title": "Грешки", + "settings.general.notifications.errors.description": "Показване на системно известие при възникване на грешка", + "settings.general.sounds.agent.title": "агент", + "settings.general.sounds.agent.description": + "Възпроизвеждане на звук, когато агентът е завършен или се нуждае от внимание", + "settings.general.sounds.permissions.title": "Разрешения", + "settings.general.sounds.permissions.description": "Възпроизвеждане на звук, когато е необходимо разрешение", + "settings.general.sounds.errors.title": "Грешки", + "settings.general.sounds.errors.description": "Възпроизвеждане на звук при възникване на грешка", + "settings.shortcuts.title": "Клавишни комбинации", + "settings.shortcuts.reset.button": "Възстановяване на настройките по подразбиране", + "settings.shortcuts.reset.toast.title": "Нулиране на преките пътища", + "settings.shortcuts.reset.toast.description": "Клавишните комбинации са нулирани до стойностите по подразбиране.", + "settings.shortcuts.conflict.title": "Пряк път вече се използва", + "settings.shortcuts.conflict.description": "{{keybind}} вече е присвоен на {{titles}}.", + "settings.shortcuts.unassigned": "Неприсвоен", + "settings.shortcuts.pressKeys": "Натиснете клавишите", + "settings.shortcuts.search.placeholder": "Преки пътища за търсене", + "settings.shortcuts.search.empty": "Няма намерени преки пътища", + "settings.shortcuts.group.general": "генерал", + "settings.shortcuts.group.session": "Сесия", + "settings.shortcuts.group.navigation": "Навигация", + "settings.shortcuts.group.modelAndAgent": "Модел и агент", + "settings.shortcuts.group.terminal": "Терминал", + "settings.shortcuts.group.prompt": "подкана", + "settings.providers.title": "Доставчици", + "settings.providers.description": "Настройките на доставчика ще могат да се конфигурират тук.", + "settings.providers.section.connected": "Свързани доставчици", + "settings.providers.connected.empty": "Няма свързани доставчици", + "settings.providers.connected.environmentDescription": "Свързан от променливите на вашата среда", + "settings.providers.section.popular": "Популярни доставчици", + "settings.providers.custom.description": "Добавете OpenAI-съвместим доставчик по база URL.", + "settings.providers.tag.environment": "Околна среда", + "settings.providers.tag.config": "Конфиг", + "settings.providers.tag.custom": "По поръчка", + "settings.providers.tag.other": "други", + "settings.models.title": "Модели", + "settings.models.description": "Настройките на модела ще могат да се конфигурират тук.", + "settings.agents.title": "Агенти", + "settings.agents.description": "Настройките на агента ще могат да се конфигурират тук.", + "settings.commands.title": "Команди", + "settings.commands.description": "Настройките на командите ще могат да се конфигурират тук.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Настройките на MCP ще могат да се конфигурират тук.", + "settings.permissions.title": "Разрешения", + "settings.permissions.description": "Контролирайте какви инструменти сървърът може да използва по подразбиране.", + "settings.permissions.section.tools": "Инструменти", + "settings.permissions.toast.updateFailed.title": "Неуспешно актуализиране на разрешенията", + "settings.permissions.action.allow": "Разрешете", + "settings.permissions.action.ask": "Попитайте", + "settings.permissions.action.deny": "Откажи", + "settings.permissions.tool.read.title": "Прочетете", + "settings.permissions.tool.read.description": "Четене на файл (съвпада с пътя на файла)", + "settings.permissions.tool.edit.title": "Редактиране", + "settings.permissions.tool.edit.description": "Променете файлове, включително редакции, записи и корекции", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Съвпадение на файлове с помощта на glob модели", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Търсете съдържанието на файла с помощта на регулярни изрази", + "settings.permissions.tool.list.title": "списък", + "settings.permissions.tool.list.description": "Избройте файлове в директория", + "settings.permissions.tool.bash.title": "Баш", + "settings.permissions.tool.bash.description": "Изпълнете shell команди", + "settings.permissions.tool.task.title": "Задача", + "settings.permissions.tool.task.description": "Стартиране на подагенти", + "settings.permissions.tool.skill.title": "Умение", + "settings.permissions.tool.skill.description": "Заредете умение по име", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Изпълнявайте заявки за езиков сървър", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Актуализирайте списъка със задачи", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Извличане на съдържание от URL", + "settings.permissions.tool.websearch.title": "Търсене в мрежата", + "settings.permissions.tool.websearch.description": "Търсете в мрежата", + "settings.permissions.tool.external_directory.title": "Външен указател", + "settings.permissions.tool.external_directory.description": "Достъп до файлове извън директорията на проекта", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": + "Откриване на повтарящи се извиквания на инструменти с идентичен вход", + "session.delete.failed.title": "Неуспешно изтриване на сесията", + "session.delete.title": "Изтриване на сесия", + "session.delete.confirm": "Да се ​​изтрие ли сесията „{{name}}“?", + "session.delete.button": "Изтриване на сесия", + "workspace.new": "Ново работно пространство", + "workspace.type.local": "местен", + "workspace.type.sandbox": "пясъчник", + "workspace.create.failed.title": "Неуспешно създаване на работно пространство", + "workspace.delete.failed.title": "Неуспешно изтриване на работното пространство", + "workspace.resetting.title": "Нулиране на работното пространство", + "workspace.resetting.description": "Това може да отнеме минута.", + "workspace.reset.failed.title": "Неуспешно нулиране на работното пространство", + "workspace.reset.success.title": "Нулиране на работното пространство", + "workspace.reset.success.description": "Работното пространство вече съответства на клона по подразбиране.", + "workspace.error.stillPreparing": "Работното пространство все още се подготвя", + "workspace.status.checking": "Проверка за необединени промени...", + "workspace.status.error": "Не може да се провери състоянието на git.", + "workspace.status.clean": "Не са открити необединени промени.", + "workspace.status.dirty": "Открити са необединени промени в това работно пространство.", + "workspace.delete.title": "Изтриване на работното пространство", + "workspace.delete.confirm": "Да се ​​изтрие ли работното пространство „{{name}}“?", + "workspace.delete.button": "Изтриване на работното пространство", + "workspace.reset.title": "Нулирайте работното пространство", + "workspace.reset.confirm": "Нулиране на работното пространство „{{name}}“?", + "workspace.reset.button": "Нулирайте работното пространство", + "workspace.reset.archived.none": "Няма да се архивират активни сесии.", + "workspace.reset.archived.one": "1 сесия ще бъде архивирана.", + "workspace.reset.archived.many": "{{count}} сесии ще бъдат архивирани.", + "workspace.reset.note": "Това ще нулира работното пространство, за да съответства на клона по подразбиране.", + "dialog.usageExceeded.dontShowAgain": "Не показвай отново", +} diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts new file mode 100644 index 0000000000000000000000000000000000000000..feb41c651a87941c445286b106fafac8cfd20882 --- /dev/null +++ b/packages/app/src/i18n/bs.ts @@ -0,0 +1,1257 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Datoteka", + "desktop.menu.edit": "Uredi", + "desktop.menu.view": "Prikaz", + "desktop.menu.go": "Idi", + "desktop.menu.window": "Prozor", + "desktop.menu.help": "Pomoć", + "desktop.menu.checkForUpdates": "Provjeri ima li ažuriranja...", + "desktop.menu.settings": "Postavke", + "desktop.menu.reloadWebview": "Ponovo učitaj Webview", + "desktop.menu.restart": "Ponovo pokreni", + "desktop.menu.exportLogs": "Izvezi zapisnike...", + "desktop.menu.newSession": "Nova sesija", + "desktop.menu.openProject": "Otvori projekat...", + "desktop.menu.newWindow": "Novi prozor", + "desktop.menu.closeWindow": "Zatvori prozor", + "desktop.menu.undo": "Poništi", + "desktop.menu.redo": "Vrati", + "desktop.menu.cut": "Izreži", + "desktop.menu.copy": "Kopiraj", + "desktop.menu.paste": "Zalijepi", + "desktop.menu.delete": "Izbriši", + "desktop.menu.selectAll": "Odaberi sve", + "desktop.menu.toggleSidebar": "Prikaži ili sakrij bočnu traku", + "desktop.menu.toggleTerminal": "Prikaži ili sakrij terminal", + "desktop.menu.toggleFileTree": "Prikaži ili sakrij stablo datoteka", + "desktop.menu.reload": "Ponovo učitaj", + "desktop.menu.toggleDeveloperTools": "Prikaži ili sakrij alate za programere", + "desktop.menu.actualSize": "Stvarna veličina", + "desktop.menu.zoomIn": "Uvećaj", + "desktop.menu.zoomOut": "Umanji", + "desktop.menu.toggleFullScreen": "Prikaži ili sakrij prikaz preko cijelog ekrana", + "desktop.menu.back": "Nazad", + "desktop.menu.forward": "Naprijed", + "desktop.menu.previousSession": "Prethodna sesija", + "desktop.menu.nextSession": "Sljedeća sesija", + "desktop.menu.previousProject": "Prethodni projekat", + "desktop.menu.nextProject": "Sljedeći projekat", + "desktop.menu.minimize": "Minimiziraj", + "desktop.menu.maximize": "Maksimiziraj", + "desktop.menu.documentation": "Dokumentacija za OpenCode", + "desktop.menu.supportForum": "Forum za podršku", + "desktop.menu.shareFeedback": "Pošalji povratne informacije", + "desktop.menu.reportBug": "Prijavi grešku", + "desktop.menu.ariaLabel": "OpenCode meni", + + "desktop.updater.dialog.checkFailed.message": "Provjera ažuriranja nije uspjela.", + "desktop.updater.dialog.checkFailed.title": "Greška pri ažuriranju", + "desktop.updater.dialog.upToDate.message": "Sve je ažurno.", + "desktop.updater.dialog.upToDate.title": "Nema ažuriranja", + "desktop.updater.dialog.ready.message": + "Ažuriranje {{version}} je preuzeto. Želiš li sada ponovo pokrenuti aplikaciju?", + "desktop.updater.dialog.ready.title": "Ažuriranje je spremno", + "desktop.updater.dialog.restart": "Ponovo pokreni", + "desktop.updater.dialog.later": "Kasnije", + + "desktop.recovery.action.relaunch": "Ponovo pokreni", + "desktop.recovery.action.exportLogs": "Izvezi zapisnike", + "desktop.recovery.action.keepWaiting": "Nastavi čekati", + "desktop.recovery.action.quit": "Izađi", + "desktop.recovery.loadFailed": "OpenCode se nije uspio učitati", + "desktop.recovery.terminated": "Prozor OpenCode-a neočekivano je zatvoren", + "desktop.recovery.unresponsive": "OpenCode ne reaguje", + "desktop.recovery.unresponsive.detail": "Možeš ponovo pokrenuti aplikaciju, otvoriti zapisnike ili nastaviti čekati.", + "desktop.recovery.loadFailed.detail": "Prozor: {{window}}\nURL: {{url}}\nGreška: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Prozor: {{window}}\nRazlog: {{reason}}\nKod: {{code}}", + "desktop.recovery.unknown": "", + + "desktop.dialog.chooseFolder": "Odaberi fasciklu", + "desktop.dialog.chooseFile": "Odaberi datoteku", + "desktop.dialog.saveFile": "Sačuvaj datoteku", + "desktop.dialog.files": "Datoteke", + "desktop.server.local": "Lokalni server", + + "desktop.wsl.error.windowsOnly": "WSL je dostupan samo u Windowsu", + "desktop.wsl.error.unavailable": "WSL nije dostupan", + "desktop.wsl.error.listInstalled": "Nije uspjelo prikazivanje instaliranih WSL distribucija", + "desktop.wsl.error.listOnline": "Nije uspjelo prikazivanje dostupnih WSL distribucija", + "desktop.wsl.error.executeDistro": "Nije moguće izvršiti komande u distribuciji", + "desktop.wsl.error.installWsl": "Instalacija WSL-a nije uspjela", + "desktop.wsl.error.installDistro": "Instalacija distribucije nije uspjela: {{distro}}", + "desktop.wsl.error.installOpencode": "Instalacija OpenCode-a nije uspjela", + "desktop.wsl.error.alreadyAdded": "Distribucija {{distro}} je već dodana", + "desktop.wsl.error.opencodeMissing": "opencode nije instaliran u ovoj distribuciji", + "desktop.wsl.error.opencodeCannotRun": "opencode je instaliran, ali se ne može pokrenuti", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode nije instaliran u distribuciji {{distro}}", + "desktop.wsl.error.updateVersion": + "Ažuriranje OpenCode-a je završeno, ali {{distro}} i dalje prijavljuje {{installed}}; očekivano je {{expected}}", + "desktop.wsl.error.noVersion": "nema verzije", + "desktop.wsl.error.serverExited": "WSL server se zatvorio nakon pokretanja (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL server se zatvorio prije nego što je postao dostupan (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Isteklo je vrijeme provjere dostupnosti komponente Sidecar za {{distro}} nakon {{timeout}} ms", + "desktop.wsl.error.commandTimeout": "Isteklo je vrijeme za {{command}} {{args}} nakon {{timeout}} ms", + "desktop.wsl.error.failedPort": "Nije uspjelo dohvaćanje porta", + + "desktop.picker.error.notSelected": "Datoteka nije odabrana u izborniku", + "desktop.picker.error.sizeLimit": "Odabrani prilozi premašuju ograničenje od {{limit}} MB", + + "command.category.suggested": "Predloženo", + "command.category.view": "Prikaz", + "command.category.project": "Projekat", + "command.category.provider": "Provajder", + "command.category.server": "Server", + "command.category.session": "Sesija", + "command.category.theme": "Tema", + "command.category.language": "Jezik", + "command.category.file": "Datoteka", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Dozvole", + "command.category.workspace": "Radni prostor", + "command.category.settings": "Postavke", + "command.logs.export": "Izvezi zapisnike", + + "theme.scheme.system": "Sistem", + "theme.scheme.light": "Svijetlo", + "theme.scheme.dark": "Tamno", + + "command.sidebar.toggle": "Prikaži/sakrij bočnu traku", + "command.project.open": "Otvori projekat", + "command.project.previous": "Prethodni projekat", + "command.project.next": "Sljedeći projekat", + "command.project.index": "Prebaci na projekat {{index}}", + "command.provider.connect": "Poveži provajdera", + "command.server.switch": "Promijeni server", + "command.settings.open": "Otvori postavke", + "command.session.previous": "Prethodna sesija", + "command.session.next": "Sljedeća sesija", + "command.session.previous.unseen": "Prethodna nepročitana sesija", + "command.session.next.unseen": "Sljedeća nepročitana sesija", + "command.session.archive": "Arhiviraj sesiju", + + "command.palette": "Paleta komandi", + + "command.theme.cycle": "Promijeni temu", + "command.theme.set": "Koristi temu: {{theme}}", + "command.theme.scheme.cycle": "Promijeni šemu boja", + "command.theme.scheme.set": "Koristi šemu boja: {{scheme}}", + + "command.language.cycle": "Promijeni jezik", + "command.language.set": "Koristi jezik: {{language}}", + + "command.session.new": "Nova sesija", + "command.file.open": "Otvori datoteku", + "command.tab.close": "Zatvori karticu", + "command.tab.reopenClosed": "Ponovo otvori zatvorenu karticu", + "command.context.addSelection": "Dodaj odabir u kontekst", + "command.context.addSelection.description": "Dodaj odabrane linije iz trenutne datoteke", + "command.input.focus": "Fokusiraj polje za unos", + "command.terminal.toggle": "Prikaži/sakrij terminal", + "command.fileTree.toggle": "Prikaži/sakrij stablo datoteka", + "command.review.toggle": "Prikaži/sakrij pregled", + "command.terminal.new": "Novi terminal", + "command.terminal.new.description": "Kreiraj novu karticu terminala", + "command.steps.toggle": "Prikaži/sakrij korake", + "command.steps.toggle.description": "Prikaži ili sakrij korake za trenutnu poruku", + "command.message.previous": "Prethodna poruka", + "command.message.previous.description": "Idi na prethodnu korisničku poruku", + "command.message.next": "Sljedeća poruka", + "command.message.next.description": "Idi na sljedeću korisničku poruku", + "command.model.choose": "Odaberi model", + "command.model.choose.description": "Odaberi drugi model", + "command.mcp.toggle": "Prikaži/sakrij MCP-ove", + "command.mcp.toggle.description": "Prikaži/sakrij MCP-ove", + "command.agent.cycle": "Promijeni agenta", + "command.agent.cycle.description": "Prebaci na sljedećeg agenta", + "command.agent.cycle.reverse": "Promijeni agenta unazad", + "command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta", + "command.model.variant.cycle": "Promijeni nivo razmišljanja", + "command.model.variant.cycle.description": "Prebaci na sljedeći nivo", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Automatski prihvati dozvole", + "command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje dozvola", + "command.workspace.toggle": "Prikaži/sakrij radne prostore", + "command.workspace.toggle.description": "Omogući ili onemogući više radnih prostora u bočnoj traci", + "command.session.undo": "Poništi", + "command.session.undo.description": "Poništi posljednju poruku", + "command.session.redo": "Vrati", + "command.session.redo.description": "Vrati posljednju poništenu poruku", + "command.session.compact": "Sažmi sesiju", + "command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst", + "command.session.fork": "Fork iz poruke", + "command.session.fork.description": "Kreiraj novu sesiju iz prethodne poruke", + "command.session.share": "Podijeli sesiju", + "command.session.share.description": "Podijeli ovu sesiju i kopiraj URL u međuspremnik", + "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", + "palette.group.commands": "Komande", + "palette.group.files": "Datoteke", + + "dialog.provider.search.placeholder": "Pretraži provajdere", + "dialog.provider.empty": "Nema pronađenih provajdera", + "dialog.provider.group.popular": "Popularno", + "dialog.provider.group.other": "Ostalo", + "dialog.provider.custom.label": "Prilagođeni provajder kompatibilan s OpenAI-jem", + "dialog.provider.tag.recommended": "Preporučeno", + "dialog.provider.opencode.note": "Kurirani modeli uključujući Claude, GPT, Gemini i druge", + "dialog.provider.opencode.tagline": "Pouzdani optimizovani modeli", + "dialog.provider.opencodeGo.tagline": "Povoljna pretplata za sve", + "dialog.provider.anthropic.note": "Direktan pristup Claude modelima, uključujući Pro i Max", + "dialog.provider.copilot.note": "AI modeli za pomoć pri kodiranju putem GitHub Copilot", + "dialog.provider.openai.note": "GPT modeli za brze, sposobne opšte AI zadatke", + "dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore", + "dialog.provider.openrouter.note": "Pristup svim podržanim modelima preko jednog provajdera", + "dialog.provider.vercel.note": "Jedinstven pristup AI modelima uz pametno rutiranje", + + "dialog.model.select.title": "Odaberi model", + "dialog.model.search.placeholder": "Pretraži modele", + "dialog.model.empty": "Nema rezultata za modele", + "dialog.model.manage": "Upravljaj modelima", + "dialog.model.manage.description": "Prilagodi koji se modeli prikazuju u izborniku modela.", + "dialog.model.manage.provider.toggle": "Uključi/isključi sve {{provider}} modele", + + "dialog.model.unpaid.freeModels.title": "Besplatni modeli koje obezbjeđuje OpenCode", + "dialog.model.unpaid.addMore.title": "Dodaj još modela od popularnih provajdera", + "dialog.model.unpaid.viewMoreProviders": "Pogledaj još više od 70 provajdera", + + "dialog.provider.viewAll": "Prikaži više provajdera", + + "provider.connect.title": "Poveži {{provider}}", + "provider.connect.title.anthropicProMax": "Prijavi se putem Claude Pro/Max", + "provider.connect.selectMethod": "Odaberi način prijave za {{provider}}.", + "provider.connect.method.apiKey": "API ključ", + "provider.connect.method.browser": "Preglednik", + "provider.connect.method.headless": "Bez grafičkog interfejsa", + "provider.connect.status.inProgress": "Autorizacija je u toku...", + "provider.connect.status.waiting": "Čekanje na autorizaciju...", + "provider.connect.status.failed": "Autorizacija nije uspjela: {{error}}", + "provider.connect.apiKey.description": + "Unesi svoj {{provider}} API ključ da povežeš račun i koristiš {{provider}} modele u OpenCode-u.", + "provider.connect.apiKey.label": "{{provider}} API ključ", + "provider.connect.apiKey.placeholder": "API ključ", + "provider.connect.apiKey.required": "API ključ je obavezan", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ti daje pristup kuriranom skupu pouzdanih, optimizovanih modela za coding agente.", + "provider.connect.opencodeZen.line2": + "Sa jednim API ključem dobijaš pristup modelima kao što su Claude, GPT, Gemini, GLM i drugi.", + "provider.connect.opencodeZen.visit.prefix": "Posjeti ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " da preuzmeš svoj API ključ.", + "provider.connect.oauth.code.visit.prefix": "Posjeti ", + "provider.connect.oauth.code.visit.link": "ovaj link", + "provider.connect.oauth.code.visit.suffix": + " da preuzmeš autorizacijski kod i povežeš račun te koristiš {{provider}} modele u OpenCode-u.", + "provider.connect.oauth.code.label": "{{method}} autorizacijski kod", + "provider.connect.oauth.code.placeholder": "Autorizacijski kod", + "provider.connect.oauth.code.required": "Autorizacijski kod je obavezan", + "provider.connect.oauth.code.invalid": "Nevažeći autorizacijski kod", + "provider.connect.oauth.auto.visit.prefix": "Posjeti ", + "provider.connect.oauth.auto.visit.link": "ovaj link", + "provider.connect.oauth.auto.visit.suffix": + " i unesi kod ispod da povežeš račun i koristiš {{provider}} modele u OpenCode-u.", + "provider.connect.oauth.auto.confirmationCode": "Kod za potvrdu", + "provider.connect.toast.connected.title": "{{provider}} povezan", + "provider.connect.toast.connected.description": "{{provider}} modeli su sada dostupni za korištenje.", + + "provider.custom.title": "Prilagođeni provajder", + "provider.custom.unavailable": "Prilagođeni provajderi nisu dostupni na ovom serveru", + "provider.custom.description.prefix": "Konfiguriši OpenAI-kompatibilnog provajdera. Pogledaj ", + "provider.custom.description.link": "dokumentaciju za konfiguraciju provajdera", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID provajdera", + "provider.custom.field.providerID.placeholder": "mojprovajder", + "provider.custom.field.providerID.description": "Mala slova, brojevi, crtice ili donje crte", + "provider.custom.field.name.label": "Prikazano ime", + "provider.custom.field.name.placeholder": "Moj AI Provajder", + "provider.custom.field.baseURL.label": "Bazni URL", + "provider.custom.field.baseURL.placeholder": "https://api.mojprovajder.com/v1", + "provider.custom.field.apiKey.label": "API ključ", + "provider.custom.field.apiKey.placeholder": "API ključ", + "provider.custom.field.apiKey.description": + "Opcionalno. Ostavi prazno ako upravljaš autentifikacijom putem zaglavlja.", + "provider.custom.models.label": "Modeli", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Ime", + "provider.custom.models.name.placeholder": "Prikazano ime", + "provider.custom.models.remove": "Ukloni model", + "provider.custom.models.add": "Dodaj model", + "provider.custom.headers.label": "Zaglavlja (opcionalno)", + "provider.custom.headers.key.label": "Zaglavlje", + "provider.custom.headers.key.placeholder": "Ime-Zaglavlja", + "provider.custom.headers.value.label": "Vrijednost", + "provider.custom.headers.value.placeholder": "vrijednost", + "provider.custom.headers.remove": "Ukloni zaglavlje", + "provider.custom.headers.add": "Dodaj zaglavlje", + "provider.custom.error.providerID.required": "ID provajdera je obavezan", + "provider.custom.error.providerID.format": "Koristi mala slova, brojeve, crtice ili donje crte", + "provider.custom.error.providerID.exists": "Taj ID provajdera već postoji", + "provider.custom.error.name.required": "Prikazano ime je obavezno", + "provider.custom.error.baseURL.required": "Bazni URL je obavezan", + "provider.custom.error.baseURL.format": "Mora početi sa http:// ili https://", + "provider.custom.error.required": "Obavezno", + "provider.custom.error.duplicate": "Duplikat", + + "provider.disconnect.toast.disconnected.title": "{{provider}} odspojen", + "provider.disconnect.toast.disconnected.description": "{{provider}} modeli više nisu dostupni.", + + "model.tag.free": "Besplatno", + "model.tag.latest": "Najnovije", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekst", + "model.input.image": "slika", + "model.input.audio": "zvuk", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Podržava: {{inputs}}", + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Ulazi", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Provajder", + "model.tooltip.reasoning": "Rasuđivanje", + "model.tooltip.reasoning.allowed": "Podržava rasuđivanje", + "model.tooltip.reasoning.none": "Bez rasuđivanja", + "model.tooltip.context": "Limit konteksta {{limit}}", + + "common.search.placeholder": "Pretraži", + "common.goBack": "Nazad", + "common.goForward": "Naprijed", + "common.loading": "Učitavanje", + "common.loading.ellipsis": "...", + "common.cancel": "Otkaži", + "common.clear": "Očisti", + "common.connect": "Poveži", + "common.disconnect": "Prekini vezu", + "common.continue": "Nastavi", + "common.submit": "Pošalji", + "common.save": "Sačuvaj", + "common.saving": "Čuvanje...", + "common.default": "Podrazumijevano", + "common.attachment": "prilog", + + "prompt.placeholder.shell": "Unesi shell naredbu... {{example}}", + "prompt.placeholder.normal": 'Pitaj bilo šta... "{{example}}"', + "prompt.placeholder.simple": "Pitaj bilo šta...", + "prompt.placeholder.summarizeComments": "Sažmi komentare…", + "prompt.placeholder.summarizeComment": "Sažmi komentar…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "esc za izlaz", + "session.child.promptDisabled": "Sesijama podagenta nije moguće slati upite.", + "session.child.backToParent": "Nazad na glavnu sesiju.", + + "prompt.example.1": "Popravi TODO u bazi koda", + "prompt.example.2": "Koji je tehnološki stack ovog projekta?", + "prompt.example.3": "Popravi pokvarene testove", + "prompt.example.4": "Objasni kako radi autentifikacija", + "prompt.example.5": "Pronađi i popravi sigurnosne ranjivosti", + "prompt.example.6": "Dodaj jedinične testove za servis korisnika", + "prompt.example.7": "Refaktoriši ovu funkciju da bude čitljivija", + "prompt.example.8": "Šta znači ova greška?", + "prompt.example.9": "Pomozi mi da otklonim ovu grešku", + "prompt.example.10": "Generiši API dokumentaciju", + "prompt.example.11": "Optimizuj upite prema bazi podataka", + "prompt.example.12": "Dodaj validaciju ulaza", + "prompt.example.13": "Napravi novu komponentu za...", + "prompt.example.14": "Kako da postavim ovaj projekat?", + "prompt.example.15": "Pregledaj moj kod prema najboljim praksama", + "prompt.example.16": "Dodaj obradu grešaka u ovu funkciju", + "prompt.example.17": "Objasni ovaj regex obrazac", + "prompt.example.18": "Pretvori ovo u TypeScript", + "prompt.example.19": "Dodaj logovanje kroz cijelu bazu koda", + "prompt.example.20": "Koje su zavisnosti zastarjele?", + "prompt.example.21": "Pomozi mi da napišem migracijsku skriptu", + "prompt.example.22": "Implementiraj keširanje za ovu krajnju tačku", + "prompt.example.23": "Dodaj paginaciju u ovu listu", + "prompt.example.24": "Napravi CLI komandu za...", + "prompt.example.25": "Kako ovdje rade varijable okruženja?", + + "prompt.popover.emptyResults": "Nema rezultata", + "prompt.popover.emptyCommands": "Nema komandi", + "prompt.dropzone.label": "Ovdje prevuci slike, PDF-ove ili tekstualne datoteke", + "prompt.dropzone.file.label": "Spusti za @spominjanje datoteke", + "prompt.slash.badge.custom": "prilagođeno", + "prompt.slash.badge.skill": "skill", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktivno", + "prompt.context.includeActiveFile": "Uključi aktivnu datoteku", + "prompt.context.removeActiveFile": "Ukloni aktivnu datoteku iz konteksta", + "prompt.context.removeFile": "Ukloni datoteku iz konteksta", + "prompt.action.attachFile": "Dodaj datoteke", + "prompt.menu.addImagesAndFiles": "Dodaj datoteke i više", + "prompt.menu.imagesAndFiles": "Slike i datoteke", + "prompt.menu.commands": "Komande", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Shell naredba", + "prompt.attachment.remove": "Ukloni prilog", + "prompt.action.send": "Pošalji", + "prompt.action.stop": "Zaustavi", + + "prompt.toast.pasteUnsupported.title": "Nepodržan prilog", + "prompt.toast.attachmentDuplicate.title": "Ova datoteka je već učitana", + "prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.", + "prompt.toast.modelAgentRequired.title": "Odaberi agenta i model", + "prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.", + "prompt.toast.worktreeCreateFailed.title": "Neuspješno kreiranje worktree-a", + "prompt.toast.sessionCreateFailed.title": "Neuspješno kreiranje sesije", + "prompt.toast.shellSendFailed.title": "Neuspješno slanje shell naredbe", + "prompt.toast.commandSendFailed.title": "Neuspješno slanje komande", + "prompt.toast.promptSendFailed.title": "Neuspješno slanje upita", + "prompt.toast.promptSendFailed.description": "Nije moguće dohvatiti sesiju", + + "dialog.mcp.title": "MCP-ovi", + "dialog.mcp.description": "{{enabled}} od {{total}} omogućeno", + "dialog.mcp.empty": "Nema konfigurisanih MCP-ova", + + "dialog.lsp.empty": "LSP-ovi se automatski otkrivaju prema tipu datoteke", + "dialog.plugins.empty": "Plugini su konfigurisani u opencode.json", + + "mcp.status.connected": "povezano", + "mcp.status.failed": "neuspjelo", + "mcp.status.needs_auth": "potrebna autentifikacija", + "mcp.auth.clickToAuthenticate": "Klikni za autentifikaciju", + "mcp.status.disabled": "onemogućeno", + + "dialog.fork.empty": "Nema poruka za fork", + + "dialog.directory.search.placeholder": "Pretraži fascikle", + "dialog.directory.empty": "Nema pronađenih fascikli", + "dialog.directory.action.selectFile": "Odaberi datoteku", + "dialog.directory.action.selectFolder": "Odaberi fasciklu", + "dialog.directory.root": "Korijen", + "dialog.directory.parent": "Nadređena fascikla", + "dialog.directory.readError": "Nije moguće pročitati ovu fasciklu", + + "dialog.server.title": "Serveri", + "dialog.server.description": "Promijeni na koji se OpenCode server ova aplikacija povezuje.", + "dialog.server.search.placeholder": "Pretraži servere", + "dialog.server.empty": "Još nema servera", + "dialog.server.add.title": "Dodaj server", + "dialog.server.add.url": "URL servera", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Nije moguće povezati se na server", + "dialog.server.add.checking": "Provjera...", + "dialog.server.add.button": "Dodaj server", + "dialog.server.add.name": "Ime servera (opcionalno)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Korisničko ime (opcionalno)", + "dialog.server.add.password": "Lozinka (opcionalno)", + "dialog.server.edit.title": "Uredi server", + "dialog.server.default.title": "Podrazumijevani server", + "dialog.server.default.description": + "Poveži se na ovaj server pri pokretanju aplikacije umjesto pokretanja lokalnog servera. Potrebno je ponovo pokrenuti aplikaciju.", + "dialog.server.default.none": "Nije odabran server", + "dialog.server.default.set": "Postavi trenutni server kao podrazumijevani", + "dialog.server.default.clear": "Očisti", + "dialog.server.action.remove": "Ukloni server", + + "dialog.server.menu.edit": "Uredi", + "dialog.server.menu.default": "Postavi kao podrazumijevano", + "dialog.server.menu.defaultRemove": "Ukloni podrazumijevano", + "dialog.server.menu.delete": "Izbriši", + "dialog.server.current": "Trenutni server", + "dialog.server.status.default": "Podrazumijevano", + "wsl.server.add": "Dodaj WSL server", + "wsl.server.addShort": "Dodaj WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL server", + "wsl.server.retryStart": "Ponovi pokretanje", + "wsl.server.updating": "Ažuriranje...", + "wsl.onboarding.step.distro": "Odaberi distribuciju", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Provjera WSL-a...", + "wsl.onboarding.restartRequired": "Potrebno je ponovo pokrenuti Windows da bi se završila instalacija WSL-a.", + "wsl.onboarding.ready": "WSL je spreman.", + "wsl.onboarding.required": "WSL je potreban za nastavak.", + "wsl.onboarding.checkingDistros": "Provjera distribucija...", + "wsl.onboarding.installingDistro": "Instaliranje {{distro}}...", + "wsl.onboarding.checkingDistro": "Provjera {{distro}}...", + "wsl.onboarding.listingDistros": "Učitavanje liste distribucija...", + "wsl.onboarding.distroReady": "Distribucija {{distro}} je spremna.", + "wsl.onboarding.distroNotInstalled": "Distribucija {{distro}} još nije instalirana.", + "wsl.onboarding.openDistroOnce": "Otvori {{distro}} jednom da završiš postavljanje.", + "wsl.onboarding.finishingDistro": "Završavanje postavljanja za {{distro}}.", + "wsl.onboarding.pickDistro": "Odaberi distribuciju ili instaliraj jednu ispod.", + "wsl.onboarding.checkingOpencode": "Provjera OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Provjera OpenCode u distribuciji {{distro}}...", + "wsl.onboarding.updatingOpencode": "Ažuriranje OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Ažuriranje OpenCode u distribuciji {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Ažuriraj OpenCode u distribuciji {{distro}}.", + "wsl.onboarding.updateOpencode": "Ažuriraj OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode je spreman u distribuciji {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode je spreman.", + "wsl.onboarding.installOpencodeIn": "Instaliraj OpenCode u distribuciji {{distro}}.", + "wsl.onboarding.installOpencode": "Instaliraj OpenCode", + "wsl.onboarding.chooseDistroFirst": "Prvo odaberi distribuciju.", + "wsl.onboarding.loadFailed": "Nije uspjelo učitavanje WSL stanja.", + "wsl.onboarding.loading": "Učitavanje...", + "wsl.onboarding.installedDistros": "Instalirane distribucije", + "wsl.onboarding.checkAgain": "Provjeri ponovo", + "wsl.onboarding.distroStatus.ready": "Spremno", + "wsl.onboarding.distroStatus.checking": "Provjera...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode nije instaliran", + "wsl.onboarding.distroStatus.missingTools": "Nedostaju bash i curl", + "wsl.onboarding.distroStatus.unsupported": "Nije podržano · Koristi WSL 2", + "wsl.onboarding.needAnotherDistro": "Treba ti druga distribucija?", + "wsl.onboarding.needAnotherDistroHint": "Instaliraj Linux distribuciju iz WSL kataloga", + "wsl.onboarding.wslNotInstalled.title": "WSL nije instaliran", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) je potreban da bi OpenCode mogao dodati WSL server", + "wsl.onboarding.wslUnavailable.title": "WSL nije dostupan", + "wsl.onboarding.wslUnavailable.description": "OpenCode nije mogao provjeriti WSL na ovom uređaju.", + "wsl.onboarding.installWsl": "Instaliraj WSL", + "wsl.onboarding.windowsRestartRequired": + "Ponovo pokreni Windows da završiš instalaciju WSL-a, zatim ponovo otvori OpenCode.", + "wsl.onboarding.next": "Dalje", + "wsl.onboarding.refresh": "Osvježi", + "wsl.onboarding.allDistrosAdded": "Sve instalirane distribucije su već dodane.", + "wsl.onboarding.noDistros": "Još nema otkrivenih distribucija.", + "wsl.onboarding.install": "Instaliraj", + "wsl.onboarding.installing": "Instaliranje...", + "wsl.onboarding.installDistro": "Instaliraj distribuciju", + "wsl.onboarding.searchDistros": "Pretraži distribucije", + "wsl.onboarding.wsl2Required": "Potreban je WSL 2.", + "wsl.onboarding.toolsRequired": "Ovoj distribuciji su potrebni bash i curl.", + "wsl.onboarding.openTerminal": "Otvori terminal", + "wsl.onboarding.path": "Putanja: {{path}}", + "wsl.onboarding.notFound": "nije pronađeno", + "wsl.onboarding.version": "Verzija: {{version}}", + "wsl.onboarding.unknown": "nepoznato", + "wsl.onboarding.desktopVersion": "desktop {{version}}", + "wsl.onboarding.versionMismatch": "Instalirana verzija ne odgovara verziji desktop aplikacije.", + "wsl.onboarding.adding": "Dodavanje...", + + "help.tabs.toast.ariaLabel": "Predstavljamo kartice. Organizuj svoj rad i aktivne sesije pomoću kartica", + "help.tabs.toast.dismiss": "Odbaci informacije o karticama", + "help.tabs.title": "Predstavljamo kartice", + "help.tabs.description": "Organizuj svoj rad i aktivne sesije pomoću kartica", + "help.tabs.date": "14. juli", + "help.tabs.introduction": "OpenCode Desktop sada je organizovan oko kartica.", + "help.tabs.sessions": + "Započni novu sesiju u kartici ili otvori postojeću sesiju iz bilo kojeg projekta. Otvori novu karticu kada započinješ nešto novo, a zatvori je kada završiš.", + "help.tabs.organize": + "Nekoliko otvorenih kartica olakšava organizovanje aktivnih sesija. Preimenuj kartice tako da ih možeš lako prepoznati ako ih namjeravaš zadržati.", + "help.tabs.home": + "Sve svoje sesije i projekte pronaći ćeš na novom početnom ekranu. Odabirom sesije ona se otvara u kartici.", + "help.tabs.persistence": "Kada ponovo otvoriš aplikaciju, tvoje kartice ostaju otvorene.", + "help.tabs.worktrees": + "Novi dizajn još ne podržava Git Worktrees, ali podrška stiže uskoro. Ako želiš nastaviti koristiti prethodni raspored, možeš mijenjati rasporede u Postavkama. Imaj na umu da će novi raspored za nekoliko sedmica postati trajan.", + + "dialog.project.edit.title": "Uredi projekat", + "dialog.project.edit.name": "Naziv", + "dialog.project.edit.icon": "Ikonica", + "dialog.project.edit.icon.alt": "Ikonica projekta", + "dialog.project.edit.icon.hint": "Klikni ili prevuci sliku", + "dialog.project.edit.icon.recommended": "Preporučeno: 128x128px", + "dialog.project.edit.color": "Boja", + "dialog.project.edit.color.select": "Odaberi boju {{color}}", + "dialog.project.edit.worktree.startup": "Skripta za pokretanje radnog prostora", + "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.', + "context.breakdown.system": "Sistem", + "context.breakdown.user": "Korisnik", + "context.breakdown.assistant": "Asistent", + "context.breakdown.tool": "Pozivi alata", + "context.breakdown.other": "Ostalo", + + "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", + "context.stats.model": "Model", + "context.stats.limit": "Limit konteksta", + "context.stats.totalTokens": "Ukupno tokena", + "context.stats.usage": "Korištenje", + "context.stats.inputTokens": "Ulazni tokeni", + "context.stats.outputTokens": "Izlazni tokeni", + "context.stats.reasoningTokens": "Tokeni za rasuđivanje", + "context.stats.cacheTokens": "Keširani tokeni (čitanje/pisanje)", + "context.stats.userMessages": "Korisničke poruke", + "context.stats.assistantMessages": "Poruke asistenta", + "context.stats.totalCost": "Ukupni trošak", + "context.stats.sessionCreated": "Sesija kreirana", + "context.stats.lastActivity": "Posljednja aktivnost", + + "context.usage.tokens": "Tokeni", + "context.usage.usage": "Korištenje", + "context.usage.cost": "Trošak", + "context.usage.clickToView": "Klikni da vidiš kontekst", + "context.usage.view": "Prikaži korištenje konteksta", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "Jezik", + "toast.language.description": "Prebačeno na {{language}}", + + "toast.theme.title": "Tema promijenjena", + "toast.scheme.title": "Šema boja", + + "toast.workspace.enabled.title": "Radni prostori omogućeni", + "toast.workspace.enabled.description": "Više worktree-ova se sada prikazuje u bočnoj traci", + "toast.workspace.disabled.title": "Radni prostori onemogućeni", + "toast.workspace.disabled.description": "Samo glavni worktree se prikazuje u bočnoj traci", + + "toast.permissions.autoaccept.on.title": "Automatsko prihvatanje dozvola", + "toast.permissions.autoaccept.on.description": "Zahtjevi za dozvole će biti automatski odobreni", + "toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvatanje dozvola", + "toast.permissions.autoaccept.off.description": "Zahtjevi za dozvole će zahtijevati odobrenje", + + "toast.model.none.title": "Nije odabran model", + "toast.model.none.description": "Poveži provajdera da sažmeš ovu sesiju", + + "toast.file.loadFailed.title": "Neuspjelo učitavanje datoteke", + "toast.file.listFailed.title": "Neuspješno listanje datoteka", + + "toast.context.noLineSelection.title": "Nema odabranih linija", + "toast.context.noLineSelection.description": "Prvo odaberi raspon linija u kartici datoteke.", + + "toast.session.share.copyFailed.title": "Neuspjelo kopiranje URL-a u međuspremnik", + "toast.session.share.success.title": "Sesija podijeljena", + "toast.session.share.success.description": "URL za dijeljenje je kopiran u međuspremnik!", + "toast.session.share.failed.title": "Neuspjelo dijeljenje sesije", + "toast.session.share.failed.description": "Došlo je do greške prilikom dijeljenja sesije", + + "toast.session.unshare.success.title": "Dijeljenje sesije ukinuto", + "toast.session.unshare.success.description": "Dijeljenje sesije je uspješno ukinuto!", + "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", + "toast.update.description": "Nova verzija OpenCode-a ({{version}}) je dostupna za instalaciju.", + "toast.update.action.installRestart": "Instaliraj i ponovo pokreni", + "toast.update.action.notYet": "Ne još", + + "error.page.title": "Nešto je pošlo po zlu", + "error.page.description": "Došlo je do greške prilikom učitavanja aplikacije.", + "error.page.description.localServerStartup": "Došlo je do greške pri pokretanju lokalnog servera.", + "error.page.details.label": "Detalji greške", + "error.page.action.restart": "Ponovo pokreni", + "error.page.action.report": "Prijavi grešku", + "error.page.action.reported": "Greška prijavljena", + "error.page.action.exportLogs": "Izvezi logove", + "error.page.action.checking": "Provjera...", + "error.page.action.checkUpdates": "Provjeri ažuriranja", + "error.page.action.updateTo": "Ažuriraj na {{version}}", + "error.page.report.prefix": "Molimo prijavi ovu grešku OpenCode timu", + "error.page.report.discord": "na Discordu", + "error.page.version": "Verzija: {{version}}", + + "error.dev.rootNotFound": + "Korijenski element nije pronađen. Da li si zaboravio da ga dodaš u index.html? Ili je možda id atribut pogrešno napisan?", + + "error.serverSync.connectFailed": "Nije moguće povezati se na server. Da li server radi na `{{url}}`?", + "directory.error.invalidUrl": "Nevažeći direktorij u URL-u.", + + "error.chain.unknown": "Nepoznata greška", + "error.chain.causedBy": "Uzrok:", + "error.chain.apiError": "API greška", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Može se ponoviti: {{retryable}}", + "error.chain.responseBody": "Tijelo odgovora:\n{{body}}", + "error.chain.didYouMean": "Da li si mislio: {{suggestions}}", + "error.chain.modelNotFound": "Model nije pronađen: {{provider}}/{{model}}", + "error.chain.checkConfig": "Provjeri nazive provajdera/modela u konfiguraciji (opencode.json)", + "error.chain.mcpFailed": 'MCP server "{{name}}" nije uspio. Napomena: OpenCode još ne podržava MCP autentifikaciju.', + "error.chain.providerAuthFailed": "Autentifikacija provajdera nije uspjela ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Neuspjelo inicijalizovanje provajdera "{{provider}}". Provjeri kredencijale i konfiguraciju.', + "error.chain.configJsonInvalid": "Konfiguracijska datoteka na {{path}} nije važeći JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije važeći JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Direktorij "{{dir}}" u {{path}} nije ispravan. Preimenuj direktorij u "{{suggestion}}" ili ga ukloni. Ovo je česta greška u kucanju.', + "error.chain.configFrontmatterError": "Neuspjelo parsiranje frontmatter-a u {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfiguracijska datoteka na {{path}} nije ispravna", + "error.chain.configInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije ispravna: {{message}}", + + "notification.permission.title": "Potrebna dozvola", + "notification.permission.description": "{{sessionTitle}} u {{projectName}} traži dozvolu", + "notification.question.title": "Pitanje", + "notification.question.description": "{{sessionTitle}} u {{projectName}} ima pitanje", + "notification.action.goToSession": "Idi na sesiju", + + "notification.session.responseReady.title": "Odgovor je spreman", + "notification.session.error.title": "Greška sesije", + "notification.session.error.fallbackDescription": "Došlo je do greške", + + "home.recentProjects": "Nedavni projekti", + "home.empty.title": "Nema nedavnih projekata", + "home.empty.description": "Kreni tako što ćeš otvoriti lokalni projekat", + "home.title": "Početna", + "home.projects": "Projekti", + "home.project.add": "Dodaj projekat", + "home.recentlyClosed": "Nedavno zatvoreni projekti", + "home.server.collapse": "Sažmi projekte servera", + "home.server.expand": "Proširi projekte servera", + "home.sessions.search.placeholder": "Pretraži sesije", + "home.sessions.search.placeholder.scoped": "Pretraži sesije u {{scope}}", + "home.sessions.search.sessions": "Sesije", + "home.sessions.search.noResults": "Nema pronađenih sesija za {{query}}", + "home.sessions.empty": "Ovdje još nema ničega", + "home.sessions.empty.description": "Kreiraj sesiju za početak", + "home.sessions.group.today": "Danas", + "home.sessions.group.yesterday": "Jučer", + "home.sessions.group.older": "Starije", + "home.providerTip": + "Poveži se s više od 75 provajdera da koristiš druge modele, uključujući Claude, GPT, Gemini i druge", + + "session.tab.session": "Sesija", + "session.tab.review": "Pregled", + "session.tab.context": "Kontekst", + "session.tab.unknown": "Nepoznata sesija", + "session.panel.reviewAndFiles": "Pregled i datoteke", + "session.error.notFound": "Ova sesija nije pronađena", + "session.error.notFound.description": "Ova kartica upućuje na sesiju koja više ne postoji na ovom serveru.", + "session.error.notFound.closeTab": "Zatvori karticu", + "session.error.serverConnection": "Nije moguće povezati se s ovim serverom", + "session.review.filesChanged": "Izmijenjeno {{count}} datoteka", + "session.review.change.one": "Izmjena", + "session.review.change.other": "Izmjene", + "session.review.loadingChanges": "Učitavanje izmjena...", + "session.review.empty": "Još nema izmjena u ovoj sesiji", + "session.review.noVcs": "Nije otkriven Git sistem za kontrolu verzija, pa se promjene ne prikazuju", + "session.review.noSnapshot": + "Praćenje snimaka (snapshot) je onemogućeno u konfiguraciji, pa promjene sesije nisu dostupne", + "session.review.noChanges": "Nema izmjena", + "session.review.noUncommittedChanges": "Još nema promjena koje nisu commitovane", + "session.review.noBranchChanges": "Još nema promjena na grani", + + "session.files.selectToOpen": "Odaberi datoteku za otvaranje", + "session.files.all": "Sve datoteke", + "session.files.empty": "Nema datoteka", + "session.files.binaryContent": "Binarna datoteka (sadržaj se ne može prikazati)", + + "session.messages.renderEarlier": "Prikaži ranije poruke", + "session.messages.loadingEarlier": "Učitavanje ranijih poruka...", + "session.messages.loadEarlier": "Učitaj ranije poruke", + "session.messages.loading": "Učitavanje poruka...", + "session.messages.jumpToLatest": "Idi na najnovije", + + "session.context.addToContext": "Dodaj {{selection}} u kontekst", + "session.todo.title": "Zadaci", + "session.todo.collapse": "Sažmi", + "session.todo.expand": "Proširi", + "session.question.minimize": "Minimiziraj pitanje", + "session.question.restore": "Vrati pitanje", + "session.question.pending.one": "{{count}} pitanje na čekanju", + "session.question.pending.few": "{{count}} pitanja na čekanju", + "session.question.pending.other": "{{count}} pitanja na čekanju", + "session.followupDock.summary.one": "{{count}} poruka na čekanju", + "session.followupDock.summary.few": "{{count}} poruke na čekanju", + "session.followupDock.summary.other": "{{count}} poruka na čekanju", + "session.followupDock.sendNow": "Pošalji sada", + "session.followupDock.edit": "Uredi", + "session.followupDock.collapse": "Sažmi poruke na čekanju", + "session.followupDock.expand": "Proširi poruke na čekanju", + "session.revertDock.summary.one": "{{count}} vraćena poruka", + "session.revertDock.summary.few": "{{count}} vraćene poruke", + "session.revertDock.summary.other": "{{count}} vraćenih poruka", + "session.revertDock.collapse": "Sažmi vraćene poruke", + "session.revertDock.expand": "Proširi vraćene poruke", + "session.revertDock.restore": "Vrati poruku", + + "session.new.title": "Napravi bilo šta", + "session.new.project.new": "Novi projekat", + "session.new.project.search": "Pretraži projekte", + "session.new.project.add": "Dodaj projekat", + "session.new.worktree.main": "Glavna grana", + "session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})", + "session.new.worktree.create": "Kreiraj novi worktree", + "session.new.workspace.runIn": "Pokreni sesiju u", + "session.new.workspace.triggerLocal": "Lokalno", + "session.new.workspace.local": "Lokalni repozitorij", + "session.new.workspace.existing": "Radni prostor…", + "session.new.git.none": "Nema Gita", + "session.new.lastModified": "Posljednja izmjena", + + "session.header.search.placeholder": "Pretraži {{project}}", + "session.header.searchFiles": "Pretraži datoteke", + "session.header.openIn": "Otvori u", + "session.header.open.action": "Otvori {{app}}", + "session.header.open.ariaLabel": "Otvori u {{app}}", + "session.header.open.menu": "Opcije otvaranja", + "session.header.open.copyPath": "Kopiraj putanju", + + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Konfiguracije servera", + "status.popover.tab.servers": "Serveri", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugini", + "status.popover.action.manageServers": "Upravljaj serverima", + + "session.share.popover.title": "Objavi na webu", + "session.share.popover.description.shared": "Ova sesija je javna na webu. Dostupna je svima koji imaju link.", + "session.share.popover.description.unshared": "Podijeli sesiju javno na webu. Biće dostupna svima koji imaju link.", + "session.share.action.share": "Podijeli", + "session.share.action.publish": "Objavi", + "session.share.action.publishing": "Objavljivanje...", + "session.share.action.unpublish": "Poništi objavu", + "session.share.action.unpublishing": "Poništavanje objave...", + "session.share.action.view": "Prikaži", + "session.share.copy.copied": "Kopirano", + "session.share.copy.copyLink": "Kopiraj link", + + "lsp.tooltip.none": "Nema LSP servera", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Učitavanje upita...", + "terminal.loading": "Učitavanje terminala...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Zatvori terminal", + "terminal.connectionLost.title": "Veza prekinuta", + "terminal.connectionLost.description": + "Veza s terminalom je prekinuta. Ovo se može desiti kada se server ponovo pokrene.", + "terminal.connectTicket.csrfError": + "PTY tiket za povezivanje odbijen je zbog provjera porijekla ili CSRF-a. Provjeri CORS konfiguraciju servera.", + "terminal.connectTicket.statusError": "PTY tiket za povezivanje nije uspio sa statusom {{status}}", + + "titlebar.update": "Ažuriraj", + "titlebar.updateVersion": "Ažuriraj {{version}}", + + "common.closeTab": "Zatvori karticu", + "common.dismiss": "Odbaci", + "common.requestFailed": "Zahtjev nije uspio", + "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", + "common.close": "Zatvori", + "common.edit": "Uredi", + "common.loadMore": "Učitaj još", + "common.key.esc": "ESC", + + "sidebar.menu.toggle": "Prikaži/sakrij meni", + "sidebar.nav.projectsAndSessions": "Projekti i sesije", + "sidebar.settings": "Postavke", + "sidebar.help": "Pomoć", + "sidebar.workspaces.enable": "Omogući radne prostore", + "sidebar.workspaces.disable": "Onemogući radne prostore", + "sidebar.gettingStarted.title": "Početak", + "sidebar.gettingStarted.line1": "OpenCode uključuje besplatne modele, tako da možeš odmah početi.", + "sidebar.gettingStarted.line2": "Poveži bilo kojeg provajdera da koristiš modele, npr. Claude, GPT, Gemini itd.", + "sidebar.project.recentSessions": "Nedavne sesije", + "sidebar.project.viewAllSessions": "Prikaži sve sesije", + "sidebar.project.clearNotifications": "Očisti obavijesti", + "sidebar.empty.title": "Nema otvorenih projekata", + "sidebar.empty.description": "Otvori projekat za početak", + + "app.name.desktop": "OpenCode Desktop", + + "settings.section.desktop": "Desktop", + "settings.section.server": "Server", + "settings.tab.general": "Opšte", + "settings.tab.shortcuts": "Prečice", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integracija", + "settings.desktop.wsl.description": "Pokreni OpenCode server unutar WSL-a na Windowsu.", + + "settings.general.section.appearance": "Izgled", + "settings.general.section.advanced": "Napredno", + "settings.general.section.notifications": "Sistemske obavijesti", + "settings.general.section.updates": "Ažuriranja", + "settings.general.section.sounds": "Zvučni efekti", + "settings.general.section.feed": "Feed", + "settings.general.section.display": "Prikaz", + + "settings.general.row.language.title": "Jezik", + "settings.general.row.language.description": "Promijeni jezik prikaza u OpenCode-u", + "settings.general.row.shell.title": "Shell terminala", + "settings.general.row.shell.description": + "Odaberi shell koji se koristi za terminal. Kompatibilni shellovi koriste se i za pozive alata agenta.", + "settings.general.row.shell.autoDefault": "Automatski (podrazumijevano)", + "settings.general.row.shell.terminalOnly": "samo terminal", + "settings.general.row.appearance.title": "Izgled", + "settings.general.row.appearance.description": "Prilagodi kako OpenCode izgleda na tvom uređaju", + "settings.general.row.colorScheme.title": "Šema boja", + "settings.general.row.colorScheme.description": "Odaberi da li OpenCode prati sistemsku, svijetlu ili tamnu temu", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Prilagodi temu OpenCode-a.", + "settings.general.row.font.title": "Font za kod", + "settings.general.row.font.description": "Prilagodi font koji se koristi u blokovima koda", + "settings.general.row.terminalFont.title": "Font terminala", + "settings.general.row.terminalFont.description": "Prilagodi font koji se koristi u terminalu", + "settings.general.row.uiFont.title": "UI font", + "settings.general.row.uiFont.description": "Prilagodi font koji se koristi u cijelom interfejsu", + "settings.general.row.followup.title": "Ponašanje nadovezivanja", + "settings.general.row.followup.description": "Odaberi da li upiti nadovezivanja usmjeravaju odmah ili čekaju u redu", + "settings.general.row.followup.option.queue": "Red čekanja", + "settings.general.row.followup.option.steer": "Usmjeri", + "settings.general.row.showFileTree.title": "Stablo datoteka", + "settings.general.row.showFileTree.description": "Prikaži stablo datoteka u sesijama", + "settings.general.row.showNavigation.title": "Kontrole navigacije", + "settings.general.row.showNavigation.description": + "Prikaži dugmad za nazad i naprijed u naslovnoj traci desktop aplikacije", + "settings.general.row.showSearch.title": "Paleta komandi", + "settings.general.row.showSearch.description": "Prikaži dugme za pretragu i paletu komandi u naslovnoj traci", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Prikaži dugme terminala u naslovnoj traci desktop aplikacije", + "settings.general.row.showStatus.title": "Status servera", + "settings.general.row.showStatus.description": "Prikaži dugme statusa servera u naslovnoj traci", + "settings.general.row.mobileTitlebarBottom.title": "Donja navigacija", + "settings.general.row.mobileTitlebarBottom.description": + "Postavi naslovnu traku i kartice sesije na dno ekrana na mobilnim uređajima", + "settings.general.row.showCustomAgents.title": "Prilagođeni agenti", + "settings.general.row.showCustomAgents.description": "Prikaži izbor agenta u uređivaču poruke", + "settings.general.row.reasoningSummaries.title": "Prikaži sažetke rasuđivanja", + "settings.general.row.reasoningSummaries.description": "Prikaži sažetke rasuđivanja modela na vremenskoj traci", + + "settings.general.row.shellToolPartsExpanded.title": "Proširi dijelove shell alata", + "settings.general.row.shellToolPartsExpanded.description": + "Prikaži dijelove shell alata podrazumijevano proširene na vremenskoj traci", + "settings.general.row.editToolPartsExpanded.title": "Proširi dijelove alata za uređivanje", + "settings.general.row.editToolPartsExpanded.description": + "Prikaži dijelove alata za uređivanje, pisanje i patch podrazumijevano proširene na vremenskoj traci", + "settings.general.row.newInterface.title": "Novi raspored", + "settings.general.row.newInterface.badge": "Novo", + "settings.general.row.newInterface.description": + "Koristi nove kartice i raspored početne stranice. Ograničeno vrijeme možeš se prebacivati između rasporeda.", + "settings.general.row.newInterfaceNotice.title": "Sada koristiš novi raspored", + "settings.general.row.newInterfaceNotice.description": "Prethodni raspored više nije dostupan", + "settings.general.row.newInterfaceNotice.dismiss": "Odbaci", + "settings.general.row.pinchZoom.title": "Zumiranje štipanjem", + "settings.general.row.pinchZoom.description": + "Dozvoli zumiranje gestom štipanja na dodirnoj ploči i pomoću Ctrl-pomjeranja", + "settings.general.row.wayland.title": "Koristi nativni Wayland", + "settings.general.row.wayland.description": + "Onemogući rezervni X11 način rada na Waylandu. Zahtijeva ponovno pokretanje.", + "settings.general.row.wayland.tooltip": + "Na Linuxu sa monitorima miješanih stopa osvježavanja, nativni Wayland može biti stabilniji.", + + "settings.general.row.releaseNotes.title": "Bilješke o izdanju", + "settings.general.row.releaseNotes.description": 'Prikaži iskačuće prozore "Šta je novo" nakon ažuriranja', + + "settings.updates.row.startup.title": "Provjeri ažuriranja pri pokretanju", + "settings.updates.row.startup.description": "Automatski provjerava ažuriranja kada se OpenCode pokrene", + "settings.updates.row.check.title": "Provjeri ažuriranja", + "settings.updates.row.check.description": "Ručno provjeri ažuriranja i instaliraj ako su dostupna", + "settings.updates.action.checkNow": "Provjeri sada", + "settings.updates.action.checking": "Provjera...", + "settings.updates.action.downloading": "Preuzimanje...", + "settings.updates.action.installing": "Instaliranje...", + "settings.updates.toast.latest.title": "Sve je ažurno", + "settings.updates.toast.latest.description": "Koristiš najnoviju verziju OpenCode-a.", + "sound.option.none": "Nijedan", + "sound.option.alert01": "Upozorenje 01", + "sound.option.alert02": "Upozorenje 02", + "sound.option.alert03": "Upozorenje 03", + "sound.option.alert04": "Upozorenje 04", + "sound.option.alert05": "Upozorenje 05", + "sound.option.alert06": "Upozorenje 06", + "sound.option.alert07": "Upozorenje 07", + "sound.option.alert08": "Upozorenje 08", + "sound.option.alert09": "Upozorenje 09", + "sound.option.alert10": "Upozorenje 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Ne 01", + "sound.option.nope02": "Ne 02", + "sound.option.nope03": "Ne 03", + "sound.option.nope04": "Ne 04", + "sound.option.nope05": "Ne 05", + "sound.option.nope06": "Ne 06", + "sound.option.nope07": "Ne 07", + "sound.option.nope08": "Ne 08", + "sound.option.nope09": "Ne 09", + "sound.option.nope10": "Ne 10", + "sound.option.nope11": "Ne 11", + "sound.option.nope12": "Ne 12", + "sound.option.yup01": "Da 01", + "sound.option.yup02": "Da 02", + "sound.option.yup03": "Da 03", + "sound.option.yup04": "Da 04", + "sound.option.yup05": "Da 05", + "sound.option.yup06": "Da 06", + + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Prikaži sistemsku obavijest kada agent završi ili zahtijeva pažnju", + "settings.general.notifications.permissions.title": "Dozvole", + "settings.general.notifications.permissions.description": "Prikaži sistemsku obavijest kada je potrebna dozvola", + "settings.general.notifications.errors.title": "Greške", + "settings.general.notifications.errors.description": "Prikaži sistemsku obavijest kada dođe do greške", + + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Pusti zvuk kada agent završi ili zahtijeva pažnju", + "settings.general.sounds.permissions.title": "Dozvole", + "settings.general.sounds.permissions.description": "Pusti zvuk kada je potrebna dozvola", + "settings.general.sounds.errors.title": "Greške", + "settings.general.sounds.errors.description": "Pusti zvuk kada dođe do greške", + + "settings.shortcuts.title": "Prečice na tastaturi", + "settings.shortcuts.reset.button": "Vrati na podrazumijevano", + "settings.shortcuts.reset.toast.title": "Prečice resetovane", + "settings.shortcuts.reset.toast.description": "Prečice na tastaturi su vraćene na podrazumijevane.", + "settings.shortcuts.conflict.title": "Prečica je već u upotrebi", + "settings.shortcuts.conflict.description": "{{keybind}} je već dodijeljeno za {{titles}}.", + "settings.shortcuts.unassigned": "Nedodijeljeno", + "settings.shortcuts.pressKeys": "Pritisni tastere", + "settings.shortcuts.search.placeholder": "Pretraži prečice", + "settings.shortcuts.search.empty": "Nema pronađenih prečica", + + "settings.shortcuts.group.general": "Opšte", + "settings.shortcuts.group.session": "Sesija", + "settings.shortcuts.group.navigation": "Navigacija", + "settings.shortcuts.group.modelAndAgent": "Model i agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Upit", + + "settings.providers.title": "Provajderi", + "settings.providers.description": "Postavke provajdera će se ovdje moći podešavati.", + "settings.providers.section.connected": "Povezani provajderi", + "settings.providers.connected.empty": "Nema povezanih provajdera", + "settings.providers.section.popular": "Popularni provajderi", + "settings.providers.tag.environment": "Okruženje", + "settings.providers.tag.config": "Konfiguracija", + "settings.providers.tag.custom": "Prilagođeno", + "settings.providers.tag.other": "Ostalo", + "settings.models.title": "Modeli", + "settings.models.description": "Postavke modela će se ovdje moći podešavati.", + "settings.agents.title": "Agenti", + "settings.agents.description": "Postavke agenata će se ovdje moći podešavati.", + "settings.commands.title": "Komande", + "settings.commands.description": "Postavke komandi će se ovdje moći podešavati.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP postavke će se ovdje moći podešavati.", + + "settings.permissions.title": "Dozvole", + "settings.permissions.description": "Kontroliši koje alate server smije koristiti zadano.", + "settings.permissions.section.tools": "Alati", + "settings.permissions.toast.updateFailed.title": "Neuspjelo ažuriranje dozvola", + + "settings.permissions.action.allow": "Dozvoli", + "settings.permissions.action.ask": "Pitaj", + "settings.permissions.action.deny": "Zabrani", + + "settings.permissions.tool.read.title": "Čitanje", + "settings.permissions.tool.read.description": "Čitanje datoteke (podudara se s putanjom datoteke)", + "settings.permissions.tool.edit.title": "Uređivanje", + "settings.permissions.tool.edit.description": + "Mijenjanje datoteka, uključujući uređivanje, pisanje, zakrpe i višestruke izmjene", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Podudaranje datoteka pomoću glob šablona", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Pretraživanje sadržaja datoteka pomoću regularnih izraza", + "settings.permissions.tool.list.title": "Lista", + "settings.permissions.tool.list.description": "Listanje datoteka unutar direktorija", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Pokretanje shell komandi", + "settings.permissions.tool.task.title": "Zadatak", + "settings.permissions.tool.task.description": "Pokretanje pod-agenta", + "settings.permissions.tool.skill.title": "Vještina", + "settings.permissions.tool.skill.description": "Učitaj vještinu po nazivu", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Pokreni upite jezičnog servera", + "settings.permissions.tool.todowrite.title": "Ažuriranje liste zadataka", + "settings.permissions.tool.todowrite.description": "Ažuriraj listu zadataka", + "settings.permissions.tool.webfetch.title": "Web preuzimanje", + "settings.permissions.tool.webfetch.description": "Preuzmi sadržaj sa URL-a", + "settings.permissions.tool.websearch.title": "Web pretraga", + "settings.permissions.tool.websearch.description": "Pretražuj web", + "settings.permissions.tool.external_directory.title": "Vanjski direktorij", + "settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta", + "settings.permissions.tool.doom_loop.title": "Beskonačna petlja", + "settings.permissions.tool.doom_loop.description": "Otkriva ponovljene pozive alata sa identičnim unosom", + + "session.delete.failed.title": "Neuspjelo brisanje sesije", + "session.delete.title": "Izbriši sesiju", + "session.delete.confirm": 'Izbriši sesiju "{{name}}"?', + "session.delete.button": "Izbriši sesiju", + + "workspace.new": "Novi radni prostor", + "workspace.type.local": "lokalno", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "Neuspješno kreiranje radnog prostora", + "workspace.delete.failed.title": "Neuspješno brisanje radnog prostora", + "workspace.resetting.title": "Vraćanje radnog prostora na početno stanje", + "workspace.resetting.description": "Ovo može potrajati minut.", + "workspace.reset.failed.title": "Neuspješno vraćanje radnog prostora na početno stanje", + "workspace.reset.success.title": "Radni prostor vraćen na početno stanje", + "workspace.reset.success.description": "Radni prostor sada odgovara podrazumijevanoj grani.", + "workspace.error.stillPreparing": "Radni prostor se još priprema", + "workspace.status.checking": "Provjera promjena koje nisu commitovane...", + "workspace.status.error": "Nije moguće provjeriti git status.", + "workspace.status.clean": "Nisu pronađene promjene koje nisu commitovane.", + "workspace.status.dirty": "Pronađene su promjene koje nisu commitovane u ovom radnom prostoru.", + "workspace.delete.title": "Izbriši radni prostor", + "workspace.delete.confirm": 'Izbriši radni prostor "{{name}}"?', + "workspace.delete.button": "Izbriši radni prostor", + "workspace.reset.title": "Vrati radni prostor na početno stanje", + "workspace.reset.confirm": 'Vrati radni prostor "{{name}}" na početno stanje?', + "workspace.reset.button": "Vrati radni prostor na početno stanje", + "workspace.reset.archived.none": "Nijedna aktivna sesija neće biti arhivirana.", + "workspace.reset.archived.one": "1 sesija će biti arhivirana.", + "workspace.reset.archived.many": "Biće arhivirano {{count}} sesija.", + "workspace.reset.note": "Ovo će vratiti radni prostor na stanje podrazumijevane grane.", + "common.open": "Otvori", + "dialog.releaseNotes.action.getStarted": "Započni", + "dialog.releaseNotes.action.next": "Sljedeće", + "dialog.releaseNotes.action.hideFuture": "Ne prikazuj ovo u budućnosti", + "dialog.releaseNotes.media.alt": "Pregled izdanja", + "toast.project.reloadFailed.title": "Nije uspjelo ponovno učitavanje {{project}}", + "error.server.invalidConfiguration": "Nevažeća konfiguracija", + "common.moreCountSuffix": " (+{{count}} više)", + "common.time.justNow": "Upravo sada", + "common.time.minutesAgo.short": "prije {{count}} min", + "common.time.hoursAgo.short": "prije {{count}} h", + "common.time.daysAgo.short": "prije {{count}} d", + "settings.providers.connected.environmentDescription": "Povezano s tvojim varijablama okruženja", + "settings.providers.custom.description": "Dodaj provajdera kompatibilnog s OpenAI putem baznog URL-a.", + + "app.server.unreachable": "Nije moguće pristupiti {{server}}", + "app.server.retrying": "Automatski ponovni pokušaj...", + "app.server.otherServers": "Drugi serveri", + "dialog.server.add.usernamePlaceholder": "korisničko ime", + "dialog.server.add.passwordPlaceholder": "lozinka", + "server.row.noUsername": "nema korisničkog imena", + "session.review.noVcs.createGit.title": "Kreiraj Git repozitorij", + "session.review.noVcs.createGit.description": "Prati, pregledaj i poništi promjene u ovom projektu", + "session.review.noVcs.createGit.actionLoading": "Kreiranje Git repozitorija...", + "session.review.noVcs.createGit.action": "Kreiraj Git repozitorij", + "session.todo.progress": "{{done}} od {{total}} zadataka završeno", + "session.question.progress": "{{current}} od {{total}} pitanja", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.reveal.finder": "Prikaži u Finderu", + "session.header.reveal.fileExplorer": "Prikaži u File Exploreru", + "session.header.reveal.containingFolder": "Otvori fasciklu koja sadrži ovu stavku", + "session.header.open.fileManager": "File Manager", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Dijagnostika performansi razvoja", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Posljednji završeni prelazak rute koji dotiče stranicu sesije, mjeren od početka rutera do prvog iscrtavanja nakon smirivanja.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Kadrovi u sekundi tokom posljednjih 5 sekundi.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Najgore vrijeme kadra u posljednjih 5 sekundi.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Kadrovi duži od 32ms u posljednjih 5 sekundi.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blokirano vrijeme i broj dugih zadataka u posljednjih 5 sekundi. Maks zadatak: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Najgore zabilježeno kašnjenje unosa u posljednjih 5 sekundi.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Približno trajanje interakcije tokom posljednjih 5 sekundi. Ovo je slično INP-u, nije službeni Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativni pomak rasporeda za trenutni životni vijek aplikacije.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Korišteni JS heap naspram limita heapa. Samo Chromium.", + "debugBar.mem.tip": "Korišteni JS heap naspram limita heapa. {{used}} od {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Prisilno primijeni stilove fokusa na sve interaktivne elemente", + "debugBar.focus.on": "UKLJUČENO", + "debugBar.focus.off": "ISKLJUČENO", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Prisilno postavi smjer rasporeda cijele aplikacije bez promjene odabranog jezika", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "nepoznato", + "error.page.circular": "[Kružno]", + "error.serverSDK.noServerAvailable": "Nema dostupnog servera", + "error.serverSDK.serverNotAvailable": "Server nije dostupan", + "error.childStore.persistedCacheCreateFailed": "Nije uspjelo kreiranje trajnog keša", + "error.childStore.persistedProjectMetadataCreateFailed": "Nije uspjelo kreiranje trajnih metapodataka projekta", + "error.childStore.persistedProjectIconCreateFailed": "Nije uspjelo kreiranje trajne ikone projekta", + "error.childStore.storeCreateFailed": "Nije uspjelo kreiranje skladišta", + "terminal.connectionLost.abnormalClose": "WebSocket zatvoren nenormalno: {{code}}", +} diff --git a/packages/app/src/i18n/cs.ts b/packages/app/src/i18n/cs.ts new file mode 100644 index 0000000000000000000000000000000000000000..d79d4eb5a7f8bea409c86c264f204f5e49bf32d4 --- /dev/null +++ b/packages/app/src/i18n/cs.ts @@ -0,0 +1,1163 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Soubor", + "desktop.menu.edit": "Upravit", + "desktop.menu.view": "Zobrazit", + "desktop.menu.go": "Jdi", + "desktop.menu.window": "okno", + "desktop.menu.help": "Pomoc", + "desktop.menu.checkForUpdates": "Zkontrolovat aktualizace...", + "desktop.menu.settings": "Nastavení", + "desktop.menu.reloadWebview": "Znovu načíst Webview", + "desktop.menu.restart": "Restartujte", + "desktop.menu.exportLogs": "Exportovat protokoly...", + "desktop.menu.newSession": "Nová relace", + "desktop.menu.openProject": "Otevřít projekt...", + "desktop.menu.newWindow": "Nové okno", + "desktop.menu.closeWindow": "Zavřít okno", + "desktop.menu.undo": "Vrátit zpět", + "desktop.menu.redo": "Znovu", + "desktop.menu.cut": "Řez", + "desktop.menu.copy": "Kopírovat", + "desktop.menu.paste": "Vložit", + "desktop.menu.delete": "Odstranit", + "desktop.menu.selectAll": "Vyberte Vše", + "desktop.menu.toggleSidebar": "Přepnout postranní panel", + "desktop.menu.toggleTerminal": "Přepnout terminál", + "desktop.menu.toggleFileTree": "Přepnout strom souborů", + "desktop.menu.reload": "Znovu načíst", + "desktop.menu.toggleDeveloperTools": "Přepnout Vývojářské nástroje", + "desktop.menu.actualSize": "Skutečná velikost", + "desktop.menu.zoomIn": "Přiblížit", + "desktop.menu.zoomOut": "Oddálit", + "desktop.menu.toggleFullScreen": "Přepnout na celou obrazovku", + "desktop.menu.back": "Zpět", + "desktop.menu.forward": "Vpřed", + "desktop.menu.previousSession": "Předchozí relace", + "desktop.menu.nextSession": "Další relace", + "desktop.menu.previousProject": "Předchozí projekt", + "desktop.menu.nextProject": "Další projekt", + "desktop.menu.minimize": "Minimalizovat", + "desktop.menu.maximize": "Maximalizovat", + "desktop.menu.documentation": "OpenCode Dokumentace", + "desktop.menu.supportForum": "Fórum podpory", + "desktop.menu.shareFeedback": "Sdílejte zpětnou vazbu", + "desktop.menu.reportBug": "Nahlásit chybu", + "desktop.menu.ariaLabel": "Nabídka OpenCode", + "desktop.updater.dialog.checkFailed.message": "Kontrola aktualizace se nezdařila.", + "desktop.updater.dialog.checkFailed.title": "Chyba aktualizace", + "desktop.updater.dialog.upToDate.message": "Jste aktuální.", + "desktop.updater.dialog.upToDate.title": "Žádné aktualizace", + "desktop.updater.dialog.ready.message": "Aktualizace {{version}} stažena. Restartovat nyní?", + "desktop.updater.dialog.ready.title": "Aktualizace připravena", + "desktop.updater.dialog.restart": "Restartujte", + "desktop.updater.dialog.later": "Později", + "desktop.recovery.action.relaunch": "Znovu spustit", + "desktop.recovery.action.exportLogs": "Exportovat protokoly", + "desktop.recovery.action.keepWaiting": "Čekej dál", + "desktop.recovery.action.quit": "Konec", + "desktop.recovery.loadFailed": "OpenCode se nepodařilo načíst", + "desktop.recovery.terminated": "OpenCode okno bylo neočekávaně ukončeno", + "desktop.recovery.unresponsive": "OpenCode nereaguje", + "desktop.recovery.unresponsive.detail": "Můžete znovu spustit aplikaci, otevřít protokoly nebo čekat.", + "desktop.recovery.loadFailed.detail": "Okno: {{window}}\nURL: {{url}}\nChyba: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Okno: {{window}}\nDůvod: {{reason}}\nKód: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Vyberte složku", + "desktop.dialog.chooseFile": "Vyberte soubor", + "desktop.dialog.saveFile": "Uložit soubor", + "desktop.dialog.files": "Soubory", + "desktop.server.local": "Místní server", + "desktop.wsl.error.windowsOnly": "WSL je k dispozici pouze na Windows", + "desktop.wsl.error.unavailable": "WSL není k dispozici", + "desktop.wsl.error.listInstalled": "Seznam nainstalovaných WSL distribucí se nezdařil", + "desktop.wsl.error.listOnline": "Seznam online WSL distribucí se nezdařil", + "desktop.wsl.error.executeDistro": "Nelze provádět příkazy v distro", + "desktop.wsl.error.installWsl": "WSL instalace se nezdařila", + "desktop.wsl.error.installDistro": "Nepodařilo se nainstalovat distribuci: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode instalace se nezdařila", + "desktop.wsl.error.alreadyAdded": "{{distro}} je již přidáno", + "desktop.wsl.error.opencodeMissing": "opencode není v této distribuci nainstalováno", + "desktop.wsl.error.opencodeCannotRun": "opencode je nainstalován, ale nelze jej spustit", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode není nainstalován v {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode aktualizace dokončena, ale {{distro}} stále hlásí {{installed}}; očekává se {{expected}}", + "desktop.wsl.error.noVersion": "žádná verze", + "desktop.wsl.error.serverExited": "Server WSL byl po spuštění ukončen (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Server WSL byl ukončen, než byl připraven (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Postranní vozík pro {{distro}} kontrolu stavu vypršel po {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} vypršel po {{timeout}}ms", + "desktop.wsl.error.failedPort": "Nepodařilo se získat port", + "desktop.picker.error.notSelected": "Soubor nebyl vybrán nástrojem pro výběr", + "desktop.picker.error.sizeLimit": "Vybrané přílohy překračují limit {{limit}} MB", + "command.category.suggested": "Doporučeno", + "command.category.view": "Zobrazit", + "command.category.project": "Projekt", + "command.category.provider": "Poskytovatel", + "command.category.server": "Server", + "command.category.session": "Relace", + "command.category.theme": "téma", + "command.category.language": "Jazyk", + "command.category.file": "Soubor", + "command.category.context": "Kontext", + "command.category.terminal": "Terminál", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Oprávnění", + "command.category.workspace": "Pracovní prostor", + "command.category.settings": "Nastavení", + "command.logs.export": "Exportovat protokoly", + "theme.scheme.system": "Systém", + "theme.scheme.light": "Světlo", + "theme.scheme.dark": "Tmavý", + "command.sidebar.toggle": "Přepnout postranní panel", + "command.project.open": "Otevřete projekt", + "command.project.previous": "Předchozí projekt", + "command.project.next": "Další projekt", + "command.project.index": "Přepnout na projekt {{index}}", + "command.provider.connect": "Připojte poskytovatele", + "command.server.switch": "Přepnout server", + "command.settings.open": "Otevřete nastavení", + "command.session.previous": "Předchozí relace", + "command.session.next": "Další sezení", + "command.session.previous.unseen": "Předchozí nepřečtená relace", + "command.session.next.unseen": "Další nepřečtená relace", + "command.session.archive": "Archivní relace", + "command.palette": "Paleta příkazů", + "command.theme.cycle": "Téma cyklu", + "command.theme.set": "Použít motiv: {{theme}}", + "command.theme.scheme.cycle": "Barevné schéma cyklu", + "command.theme.scheme.set": "Použít barevné schéma: {{scheme}}", + "command.language.cycle": "Jazyk cyklu", + "command.language.set": "Použít jazyk: {{language}}", + "command.session.new": "Nová relace", + "command.file.open": "Otevřít soubor", + "command.tab.close": "Zavřít kartu", + "command.tab.reopenClosed": "Znovu otevřete zavřenou kartu", + "command.context.addSelection": "Přidejte výběr do kontextu", + "command.context.addSelection.description": "Přidat vybrané řádky z aktuálního souboru", + "command.input.focus": "Zaměření vstupu", + "command.terminal.toggle": "Přepínací terminál", + "command.fileTree.toggle": "Přepnout strom souborů", + "command.review.toggle": "Přepnout recenzi", + "command.terminal.new": "Nový terminál", + "command.terminal.new.description": "Vytvořte novou kartu terminálu", + "command.steps.toggle": "Přepínání kroků", + "command.steps.toggle.description": "Zobrazit nebo skrýt kroky pro aktuální zprávu", + "command.message.previous": "Předchozí zpráva", + "command.message.previous.description": "Přejděte na předchozí zprávu uživatele", + "command.message.next": "Další zpráva", + "command.message.next.description": "Přejděte na další uživatelskou zprávu", + "command.model.choose": "Vyberte model", + "command.model.choose.description": "Vyberte jiný model", + "command.mcp.toggle": "Přepnout MCP", + "command.mcp.toggle.description": "Přepnout MCP", + "command.agent.cycle": "Agent cyklu", + "command.agent.cycle.description": "Přepněte na dalšího agenta", + "command.agent.cycle.reverse": "Cyklujte agenta zpět", + "command.agent.cycle.reverse.description": "Přepnout na předchozího agenta", + "command.model.variant.cycle": "Cyklické myšlení", + "command.model.variant.cycle.description": "Přepněte na další úroveň úsilí", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Automaticky přijímat oprávnění", + "command.permissions.autoaccept.disable": "Zastavit automatické přijímání oprávnění", + "command.workspace.toggle": "Přepnout pracovní prostory", + "command.workspace.toggle.description": "Povolit nebo zakázat více pracovních ploch na postranním panelu", + "command.session.undo": "Vrátit zpět", + "command.session.undo.description": "Vrátit zpět poslední zprávu", + "command.session.redo": "Znovu", + "command.session.redo.description": "Opakujte poslední vrácenou zprávu", + "command.session.compact": "Kompaktní relace", + "command.session.compact.description": "Shrňte relaci, abyste snížili velikost kontextu", + "command.session.fork": "Vytvořit větev ze zprávy", + "command.session.fork.description": "Vytvořte novou relaci z předchozí zprávy", + "command.session.share": "Sdílejte relaci", + "command.session.share.description": "Sdílejte tuto relaci a zkopírujte URL do schránky", + "command.session.unshare": "Zrušit sdílení relace", + "command.session.unshare.description": "Přestat sdílet tuto relaci", + "command.session.export": "Export relace", + "command.session.export.description": "Exportovat celý přepis relace jako JSON", + "palette.search.placeholder": "Prohledávejte soubory, příkazy a relace", + "palette.search.placeholder.home": "Hledat příkazy a relace", + "palette.empty": "Nebyly nalezeny žádné výsledky", + "palette.group.commands": "Příkazy", + "palette.group.files": "Soubory", + "dialog.provider.search.placeholder": "Hledat poskytovatele", + "dialog.provider.empty": "Nebyli nalezeni žádní poskytovatelé", + "dialog.provider.group.popular": "Populární", + "dialog.provider.group.other": "Jiné", + "dialog.provider.custom.label": "Vlastní poskytovatel kompatibilní s OpenAI", + "dialog.provider.tag.recommended": "Doporučeno", + "dialog.provider.opencode.note": "Vybrané modely včetně Claude, GPT, Gemini a dalších", + "dialog.provider.opencode.tagline": "Spolehlivé optimalizované modely", + "dialog.provider.opencodeGo.tagline": "Nízkonákladové předplatné pro každého", + "dialog.provider.anthropic.note": "Přímý přístup k modelům Claude, včetně modelů Pro a Max", + "dialog.provider.copilot.note": "Modely umělé inteligence pro pomoc s kódováním prostřednictvím GitHub Copilot", + "dialog.provider.openai.note": "GPT modely pro rychlé a schopné obecné úlohy umělé inteligence", + "dialog.provider.google.note": "Gemini modely pro rychlé, strukturované odpovědi", + "dialog.provider.openrouter.note": "Získejte přístup ke všem podporovaným modelům od jednoho poskytovatele", + "dialog.provider.vercel.note": "Jednotný přístup k modelům AI s chytrým směrováním", + "dialog.model.select.title": "Vyberte model", + "dialog.model.search.placeholder": "Hledat modely", + "dialog.model.empty": "Žádné výsledky modelu", + "dialog.model.manage": "Správa modelů", + "dialog.model.manage.description": "Upravte, které modely se zobrazí ve voliči modelů.", + "dialog.model.manage.provider.toggle": "Přepnout všechny modely {{provider}}", + "dialog.model.unpaid.freeModels.title": "Bezplatné modely poskytované OpenCode", + "dialog.model.unpaid.addMore.title": "Přidejte další modely od oblíbených poskytovatelů", + "dialog.model.unpaid.viewMoreProviders": "Zobrazit více než 70 dalších poskytovatelů", + "dialog.provider.viewAll": "Zobrazit další poskytovatele", + "provider.connect.title": "Připojit {{provider}}", + "provider.connect.title.anthropicProMax": "Přihlaste se pomocí Claude Pro/Max", + "provider.connect.selectMethod": "Vyberte způsob přihlášení pro {{provider}}.", + "provider.connect.method.apiKey": "API klíč", + "provider.connect.method.browser": "Prohlížeč", + "provider.connect.method.headless": "Bezhlavě", + "provider.connect.status.inProgress": "Probíhá autorizace...", + "provider.connect.status.waiting": "Čekání na autorizaci...", + "provider.connect.status.failed": "Autorizace se nezdařila: {{error}}", + "provider.connect.apiKey.description": + "Zadejte klíč API služby {{provider}}, připojte účet a používejte modely {{provider}} v OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API klíč", + "provider.connect.apiKey.placeholder": "API klíč", + "provider.connect.apiKey.required": "Je vyžadováno tlačítko API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen vám poskytuje přístup k upravené sadě spolehlivých optimalizovaných modelů pro kódovací agenty.", + "provider.connect.opencodeZen.line2": + "Pomocí jediného tlačítka API získáte přístup k modelům jako Claude, GPT, Gemini, GLM a dalším.", + "provider.connect.opencodeZen.visit.prefix": "Návštěva ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " vyzvedněte svůj klíč API.", + "provider.connect.oauth.code.visit.prefix": "Návštěva ", + "provider.connect.oauth.code.visit.link": "tento odkaz", + "provider.connect.oauth.code.visit.suffix": + " k vyzvednutí autorizačního kódu pro připojení vašeho účtu a používání {{provider}} modelů v OpenCode.", + "provider.connect.oauth.code.label": "{{method}} autorizační kód", + "provider.connect.oauth.code.placeholder": "Autorizační kód", + "provider.connect.oauth.code.required": "Je vyžadován autorizační kód", + "provider.connect.oauth.code.invalid": "Neplatný autorizační kód", + "provider.connect.oauth.auto.visit.prefix": "Návštěva ", + "provider.connect.oauth.auto.visit.link": "tento odkaz", + "provider.connect.oauth.auto.visit.suffix": + " a zadejte níže uvedený kód pro připojení svého účtu a používání {{provider}} modelů v OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Potvrzovací kód", + "provider.connect.toast.connected.title": "{{provider}} připojeno", + "provider.connect.toast.connected.description": "Nyní jsou k dispozici modely {{provider}}.", + "provider.custom.title": "Vlastní poskytovatel", + "provider.custom.unavailable": "Vlastní poskytovatelé nejsou na tomto serveru k dispozici", + "provider.custom.description.prefix": "Nakonfigurujte poskytovatele kompatibilního s OpenAI. Viz ", + "provider.custom.description.link": "konfigurační dokumenty poskytovatele", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Poskytovatel ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Malá písmena, čísla, pomlčky nebo podtržítka", + "provider.custom.field.name.label": "Zobrazovaný název", + "provider.custom.field.name.placeholder": "Můj poskytovatel AI", + "provider.custom.field.baseURL.label": "základ URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API klíč", + "provider.custom.field.apiKey.placeholder": "API klíč", + "provider.custom.field.apiKey.description": + "Volitelné. Pokud autorizaci spravujete pomocí záhlaví, ponechte prázdné.", + "provider.custom.models.label": "Modely", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Jméno", + "provider.custom.models.name.placeholder": "Zobrazovaný název", + "provider.custom.models.remove": "Odebrat model", + "provider.custom.models.add": "Přidat model", + "provider.custom.headers.label": "Záhlaví (volitelné)", + "provider.custom.headers.key.label": "Záhlaví", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Hodnota", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "Odstraňte záhlaví", + "provider.custom.headers.add": "Přidat záhlaví", + "provider.custom.error.providerID.required": "Poskytovatel ID je povinný", + "provider.custom.error.providerID.format": "Používejte malá písmena, čísla, pomlčky nebo podtržítka", + "provider.custom.error.providerID.exists": "Tento poskytovatel ID již existuje", + "provider.custom.error.name.required": "Zobrazovaný název je povinný", + "provider.custom.error.baseURL.required": "Je vyžadována základna URL", + "provider.custom.error.baseURL.format": "Musí začínat http:// nebo https://", + "provider.custom.error.required": "Povinné", + "provider.custom.error.duplicate": "Duplicitní", + "provider.disconnect.toast.disconnected.title": "{{provider}} odpojeno", + "provider.disconnect.toast.disconnected.description": "{{provider}} modely již nejsou k dispozici.", + "model.tag.free": "Zdarma", + "model.tag.latest": "Nejnovější", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "text", + "model.input.image": "obrázek", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Umožňuje: {{inputs}}", + "model.tooltip.context.label": "Kontext", + "model.tooltip.inputs": "Vstupy", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Poskytovatel", + "model.tooltip.reasoning": "Zdůvodnění", + "model.tooltip.reasoning.allowed": "Umožňuje uvažování", + "model.tooltip.reasoning.none": "Žádné zdůvodnění", + "model.tooltip.context": "Kontextový limit {{limit}}", + "common.search.placeholder": "Hledat", + "common.goBack": "Přejít zpět", + "common.goForward": "Přejděte vpřed", + "common.loading": "Načítání", + "common.loading.ellipsis": "...", + "common.cancel": "Zrušit", + "common.clear": "Jasný", + "common.open": "Otevřít", + "common.connect": "Připojte se", + "common.disconnect": "Odpojit", + "common.continue": "Pokračujte", + "common.submit": "Odeslat", + "common.save": "Uložit", + "common.saving": "Ukládání...", + "common.default": "Výchozí", + "common.attachment": "příloha", + "prompt.placeholder.shell": "Zadejte příkaz shellu... {{example}}", + "prompt.placeholder.normal": 'Zeptejte se na cokoliv... "{{example}}"', + "prompt.placeholder.simple": "Zeptejte se na cokoliv...", + "prompt.placeholder.summarizeComments": "Shrnout komentáře…", + "prompt.placeholder.summarizeComment": "Shrnout komentář…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "esc pro ukončení", + "session.child.promptDisabled": "Nelze vyžádat relace podagenta.", + "session.child.backToParent": "Zpět k hlavní relaci.", + "prompt.example.1": "Opravte TODO v kódové základně", + "prompt.example.2": "Jaký je technický stack tohoto projektu?", + "prompt.example.3": "Opravte nefunkční testy", + "prompt.example.4": "Vysvětlete, jak funguje ověřování", + "prompt.example.5": "Najděte a opravte slabá místa zabezpečení", + "prompt.example.6": "Přidejte testy jednotek pro uživatelskou službu", + "prompt.example.7": "Refaktorujte tuto funkci, aby byla čitelnější", + "prompt.example.8": "Co tato chyba znamená?", + "prompt.example.9": "Pomozte mi odladit tento problém", + "prompt.example.10": "Vygenerujte dokumentaci API", + "prompt.example.11": "Optimalizujte databázové dotazy", + "prompt.example.12": "Přidejte ověření vstupu", + "prompt.example.13": "Vytvořit novou komponentu pro...", + "prompt.example.14": "Jak nasadím tento projekt?", + "prompt.example.15": "Osvědčené postupy naleznete v mém kódu", + "prompt.example.16": "Přidejte k této funkci zpracování chyb", + "prompt.example.17": "Vysvětlete tento vzor regulárního výrazu", + "prompt.example.18": "Převést na TypeScript", + "prompt.example.19": "Přidejte protokolování v celé kódové základně", + "prompt.example.20": "Jaké závislosti jsou zastaralé?", + "prompt.example.21": "Pomozte mi napsat migrační skript", + "prompt.example.22": "Implementujte ukládání do mezipaměti pro tento koncový bod", + "prompt.example.23": "Přidejte stránkování do tohoto seznamu", + "prompt.example.24": "Vytvořte příkaz CLI pro...", + "prompt.example.25": "Jak zde fungují proměnné prostředí?", + "prompt.popover.emptyResults": "Žádné odpovídající výsledky", + "prompt.popover.emptyCommands": "Žádné odpovídající příkazy", + "prompt.dropzone.label": "Sem přetáhněte obrázky PDFs nebo textové soubory", + "prompt.dropzone.file.label": "Přesuňte se na soubor @zmínka", + "prompt.slash.badge.custom": "vlastní", + "prompt.slash.badge.skill": "dovednost", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktivní", + "prompt.context.includeActiveFile": "Zahrnout aktivní soubor", + "prompt.context.removeActiveFile": "Odebrat aktivní soubor z kontextu", + "prompt.context.removeFile": "Odebrat soubor z kontextu", + "prompt.action.attachFile": "Přidat soubory", + "prompt.menu.addImagesAndFiles": "Přidejte soubory a další", + "prompt.menu.imagesAndFiles": "Obrázky a soubory", + "prompt.menu.commands": "Příkazy", + "prompt.menu.context": "Kontext", + "prompt.menu.shellCommand": "Shell příkaz", + "prompt.attachment.remove": "Odstraňte přílohu", + "prompt.action.send": "Odeslat", + "prompt.action.stop": "Přestaň", + "prompt.toast.pasteUnsupported.title": "Nepodporovaná příloha", + "prompt.toast.pasteUnsupported.description": "Zde lze připojit pouze obrázky, PDFs nebo textové soubory.", + "prompt.toast.attachmentDuplicate.title": "Tento soubor již byl nahrán", + "prompt.toast.modelAgentRequired.title": "Vyberte agenta a model", + "prompt.toast.modelAgentRequired.description": "Před odesláním výzvy vyberte zástupce a model.", + "prompt.toast.worktreeCreateFailed.title": "Vytvoření pracovního stromu se nezdařilo", + "prompt.toast.sessionCreateFailed.title": "Vytvoření relace se nezdařilo", + "prompt.toast.shellSendFailed.title": "Odeslání příkazu shellu se nezdařilo", + "prompt.toast.commandSendFailed.title": "Odeslání příkazu se nezdařilo", + "prompt.toast.promptSendFailed.title": "Odeslání výzvy se nezdařilo", + "prompt.toast.promptSendFailed.description": "Nelze načíst relaci", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{enabled}} z {{total}} povoleno", + "dialog.mcp.empty": "Nejsou nakonfigurovány žádné MCP", + "dialog.lsp.empty": "LSP automaticky detekovány z typů souborů", + "dialog.plugins.empty": "Pluginy nakonfigurované v opencode.json", + "mcp.status.connected": "připojeno", + "mcp.status.failed": "nepodařilo", + "mcp.status.needs_auth": "potřebuje autentizaci", + "mcp.status.disabled": "zakázáno", + "mcp.auth.clickToAuthenticate": "Klikněte pro ověření", + "dialog.fork.empty": "Žádné zprávy k rozvětvení", + "dialog.directory.search.placeholder": "Hledat složky", + "dialog.directory.empty": "Nebyly nalezeny žádné složky", + "dialog.directory.action.selectFile": "Vyberte soubor", + "dialog.directory.action.selectFolder": "Vyberte složku", + "dialog.directory.root": "Kořen", + "dialog.directory.parent": "Rodič", + "dialog.directory.readError": "Tuto složku nelze přečíst", + "app.server.unreachable": "Nelze dosáhnout {{server}}", + "app.server.retrying": "Automaticky opakování...", + "app.server.otherServers": "Jiné servery", + "dialog.server.title": "servery", + "dialog.server.description": "Přepněte, ke kterému OpenCode serveru se tato aplikace připojuje.", + "dialog.server.search.placeholder": "Vyhledávací servery", + "dialog.server.empty": "Zatím žádné servery", + "dialog.server.add.title": "Přidat server", + "dialog.server.add.url": "Adresa serveru", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Nelze se připojit k serveru", + "dialog.server.add.checking": "Kontrola...", + "dialog.server.add.button": "Přidat server", + "dialog.server.add.name": "Název serveru (volitelné)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "uživatelské jméno (volitelné)", + "dialog.server.add.usernamePlaceholder": "uživatelské jméno", + "dialog.server.add.password": "Heslo (volitelné)", + "dialog.server.add.passwordPlaceholder": "heslo", + "dialog.server.edit.title": "Upravit server", + "dialog.server.default.title": "Výchozí server", + "dialog.server.default.description": + "Připojte se k tomuto serveru při spuštění aplikace namísto spuštění místního serveru. Vyžaduje restart.", + "dialog.server.default.none": "Není vybrán žádný server", + "dialog.server.default.set": "Nastavit aktuální server jako výchozí", + "dialog.server.default.clear": "Jasný", + "dialog.server.action.remove": "Odebrat server", + "dialog.server.menu.edit": "Upravit", + "dialog.server.menu.default": "Nastavit jako výchozí", + "dialog.server.menu.defaultRemove": "Odebrat výchozí", + "dialog.server.menu.delete": "Odstranit", + "dialog.server.current": "Aktuální server", + "dialog.server.status.default": "Výchozí", + "wsl.server.add": "Přidat server WSL", + "wsl.server.addShort": "Přidat WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL server", + "wsl.server.retryStart": "Zkuste spustit znovu", + "wsl.server.updating": "Aktualizace...", + "wsl.onboarding.step.distro": "Vyberte distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Kontrola WSL...", + "wsl.onboarding.restartRequired": "Windows vyžaduje restart k dokončení instalace WSL.", + "wsl.onboarding.ready": "WSL je připraven.", + "wsl.onboarding.required": "Pro pokračování je vyžadováno WSL.", + "wsl.onboarding.checkingDistros": "Kontrola distribucí...", + "wsl.onboarding.installingDistro": "Instalace {{distro}}...", + "wsl.onboarding.checkingDistro": "Kontrola {{distro}}...", + "wsl.onboarding.listingDistros": "Výpis distribucí...", + "wsl.onboarding.distroReady": "{{distro}} je připraven.", + "wsl.onboarding.distroNotInstalled": "{{distro}} ještě není nainstalováno.", + "wsl.onboarding.openDistroOnce": "Pro dokončení nastavení jednou otevřete {{distro}}.", + "wsl.onboarding.finishingDistro": "Dokončování nastavení pro {{distro}}.", + "wsl.onboarding.pickDistro": "Vyberte distribuci nebo ji nainstalujte níže.", + "wsl.onboarding.checkingOpencode": "Kontrola OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Kontrola OpenCode v {{distro}}...", + "wsl.onboarding.updatingOpencode": "Aktualizace OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Aktualizace OpenCode v {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Aktualizace OpenCode v {{distro}}.", + "wsl.onboarding.updateOpencode": "Aktualizovat OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode je připraveno v {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode je připraven.", + "wsl.onboarding.installOpencodeIn": "Nainstalujte OpenCode do {{distro}}.", + "wsl.onboarding.installOpencode": "Instalovat OpenCode", + "wsl.onboarding.chooseDistroFirst": "Nejprve si vyberte distro.", + "wsl.onboarding.loadFailed": "Nepodařilo se načíst stav WSL.", + "wsl.onboarding.loading": "Načítání...", + "wsl.onboarding.installedDistros": "Nainstalované distribuce", + "wsl.onboarding.checkAgain": "Zkontrolujte znovu", + "wsl.onboarding.distroStatus.ready": "Připraveno", + "wsl.onboarding.distroStatus.checking": "Kontrola...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode není nainstalováno", + "wsl.onboarding.distroStatus.missingTools": "Chybí bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Nepodporováno · Použijte WSL 2", + "wsl.onboarding.needAnotherDistro": "Potřebujete další distribuci?", + "wsl.onboarding.needAnotherDistroHint": "Nainstalujte distribuci Linux z katalogu WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL není nainstalováno", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystém pro Linux) je vyžadován předtím, než OpenCode může přidat server WSL", + "wsl.onboarding.wslUnavailable.title": "WSL není k dispozici", + "wsl.onboarding.wslUnavailable.description": "OpenCode nemohl ověřit WSL na tomto počítači.", + "wsl.onboarding.installWsl": "Instalovat WSL", + "wsl.onboarding.windowsRestartRequired": + "Restartujte Windows pro dokončení instalace WSL a poté znovu otevřete OpenCode.", + "wsl.onboarding.next": "Další", + "wsl.onboarding.refresh": "Obnovit", + "wsl.onboarding.allDistrosAdded": "Všechny nainstalované distribuce jsou již přidány.", + "wsl.onboarding.noDistros": "Dosud nebyla zjištěna žádná distribuce.", + "wsl.onboarding.install": "Instalovat", + "wsl.onboarding.installing": "Instalace...", + "wsl.onboarding.installDistro": "Nainstalujte distro", + "wsl.onboarding.searchDistros": "Hledat distribuce", + "wsl.onboarding.wsl2Required": "WSL 2 je vyžadováno.", + "wsl.onboarding.toolsRequired": "Toto distro potřebuje bash a curl.", + "wsl.onboarding.openTerminal": "Otevřete terminál", + "wsl.onboarding.path": "Cesta: {{path}}", + "wsl.onboarding.notFound": "nenalezen", + "wsl.onboarding.version": "Verze: {{version}}", + "wsl.onboarding.unknown": "neznámý", + "wsl.onboarding.desktopVersion": "desktop {{version}}", + "wsl.onboarding.versionMismatch": "Nainstalovaná verze neodpovídá verzi aplikace pro stolní počítače.", + "wsl.onboarding.adding": "Přidávání...", + "help.tabs.toast.ariaLabel": "Představujeme karty. Uspořádejte si práci a aktivní relace pomocí karet", + "help.tabs.toast.dismiss": "Zavřít informace o kartách", + "help.tabs.title": "Představujeme karty", + "help.tabs.description": "Uspořádejte si práci a aktivní relace pomocí karet", + "help.tabs.date": "14. července", + "help.tabs.introduction": "OpenCode Plocha je nyní postavena na kartách.", + "help.tabs.sessions": + "Začněte novou relaci na kartě nebo otevřete existující relaci z libovolného z vašich projektů. Když začínáte s něčím novým, otevřete novou kartu a po dokončení ji zavřete.", + "help.tabs.organize": + "Ponechání několika otevřených karet usnadňuje organizaci aktivních relací. Přejmenujte karty na něco zapamatovatelného, ​​pokud si je plánujete ponechat.", + "help.tabs.home": + "Všechny relace a projekty najdete na nové domovské obrazovce. Výběrem relace ji otevřete na kartě.", + "help.tabs.persistence": "Když aplikaci znovu otevřete, vaše karty jsou stále otevřené.", + "help.tabs.worktrees": + "Nový design zatím nepodporuje Git Worktrees, brzy bude k dispozici. Pokud tedy chcete pokračovat v používání předchozího rozložení, můžete mezi rozloženími přepínat v Nastavení. Jen mějte na paměti, že nové rozložení se za několik týdnů stane trvalým.", + "server.row.noUsername": "žádné uživatelské jméno", + "dialog.project.edit.title": "Upravit projekt", + "dialog.project.edit.name": "Jméno", + "dialog.project.edit.icon": "ikona", + "dialog.project.edit.icon.alt": "Ikona projektu", + "dialog.project.edit.icon.hint": "Klikněte nebo přetáhněte obrázek", + "dialog.project.edit.icon.recommended": "Doporučeno: 128x128px", + "dialog.project.edit.color": "Barva", + "dialog.project.edit.color.select": "Vyberte barvu {{color}}", + "dialog.project.edit.worktree.startup": "Spouštěcí skript pracovního prostoru", + "dialog.project.edit.worktree.startup.description": + "Spustí se po vytvoření nového pracovního prostoru (pracovního stromu).", + "dialog.project.edit.worktree.startup.placeholder": "např. bun install", + "dialog.releaseNotes.action.getStarted": "Začněte", + "dialog.releaseNotes.action.next": "Další", + "dialog.releaseNotes.action.hideFuture": "V budoucnu je nezobrazujte", + "dialog.releaseNotes.media.alt": "Náhled vydání", + "context.breakdown.title": "Rozdělení kontextu", + "context.breakdown.note": 'Přibližný rozpis vstupních tokenů. "Jiné" zahrnuje definice nástrojů a režii.', + "context.breakdown.system": "Systém", + "context.breakdown.user": "Uživatel", + "context.breakdown.assistant": "asistent", + "context.breakdown.tool": "Volání nástroje", + "context.breakdown.other": "Jiné", + "context.systemPrompt.title": "Systémový prompt", + "context.rawMessages.title": "Syrové zprávy", + "context.export.session": "Export relace", + "context.stats.session": "Relace", + "context.stats.messages": "Zprávy", + "context.stats.provider": "Poskytovatel", + "context.stats.model": "Model", + "context.stats.limit": "Kontextový limit", + "context.stats.totalTokens": "Celkový počet tokenů", + "context.stats.usage": "Využití", + "context.stats.inputTokens": "Vstupní tokeny", + "context.stats.outputTokens": "Výstupní tokeny", + "context.stats.reasoningTokens": "Tokeny uvažování", + "context.stats.cacheTokens": "Tokeny mezipaměti (čtení/zápis)", + "context.stats.userMessages": "Uživatelské zprávy", + "context.stats.assistantMessages": "Zprávy Asistenta", + "context.stats.totalCost": "Celkové náklady", + "context.stats.sessionCreated": "Relace vytvořena", + "context.stats.lastActivity": "Poslední aktivita", + "context.usage.tokens": "Tokeny", + "context.usage.usage": "Využití", + "context.usage.cost": "náklady", + "context.usage.clickToView": "Kliknutím zobrazíte kontext", + "context.usage.view": "Zobrazit kontextové použití", + "language.en": "anglicky", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "německy", + "language.es": "španělsky", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "português (Brazílie)", + "language.bs": "Bosanský", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Turecko", + "toast.language.title": "Jazyk", + "toast.language.description": "Přepnuto na {{language}}", + "toast.theme.title": "Téma se změnilo", + "toast.scheme.title": "Barevné schéma", + "toast.workspace.enabled.title": "Pracovní prostory povoleny", + "toast.workspace.enabled.description": "V postranním panelu se nyní zobrazuje více pracovních stromů", + "toast.workspace.disabled.title": "Pracovní prostory jsou zakázány", + "toast.workspace.disabled.description": "Na postranním panelu je zobrazen pouze hlavní pracovní strom", + "toast.permissions.autoaccept.on.title": "Automatické přijímání oprávnění", + "toast.permissions.autoaccept.on.description": "Žádosti o povolení budou automaticky schváleny", + "toast.permissions.autoaccept.off.title": "Zastaveno automatické přijímání oprávnění", + "toast.permissions.autoaccept.off.description": "Žádosti o povolení budou vyžadovat schválení", + "toast.model.none.title": "Není vybrán žádný model", + "toast.model.none.description": "Chcete-li tuto relaci shrnout, připojte poskytovatele", + "toast.file.loadFailed.title": "Soubor se nepodařilo načíst", + "toast.file.listFailed.title": "Seznam souborů se nezdařil", + "toast.context.noLineSelection.title": "Žádný výběr řádku", + "toast.context.noLineSelection.description": "Nejprve vyberte rozsah řádků na kartě souboru.", + "toast.session.share.copyFailed.title": "Zkopírování URL do schránky se nezdařilo", + "toast.session.share.success.title": "Relace sdílena", + "toast.session.share.success.description": "Sdílet URL zkopírováno do schránky!", + "toast.session.share.failed.title": "Sdílení relace se nezdařilo", + "toast.session.share.failed.description": "Při sdílení relace došlo k chybě", + "toast.session.unshare.success.title": "Relace byla zrušena", + "toast.session.unshare.success.description": "Sdílení relace bylo úspěšně zrušeno!", + "toast.session.unshare.failed.title": "Zrušení sdílení relace se nezdařilo", + "toast.session.unshare.failed.description": "Při zrušení sdílení relace došlo k chybě", + "toast.session.export.success.title": "Relace byla exportována", + "toast.session.export.success.description": "Relace byla uložena do {{filename}}", + "toast.session.export.failed.title": "Export relace se nezdařil", + "toast.session.export.failed.description": "Při exportu relace došlo k chybě", + "toast.session.listFailed.title": "Nepodařilo se načíst relace pro {{project}}", + "toast.project.reloadFailed.title": "Nepodařilo se znovu načíst {{project}}", + "toast.update.title": "Aktualizace k dispozici", + "toast.update.description": "Nyní je k dispozici k instalaci nová verze OpenCode ({{version}}).", + "toast.update.action.installRestart": "Nainstalujte a restartujte", + "toast.update.action.notYet": "Ještě ne", + "error.page.title": "Něco se pokazilo", + "error.page.description": "Při načítání aplikace došlo k chybě.", + "error.page.description.localServerStartup": "Při spouštění místního serveru došlo k chybě.", + "error.page.details.label": "Podrobnosti o chybě", + "error.page.action.restart": "Restartujte", + "error.page.action.report": "Nahlásit chybu", + "error.page.action.reported": "Nahlášena chyba", + "error.page.action.exportLogs": "Exportovat protokoly", + "error.page.action.checking": "Kontrola...", + "error.page.action.checkUpdates": "Zkontrolujte aktualizace", + "error.page.action.updateTo": "Aktualizovat na {{version}}", + "error.page.circular": "[Kruhový]", + "error.page.report.prefix": "Nahlaste tuto chybu týmu OpenCode", + "error.page.report.discord": "dne Discord", + "error.page.version": "Verze: {{version}}", + "error.dev.rootNotFound": + "Kořenový prvek nenalezen. Zapomněli jste to přidat do index.html? Nebo je možná chyba v atributu id?", + "error.serverSync.connectFailed": "Nelze se připojit k serveru. Existuje server spuštěný na `{{url}}`?", + "error.serverSDK.noServerAvailable": "Žádný dostupný server", + "error.serverSDK.serverNotAvailable": "Server není dostupný", + "error.childStore.persistedCacheCreateFailed": "Nepodařilo se vytvořit trvalou mezipaměť", + "error.childStore.persistedProjectMetadataCreateFailed": "Nepodařilo se vytvořit trvalá metadata projektu", + "error.childStore.persistedProjectIconCreateFailed": "Nepodařilo se vytvořit trvalou ikonu projektu", + "error.childStore.storeCreateFailed": "Obchod se nepodařilo vytvořit", + "directory.error.invalidUrl": "Neplatný adresář v URL.", + "error.chain.unknown": "Neznámá chyba", + "error.server.invalidConfiguration": "Neplatná konfigurace", + "error.chain.causedBy": "Způsobeno:", + "error.chain.apiError": "API chyba", + "error.chain.status": "Stav: {{status}}", + "error.chain.retryable": "Opakovatelný pokus: {{retryable}}", + "error.chain.responseBody": "Tělo odpovědi:\n{{body}}", + "error.chain.didYouMean": "Měli jste na mysli: {{suggestions}}", + "error.chain.modelNotFound": "Model nenalezen: {{provider}}/{{model}}", + "error.chain.checkConfig": "Zkontrolujte název poskytovatele/modelu konfigurace (opencode.json).", + "error.chain.mcpFailed": 'MCP server "{{name}}" selhal. Poznámka: OpenCode zatím nepodporuje ověřování MCP.', + "error.chain.providerAuthFailed": "Ověření poskytovatele se nezdařilo ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Nepodařilo se inicializovat poskytovatele "{{provider}}". Zkontrolujte přihlašovací údaje a konfiguraci.', + "error.chain.configJsonInvalid": "Konfigurační soubor na {{path}} není platný JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Konfigurační soubor na {{path}} není platný JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Adresář "{{dir}}" v {{path}} není platný. Přejmenujte adresář na "{{suggestion}}" nebo jej odeberte. Toto je běžný překlep.', + "error.chain.configFrontmatterError": "Nepodařilo se analyzovat frontmatter v {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfigurační soubor na {{path}} je neplatný", + "error.chain.configInvalidWithMessage": "Konfigurační soubor na {{path}} je neplatný: {{message}}", + "notification.permission.title": "Vyžaduje se povolení", + "notification.permission.description": "{{sessionTitle}} v {{projectName}} potřebuje povolení", + "notification.question.title": "Otázka", + "notification.question.description": "{{sessionTitle}} v {{projectName}} má otázku", + "notification.action.goToSession": "Přejít na relaci", + "notification.session.responseReady.title": "Odpověď připravena", + "notification.session.error.title": "Chyba relace", + "notification.session.error.fallbackDescription": "Došlo k chybě", + "home.recentProjects": "Nedávné projekty", + "home.empty.title": "Žádné nedávné projekty", + "home.empty.description": "Začněte otevřením místního projektu", + "home.title": "Domů", + "home.projects": "Projekty", + "home.project.add": "Přidat projekt", + "home.recentlyClosed": "Nedávno zavřeno", + "home.server.collapse": "Sbalit projekty serveru", + "home.server.expand": "Rozbalte projekty serveru", + "home.sessions.search.placeholder": "Vyhledávání relací", + "home.sessions.search.placeholder.scoped": "Hledat relace v {{scope}}", + "home.sessions.search.sessions": "Relace", + "home.sessions.search.noResults": "Pro {{query}} nebyly nalezeny žádné relace", + "home.sessions.empty": "Tady zatím nic", + "home.sessions.empty.description": "Začněte vytvořením relace", + "home.sessions.group.today": "dnes", + "home.sessions.group.yesterday": "včera", + "home.sessions.group.older": "Starší", + "home.providerTip": + "Připojte se k více než 75 poskytovatelům a použijte další modely, včetně Claude, GPT, Gemini atd", + "session.tab.session": "Relace", + "session.tab.review": "Recenze", + "session.tab.context": "Kontext", + "session.tab.unknown": "Neznámá relace", + "session.panel.reviewAndFiles": "Recenze a soubory", + "session.error.notFound": "Tuto relaci nelze najít", + "session.error.notFound.description": "Tato karta ukazuje na relaci, která již na tomto serveru neexistuje.", + "session.error.notFound.closeTab": "Zavřít kartu", + "session.error.serverConnection": "K tomuto serveru se nelze připojit", + "session.review.filesChanged": "Soubory změněny {{count}}", + "session.review.change.one": "Změna", + "session.review.change.other": "Změny", + "session.review.loadingChanges": "Načítání změn...", + "session.review.empty": "V této relaci zatím žádné změny", + "session.review.noVcs": "Ne Git Zjištěn systém správy verzí, změny se nezobrazují", + "session.review.noVcs.createGit.title": "Vytvořte úložiště Git", + "session.review.noVcs.createGit.description": "Sledujte, kontrolujte a vracejte změny v tomto projektu", + "session.review.noVcs.createGit.actionLoading": "Vytváření Git úložiště...", + "session.review.noVcs.createGit.action": "Vytvořit Git úložiště", + "session.review.noSnapshot": "Sledování snímků je v konfiguraci zakázáno, takže změny relace nejsou dostupné", + "session.review.noChanges": "Žádné změny", + "session.review.noUncommittedChanges": "Zatím žádné neprovedené změny", + "session.review.noBranchChanges": "Zatím žádné změny ve větvi", + "session.files.selectToOpen": "Vyberte soubor, který chcete otevřít", + "session.files.all": "Všechny soubory", + "session.files.empty": "Žádné soubory", + "session.files.binaryContent": "Binární soubor (obsah nelze zobrazit)", + "session.messages.renderEarlier": "Vykreslit dřívější zprávy", + "session.messages.loadingEarlier": "Načítání dřívějších zpráv...", + "session.messages.loadEarlier": "Načíst dřívější zprávy", + "session.messages.loading": "Načítání zpráv...", + "session.messages.jumpToLatest": "Přejít na nejnovější", + "session.context.addToContext": "Přidejte {{selection}} do kontextu", + "session.todo.title": "Úkoly", + "session.todo.collapse": "kolaps", + "session.todo.expand": "Rozbalit", + "session.todo.progress": "Dokončeno {{done}} z {{total}} úkolů", + "session.question.progress": "{{current}} z {{total}} otázek", + "session.question.minimize": "Minimalizujte otázku", + "session.question.restore": "Obnovit otázku", + "session.question.pending.one": "{{count}} nevyřízená otázka", + "session.question.pending.other": "{{count}} nevyřízené otázky", + "session.question.pending.few": "{{count}} nevyřízené otázky", + "session.question.pending.many": "{{count}} nevyřízených otázek", + "session.followupDock.summary.one": "{{count}} zpráva ve frontě", + "session.followupDock.summary.other": "{{count}} zpráv ve frontě", + "session.followupDock.summary.few": "{{count}} zprávy ve frontě", + "session.followupDock.summary.many": "{{count}} zprávy ve frontě", + "session.followupDock.sendNow": "Poslat nyní", + "session.followupDock.edit": "Upravit", + "session.followupDock.collapse": "Sbalit zprávy ve frontě", + "session.followupDock.expand": "Rozbalte zprávy ve frontě", + "session.revertDock.summary.one": "{{count}} vrácená zpráva", + "session.revertDock.summary.other": "{{count}} vrátilo zpět zprávy", + "session.revertDock.summary.few": "{{count}} vrácené zprávy", + "session.revertDock.summary.many": "{{count}} vrácené zprávy", + "session.revertDock.collapse": "Sbalit vrácené zprávy", + "session.revertDock.expand": "Rozbalte vrácené zprávy", + "session.revertDock.restore": "Obnovit zprávu", + "session.new.title": "Postavte cokoliv", + "session.new.project.new": "Nový projekt", + "session.new.project.search": "Hledat projekty", + "session.new.project.add": "Přidat projekt", + "session.new.worktree.main": "Hlavní větev", + "session.new.worktree.mainWithBranch": "Hlavní větev ({{branch}})", + "session.new.worktree.create": "Vytvořte nový pracovní strom", + "session.new.workspace.runIn": "Spustit relaci", + "session.new.workspace.triggerLocal": "Místní", + "session.new.workspace.local": "Místní úložiště", + "session.new.workspace.existing": "Pracovní prostor…", + "session.new.git.none": "ne Git", + "session.new.lastModified": "Naposledy upraveno", + "session.header.search.placeholder": "Hledat {{project}}", + "session.header.searchFiles": "Hledat soubory", + "session.header.openIn": "Otevřít v", + "session.header.open.action": "Otevřít {{app}}", + "session.header.open.ariaLabel": "Otevřít v {{app}}", + "session.header.open.menu": "Otevřete možnosti", + "session.header.open.copyPath": "Kopírovat cestu", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "Správce souborů", + "session.header.reveal.finder": "Odhalit v Finder", + "session.header.reveal.fileExplorer": "Odhalit v File Explorer", + "session.header.reveal.containingFolder": "Otevřete složku obsahující", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminál", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Stav", + "status.popover.ariaLabel": "Konfigurace serveru", + "status.popover.tab.servers": "servery", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Pluginy", + "status.popover.action.manageServers": "Správa serverů", + "session.share.popover.title": "Publikovat na webu", + "session.share.popover.description.shared": "Tato relace je veřejná na webu. Je přístupný komukoli s odkazem.", + "session.share.popover.description.unshared": "Sdílejte relaci veřejně na webu. Bude přístupný komukoli s odkazem.", + "session.share.action.share": "Sdílejte", + "session.share.action.publish": "Publikovat", + "session.share.action.publishing": "Publikování...", + "session.share.action.unpublish": "Zrušit publikování", + "session.share.action.unpublishing": "Rušení publikování...", + "session.share.action.view": "Zobrazit", + "session.share.copy.copied": "Zkopírováno", + "session.share.copy.copyLink": "Zkopírujte odkaz", + "lsp.tooltip.none": "Žádné LSP servery", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Načítání výzvy...", + "terminal.loading": "Načítání terminálu...", + "terminal.title": "Terminál", + "terminal.title.numbered": "Terminál {{number}}", + "terminal.close": "Zavřete terminál", + "terminal.connectionLost.title": "Připojení ztraceno", + "terminal.connectionLost.abnormalClose": "WebSocket abnormálně uzavřeno: {{code}}", + "terminal.connectionLost.description": + "Spojení terminálu bylo přerušeno. To se může stát, když se server restartuje.", + "terminal.connectTicket.csrfError": + "PTY lístek připojení zamítnut původem nebo CSRF kontrolami. Zkontrolujte konfiguraci serveru CORS.", + "terminal.connectTicket.statusError": "PTY lístek připojení se nezdařil s {{status}}", + "titlebar.update": "Aktualizovat", + "titlebar.updateVersion": "Aktualizovat {{version}}", + "common.closeTab": "Zavřít kartu", + "common.dismiss": "Odmítnout", + "common.moreCountSuffix": " (+{{count}} další)", + "common.requestFailed": "Požadavek se nezdařil", + "common.moreOptions": "Více možností", + "common.learnMore": "Zjistěte více", + "common.rename": "Přejmenovat", + "common.export": "Exportovat", + "common.reset": "Resetovat", + "common.archive": "Archiv", + "common.delete": "Odstranit", + "common.close": "Zavřít", + "common.edit": "Upravit", + "common.loadMore": "Načíst více", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "neznámý", + "common.time.justNow": "Právě teď", + "common.time.minutesAgo.short": "před {{count}}min", + "common.time.hoursAgo.short": "před {{count}}h", + "common.time.daysAgo.short": "před {{count}} d", + "sidebar.menu.toggle": "Přepnout nabídku", + "sidebar.nav.projectsAndSessions": "Projekty a relace", + "sidebar.settings": "Nastavení", + "sidebar.help": "Pomoc", + "sidebar.workspaces.enable": "Povolit pracovní prostory", + "sidebar.workspaces.disable": "Zakázat pracovní prostory", + "sidebar.gettingStarted.title": "Začínáme", + "sidebar.gettingStarted.line1": "OpenCode obsahuje bezplatné modely, takže můžete začít okamžitě.", + "sidebar.gettingStarted.line2": + "Připojte libovolného poskytovatele, abyste mohli používat modely, vč. Claude, GPT, Gemini atd.", + "sidebar.project.recentSessions": "Nedávné relace", + "sidebar.project.viewAllSessions": "Zobrazit všechny relace", + "sidebar.project.clearNotifications": "Vymazat oznámení", + "sidebar.empty.title": "Nejsou otevřeny žádné projekty", + "sidebar.empty.description": "Chcete-li začít, otevřete projekt", + "debugBar.ariaLabel": "Diagnostika vývoje výkonnosti", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Poslední dokončený přechod trasy dotykem stránky relace, měřeno od spuštění routeru do prvního nátěru po jeho usazení.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Pohyblivé snímky za sekundu za posledních 5 sekund.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Nejhorší doba snímku za posledních 5 sekund.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Snímky delší než 32 ms za posledních 5 sekund.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blokovaný čas a počet dlouhých úkolů za posledních 5 sekund. Maximální úkol: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Nejhorší pozorované zpoždění vstupu za posledních 5 sekund.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Přibližná doba trvání interakce za posledních 5 sekund. Toto je INP, nikoli oficiální Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativní změna rozvržení pro aktuální životnost aplikace.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Použitá halda JS vs limit haldy. pouze Chromium.", + "debugBar.mem.tip": "Použitá halda JS vs limit haldy. {{used}} z {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Vynutit styly zaměření na všechny interaktivní prvky", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "VYPNUTO", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Vynutit úplný směr rozvržení aplikace bez změny vybraného jazyka", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Desktop", + "settings.section.server": "Server", + "settings.tab.general": "Generál", + "settings.tab.shortcuts": "Zkratky", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integrace", + "settings.desktop.wsl.description": "Spusťte server OpenCode uvnitř WSL na Windows.", + "settings.general.section.appearance": "Vzhled", + "settings.general.section.advanced": "Pokročilé", + "settings.general.section.notifications": "Systémová upozornění", + "settings.general.section.updates": "Aktualizace", + "settings.general.section.sounds": "Zvukové efekty", + "settings.general.section.feed": "Krmivo", + "settings.general.section.display": "Displej", + "settings.general.row.language.title": "Jazyk", + "settings.general.row.language.description": "Změnit jazyk zobrazení pro OpenCode", + "settings.general.row.shell.title": "Shell terminálu", + "settings.general.row.shell.description": "Shell používaný terminálem a nástroji agenta", + "settings.general.row.shell.autoDefault": "Auto (výchozí)", + "settings.general.row.shell.terminalOnly": "pouze terminál", + "settings.general.row.appearance.title": "Vzhled", + "settings.general.row.appearance.description": "Přizpůsobte si vzhled OpenCode na vašem zařízení", + "settings.general.row.colorScheme.title": "Barevné schéma", + "settings.general.row.colorScheme.description": + "Zvolte, zda bude OpenCode následovat systémové, světlé nebo tmavé téma", + "settings.general.row.theme.title": "téma", + "settings.general.row.theme.description": "Přizpůsobte, jak je téma OpenCode.", + "settings.general.row.font.title": "Písmo kódu", + "settings.general.row.font.description": "Přizpůsobte písmo používané v blocích kódu", + "settings.general.row.terminalFont.title": "Terminálové písmo", + "settings.general.row.terminalFont.description": "Přizpůsobte písmo použité v terminálu", + "settings.general.row.uiFont.title": "UI Písmo", + "settings.general.row.uiFont.description": "Přizpůsobte písmo používané v celém rozhraní", + "settings.general.row.followup.title": "Následné chování", + "settings.general.row.followup.description": "Zvolte, zda se mají následné výzvy řídit okamžitě nebo čekat ve frontě", + "settings.general.row.followup.option.queue": "Fronta", + "settings.general.row.followup.option.steer": "Řídit", + "settings.general.row.showFileTree.title": "Strom souborů", + "settings.general.row.showFileTree.description": "Zobrazit panel stromu souborů v relacích", + "settings.general.row.showNavigation.title": "Ovládací prvky navigace", + "settings.general.row.showNavigation.description": "Zobrazte tlačítka zpět a vpřed v záhlaví plochy", + "settings.general.row.showSearch.title": "Paleta příkazů", + "settings.general.row.showSearch.description": "Zobrazit tlačítko vyhledávací a příkazové palety v záhlaví", + "settings.general.row.showTerminal.title": "Terminál", + "settings.general.row.showTerminal.description": "Zobrazit tlačítko terminálu v záhlaví plochy", + "settings.general.row.showStatus.title": "Stav serveru", + "settings.general.row.showStatus.description": "Zobrazit tlačítko stavu serveru v záhlaví", + "settings.general.row.mobileTitlebarBottom.title": "Spodní navigace", + "settings.general.row.mobileTitlebarBottom.description": + "Umístěte záhlaví a karty relací do spodní části obrazovky na mobilu", + "settings.general.row.showCustomAgents.title": "Zobrazit agenta", + "settings.general.row.showCustomAgents.description": + "Přepínání mezi agenty ve skladateli. Když je skrytý, výchozí nastavení je Sestavit agenta.", + "settings.general.row.reasoningSummaries.title": "Ukažte shrnutí odůvodnění", + "settings.general.row.reasoningSummaries.description": "Zobrazte souhrny zdůvodnění modelu na časové ose", + "settings.general.row.shellToolPartsExpanded.title": "Rozbalte části nástroje shell", + "settings.general.row.shellToolPartsExpanded.description": + "Ve výchozím nastavení zobrazit na časové ose rozbalené části nástroje shell", + "settings.general.row.editToolPartsExpanded.title": "Rozbalte části nástroje pro úpravy", + "settings.general.row.editToolPartsExpanded.description": + "Ve výchozím nastavení zobrazit na časové ose rozbalené části nástroje pro úpravy, zápis a opravy", + "settings.general.row.newInterface.title": "Nové rozložení", + "settings.general.row.newInterface.badge": "Nové", + "settings.general.row.newInterface.description": + "Použijte nové karty a domovské rozvržení. Přepínejte mezi rozvrženími po omezenou dobu.", + "settings.general.row.newInterfaceNotice.title": "Nyní používáte nové rozvržení", + "settings.general.row.newInterfaceNotice.description": "Předchozí rozložení již není k dispozici", + "settings.general.row.newInterfaceNotice.dismiss": "Odmítnout", + "settings.general.row.pinchZoom.title": "Stažením prstů přiblížíte", + "settings.general.row.pinchZoom.description": "Povolit přiblížení pomocí gest trackpadu a gesta Ctrl posouvání", + "settings.general.row.wayland.title": "Použít nativní Wayland", + "settings.general.row.wayland.description": "Zakázat zálohu X11 na Wayland. Vyžaduje restart.", + "settings.general.row.wayland.tooltip": + "Na Linux s monitory se smíšenou obnovovací frekvencí může být nativní Wayland stabilnější.", + "settings.general.row.releaseNotes.title": "Poznámky k vydání", + "settings.general.row.releaseNotes.description": "Zobrazit vyskakovací okna Co je nového po aktualizacích", + "settings.updates.row.startup.title": "Zkontrolujte aktualizace při spuštění", + "settings.updates.row.startup.description": "Automaticky kontrolovat aktualizace při spuštění OpenCode", + "settings.updates.row.check.title": "Zkontrolujte aktualizace", + "settings.updates.row.check.description": "Ručně zkontrolujte aktualizace a nainstalujte je, pokud jsou k dispozici", + "settings.updates.action.checkNow": "Zkontrolujte nyní", + "settings.updates.action.checking": "Kontrola...", + "settings.updates.action.downloading": "Stahování...", + "settings.updates.action.installing": "Instalace...", + "settings.updates.toast.latest.title": "Jste aktuální", + "settings.updates.toast.latest.description": "Používáte nejnovější verzi OpenCode.", + "sound.option.none": "žádný", + "sound.option.alert01": "Upozornění 01", + "sound.option.alert02": "Upozornění 02", + "sound.option.alert03": "Upozornění 03", + "sound.option.alert04": "Upozornění 04", + "sound.option.alert05": "Upozornění 05", + "sound.option.alert06": "Upozornění 06", + "sound.option.alert07": "Upozornění 07", + "sound.option.alert08": "Upozornění 08", + "sound.option.alert09": "Upozornění 09", + "sound.option.alert10": "Upozornění 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Ne 01", + "sound.option.nope02": "Ne 02", + "sound.option.nope03": "Ne 03", + "sound.option.nope04": "Ne 04", + "sound.option.nope05": "Ne 05", + "sound.option.nope06": "Ne 06", + "sound.option.nope07": "Ne 07", + "sound.option.nope08": "Ne 08", + "sound.option.nope09": "Ne 09", + "sound.option.nope10": "Ne 10", + "sound.option.nope11": "Ne 11", + "sound.option.nope12": "Ne 12", + "sound.option.yup01": "Ano 01", + "sound.option.yup02": "Ano 02", + "sound.option.yup03": "Ano 03", + "sound.option.yup04": "Ano 04", + "sound.option.yup05": "Ano 05", + "sound.option.yup06": "Ano 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Zobrazit systémové upozornění, když je agent kompletní nebo vyžaduje pozornost", + "settings.general.notifications.permissions.title": "Oprávnění", + "settings.general.notifications.permissions.description": + "Zobrazit systémové upozornění, když je vyžadováno oprávnění", + "settings.general.notifications.errors.title": "Chyby", + "settings.general.notifications.errors.description": "Zobrazit systémové upozornění, když dojde k chybě", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Přehrajte zvuk, když je agent kompletní nebo vyžaduje pozornost", + "settings.general.sounds.permissions.title": "Oprávnění", + "settings.general.sounds.permissions.description": "Přehrát zvuk, když je vyžadováno povolení", + "settings.general.sounds.errors.title": "Chyby", + "settings.general.sounds.errors.description": "Přehrát zvuk, když dojde k chybě", + "settings.shortcuts.title": "Klávesové zkratky", + "settings.shortcuts.reset.button": "Obnovit výchozí nastavení", + "settings.shortcuts.reset.toast.title": "Zkratky byly resetovány", + "settings.shortcuts.reset.toast.description": "Klávesové zkratky byly resetovány na výchozí hodnoty.", + "settings.shortcuts.conflict.title": "Zástupce se již používá", + "settings.shortcuts.conflict.description": "{{keybind}} je již přiřazeno {{titles}}.", + "settings.shortcuts.unassigned": "Nepřiřazeno", + "settings.shortcuts.pressKeys": "Stiskněte klávesy", + "settings.shortcuts.search.placeholder": "Vyhledávací zkratky", + "settings.shortcuts.search.empty": "Nebyli nalezeni žádní zástupci", + "settings.shortcuts.group.general": "Generál", + "settings.shortcuts.group.session": "Relace", + "settings.shortcuts.group.navigation": "Navigace", + "settings.shortcuts.group.modelAndAgent": "Model a agent", + "settings.shortcuts.group.terminal": "Terminál", + "settings.shortcuts.group.prompt": "Prompt", + "settings.providers.title": "Poskytovatelé", + "settings.providers.description": "Zde bude možné konfigurovat nastavení poskytovatele.", + "settings.providers.section.connected": "Připojení poskytovatelé", + "settings.providers.connected.empty": "Žádní připojení poskytovatelé", + "settings.providers.connected.environmentDescription": "Připojeno z vašich proměnných prostředí", + "settings.providers.section.popular": "Populární poskytovatelé", + "settings.providers.custom.description": "Přidejte poskytovatele kompatibilního s OpenAI podle základny URL.", + "settings.providers.tag.environment": "Životní prostředí", + "settings.providers.tag.config": "Konfigurace", + "settings.providers.tag.custom": "Vlastní", + "settings.providers.tag.other": "Jiné", + "settings.models.title": "Modely", + "settings.models.description": "Zde bude možné konfigurovat nastavení modelu.", + "settings.agents.title": "Agenti", + "settings.agents.description": "Zde bude možné konfigurovat nastavení agenta.", + "settings.commands.title": "Příkazy", + "settings.commands.description": "Zde bude možné konfigurovat nastavení příkazů.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Zde bude možné konfigurovat nastavení MCP.", + "settings.permissions.title": "Oprávnění", + "settings.permissions.description": "Určete, jaké nástroje může server ve výchozím nastavení používat.", + "settings.permissions.section.tools": "Nástroje", + "settings.permissions.toast.updateFailed.title": "Aktualizace oprávnění se nezdařila", + "settings.permissions.action.allow": "Povolit", + "settings.permissions.action.ask": "Zeptejte se", + "settings.permissions.action.deny": "Odmítnout", + "settings.permissions.tool.read.title": "Přečtěte si", + "settings.permissions.tool.read.description": "Čtení souboru (odpovídá cestě k souboru)", + "settings.permissions.tool.edit.title": "Upravit", + "settings.permissions.tool.edit.description": "Upravte soubory, včetně úprav, zápisů a oprav", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Porovnejte soubory pomocí vzorů glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Prohledávejte obsah souboru pomocí regulárních výrazů", + "settings.permissions.tool.list.title": "Seznam", + "settings.permissions.tool.list.description": "Seznam souborů v adresáři", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Spusťte příkazy shellu", + "settings.permissions.tool.task.title": "Úkol", + "settings.permissions.tool.task.description": "Spusťte dílčí agenty", + "settings.permissions.tool.skill.title": "Dovednost", + "settings.permissions.tool.skill.description": "Načtěte dovednost podle názvu", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Spusťte dotazy na jazykový server", + "settings.permissions.tool.todowrite.title": "Todo Napište", + "settings.permissions.tool.todowrite.description": "Aktualizujte seznam úkolů", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Načíst obsah z URL", + "settings.permissions.tool.websearch.title": "Vyhledávání na webu", + "settings.permissions.tool.websearch.description": "Hledejte na webu", + "settings.permissions.tool.external_directory.title": "Externí adresář", + "settings.permissions.tool.external_directory.description": "Přístup k souborům mimo adresář projektu", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Detekce opakovaných volání nástrojů se stejným zadáním", + "session.delete.failed.title": "Smazání relace se nezdařilo", + "session.delete.title": "Odstranit relaci", + "session.delete.confirm": 'Odstranit relaci "{{name}}"?', + "session.delete.button": "Odstranit relaci", + "workspace.new": "Nový pracovní prostor", + "workspace.type.local": "místní", + "workspace.type.sandbox": "pískoviště", + "workspace.create.failed.title": "Vytvoření pracovního prostoru se nezdařilo", + "workspace.delete.failed.title": "Smazání pracovního prostoru se nezdařilo", + "workspace.resetting.title": "Resetování pracovního prostoru", + "workspace.resetting.description": "Může to chvíli trvat.", + "workspace.reset.failed.title": "Obnovení pracovního prostoru se nezdařilo", + "workspace.reset.success.title": "Obnovení pracovního prostoru", + "workspace.reset.success.description": "Pracovní prostor nyní odpovídá výchozí větvi.", + "workspace.error.stillPreparing": "Pracovní plocha se stále připravuje", + "workspace.status.checking": "Kontrola nesloučených změn...", + "workspace.status.error": "Nelze ověřit stav git.", + "workspace.status.clean": "Nebyly zjištěny žádné nesloučené změny.", + "workspace.status.dirty": "V tomto pracovním prostoru byly zjištěny nesloučené změny.", + "workspace.delete.title": "Odstranit pracovní prostor", + "workspace.delete.confirm": 'Odstranit pracovní prostor "{{name}}"?', + "workspace.delete.button": "Odstranit pracovní prostor", + "workspace.reset.title": "Obnovit pracovní prostor", + "workspace.reset.confirm": "Resetovat pracovní prostor „{{name}}“?", + "workspace.reset.button": "Obnovit pracovní prostor", + "workspace.reset.archived.none": "Žádné aktivní relace nebudou archivovány.", + "workspace.reset.archived.one": "1 relace bude archivována.", + "workspace.reset.archived.many": "{{count}} relací bude archivováno.", + "workspace.reset.note": "Tím se pracovní prostor resetuje tak, aby odpovídal výchozí větvi.", + "dialog.usageExceeded.dontShowAgain": "Znovu nezobrazovat", +} diff --git a/packages/app/src/i18n/desktop-native.ts b/packages/app/src/i18n/desktop-native.ts new file mode 100644 index 0000000000000000000000000000000000000000..1aa9bd39f76e31535bfc635259b0cdb9da349175 --- /dev/null +++ b/packages/app/src/i18n/desktop-native.ts @@ -0,0 +1,365 @@ +export const DESKTOP_NATIVE_LOCALES = [ + "en", + "zh", + "zht", + "ko", + "de", + "es", + "fr", + "da", + "ja", + "pl", + "ru", + "uk", + "bs", + "ar", + "no", + "br", + "th", + "tr", + "hi", + "nl", + "id", + "vi", + "it", + "ur", + "pa", + "az", + "fi", + "sv", + "am", + "bg", + "bn", + "ca", + "cs", + "dv", + "dz", + "el", + "et", + "fa", + "fo", + "hr", + "hu", + "hy", + "is", + "ka", + "km", + "lo", + "lt", + "lv", + "mk", + "mn", + "ms", + "my", + "ne", + "ro", + "si", + "sk", + "sl", + "sq", + "sr", + "tg", + "tk", + "uz", +] as const + +export type DesktopNativeLocale = (typeof DESKTOP_NATIVE_LOCALES)[number] + +export const DESKTOP_NATIVE_LABELS: Record = { + en: "English", + zh: "简体中文", + zht: "繁體中文", + ko: "한국어", + de: "Deutsch", + es: "Español", + fr: "Français", + da: "Dansk", + ja: "日本語", + pl: "Polski", + ru: "Русский", + uk: "Українська", + bs: "Bosanski", + ar: "العربية", + no: "Norsk", + br: "Português (Brasil)", + th: "ไทย", + tr: "Türkçe", + hi: "हिन्दी", + nl: "Nederlands", + id: "Bahasa Indonesia", + vi: "Tiếng Việt", + it: "Italiano", + ur: "اردو", + pa: "پنجابی", + az: "Azərbaycanca", + fi: "Suomi", + sv: "Svenska", + am: "አማርኛ", + bg: "Български", + bn: "বাংলা", + ca: "Català", + cs: "Čeština", + dv: "ދިވެހި", + dz: "རྫོང་ཁ", + el: "Ελληνικά", + et: "Eesti", + fa: "فارسی", + fo: "Føroyskt", + hr: "Hrvatski", + hu: "Magyar", + hy: "Հայերեն", + is: "Íslenska", + ka: "ქართული", + km: "ខ្មែរ", + lo: "ລາວ", + lt: "Lietuvių", + lv: "Latviešu", + mk: "Македонски", + mn: "Монгол", + ms: "Bahasa Melayu", + my: "မြန်မာ", + ne: "नेपाली", + ro: "Română", + si: "සිංහල", + sk: "Slovenčina", + sl: "Slovenščina", + sq: "Shqip", + sr: "Српски", + tg: "Тоҷикӣ", + tk: "Türkmençe", + uz: "Oʻzbekcha", +} + +export const DESKTOP_NATIVE_LOCALE_TAGS: Record = { + en: "en", + zh: "zh-Hans", + zht: "zh-Hant", + ko: "ko", + de: "de", + es: "es", + fr: "fr", + da: "da", + ja: "ja", + pl: "pl", + ru: "ru", + uk: "uk", + bs: "bs", + ar: "ar", + no: "nb-NO", + br: "pt-BR", + th: "th", + tr: "tr", + hi: "hi-IN", + nl: "nl-NL", + id: "id-ID", + vi: "vi-VN", + it: "it-IT", + ur: "ur-PK", + pa: "pa-Arab-PK", + az: "az-Latn-AZ", + fi: "fi-FI", + sv: "sv-SE", + am: "am-ET", + bg: "bg-BG", + bn: "bn-BD", + ca: "ca-AD", + cs: "cs-CZ", + dv: "dv-MV", + dz: "dz-BT", + el: "el-GR", + et: "et-EE", + fa: "fa-IR", + fo: "fo-FO", + hr: "hr-HR", + hu: "hu-HU", + hy: "hy-AM", + is: "is-IS", + ka: "ka-GE", + km: "km-KH", + lo: "lo-LA", + lt: "lt-LT", + lv: "lv-LV", + mk: "mk-MK", + mn: "mn-MN", + ms: "ms-MY", + my: "my-MM", + ne: "ne-NP", + ro: "ro-RO", + si: "si-LK", + sk: "sk-SK", + sl: "sl-SI", + sq: "sq-AL", + sr: "sr-Cyrl-RS", + tg: "tg-Cyrl-TJ", + tk: "tk-Latn-TM", + uz: "uz-Latn-UZ", +} + +export function detectDesktopNativeLocale(languages: readonly string[]): DesktopNativeLocale { + for (const language of languages) { + const source = locale(language) + if (!source) continue + if (["no", "nb", "nn"].includes(source.language)) return "no" + const match = DESKTOP_NATIVE_LOCALES.find((candidate) => { + const target = locale(DESKTOP_NATIVE_LOCALE_TAGS[candidate]) + return target?.language === source.language && target.script === source.script + }) + if (match) return match + } + return "en" +} + +export function desktopNativePluralCategories(locale: DesktopNativeLocale) { + return new Intl.PluralRules(DESKTOP_NATIVE_LOCALE_TAGS[locale]).resolvedOptions().pluralCategories +} + +function locale(value: string) { + try { + return new Intl.Locale(value).maximize() + } catch { + return undefined + } +} + +export const DESKTOP_NATIVE_ENGLISH = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "File", + "desktop.menu.edit": "Edit", + "desktop.menu.view": "View", + "desktop.menu.go": "Go", + "desktop.menu.window": "Window", + "desktop.menu.help": "Help", + "desktop.menu.checkForUpdates": "Check for Updates...", + "desktop.menu.settings": "Settings", + "desktop.menu.reloadWebview": "Reload Webview", + "desktop.menu.restart": "Restart", + "desktop.menu.exportLogs": "Export Logs...", + "desktop.menu.newSession": "New Session", + "desktop.menu.openProject": "Open Project...", + "desktop.menu.newWindow": "New Window", + "desktop.menu.closeWindow": "Close Window", + "desktop.menu.undo": "Undo", + "desktop.menu.redo": "Redo", + "desktop.menu.cut": "Cut", + "desktop.menu.copy": "Copy", + "desktop.menu.paste": "Paste", + "desktop.menu.delete": "Delete", + "desktop.menu.selectAll": "Select All", + "desktop.menu.toggleSidebar": "Toggle Sidebar", + "desktop.menu.toggleTerminal": "Toggle Terminal", + "desktop.menu.toggleFileTree": "Toggle File Tree", + "desktop.menu.reload": "Reload", + "desktop.menu.toggleDeveloperTools": "Toggle Developer Tools", + "desktop.menu.actualSize": "Actual Size", + "desktop.menu.zoomIn": "Zoom In", + "desktop.menu.zoomOut": "Zoom Out", + "desktop.menu.toggleFullScreen": "Toggle Full Screen", + "desktop.menu.back": "Back", + "desktop.menu.forward": "Forward", + "desktop.menu.previousSession": "Previous Session", + "desktop.menu.nextSession": "Next Session", + "desktop.menu.previousProject": "Previous Project", + "desktop.menu.nextProject": "Next Project", + "desktop.menu.minimize": "Minimize", + "desktop.menu.maximize": "Maximize", + "desktop.menu.documentation": "OpenCode Documentation", + "desktop.menu.supportForum": "Support Forum", + "desktop.menu.shareFeedback": "Share Feedback", + "desktop.menu.reportBug": "Report a Bug", + "desktop.menu.ariaLabel": "OpenCode menu", + + "desktop.updater.dialog.checkFailed.message": "Update check failed.", + "desktop.updater.dialog.checkFailed.title": "Update Error", + "desktop.updater.dialog.upToDate.message": "You're up to date.", + "desktop.updater.dialog.upToDate.title": "No Updates", + "desktop.updater.dialog.ready.message": "Update {{version}} downloaded. Restart now?", + "desktop.updater.dialog.ready.title": "Update Ready", + "desktop.updater.dialog.restart": "Restart", + "desktop.updater.dialog.later": "Later", + + "desktop.recovery.action.relaunch": "Relaunch", + "desktop.recovery.action.exportLogs": "Export Logs", + "desktop.recovery.action.keepWaiting": "Keep Waiting", + "desktop.recovery.action.quit": "Quit", + "desktop.recovery.loadFailed": "OpenCode failed to load", + "desktop.recovery.terminated": "OpenCode window terminated unexpectedly", + "desktop.recovery.unresponsive": "OpenCode is not responding", + "desktop.recovery.unresponsive.detail": "You can relaunch the app, open the logs, or keep waiting.", + "desktop.recovery.loadFailed.detail": "Window: {{window}}\nURL: {{url}}\nError: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Window: {{window}}\nReason: {{reason}}\nCode: {{code}}", + "desktop.recovery.unknown": "", + + "desktop.dialog.chooseFolder": "Choose a folder", + "desktop.dialog.chooseFile": "Choose a file", + "desktop.dialog.saveFile": "Save file", + "desktop.dialog.files": "Files", + "desktop.server.local": "Local Server", + + "desktop.wsl.error.windowsOnly": "WSL is only available on Windows", + "desktop.wsl.error.unavailable": "WSL is unavailable", + "desktop.wsl.error.listInstalled": "Failed to list installed WSL distros", + "desktop.wsl.error.listOnline": "Failed to list online WSL distros", + "desktop.wsl.error.executeDistro": "Cannot execute commands in distro", + "desktop.wsl.error.installWsl": "WSL installation failed", + "desktop.wsl.error.installDistro": "Failed to install distro: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode installation failed", + "desktop.wsl.error.alreadyAdded": "{{distro}} is already added", + "desktop.wsl.error.opencodeMissing": "opencode is not installed in this distro", + "desktop.wsl.error.opencodeCannotRun": "opencode is installed but could not run", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode is not installed in {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode update finished but {{distro}} still reports {{installed}}; expected {{expected}}", + "desktop.wsl.error.noVersion": "no version", + "desktop.wsl.error.serverExited": "WSL server exited after startup (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL server exited before becoming healthy (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Sidecar for {{distro}} health check timed out after {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} timed out after {{timeout}}ms", + "desktop.wsl.error.failedPort": "Failed to get port", + + "desktop.picker.error.notSelected": "File was not selected by the picker", + "desktop.picker.error.sizeLimit": "Selected attachments exceed the {{limit}} MB limit", +} as const + +export type DesktopNativeKey = keyof typeof DESKTOP_NATIVE_ENGLISH +export type DesktopNativeMessages = Record +export type DesktopNativeBundle = { locale: DesktopNativeLocale; messages: DesktopNativeMessages } + +export const DESKTOP_NATIVE_KEYS = Object.keys(DESKTOP_NATIVE_ENGLISH) as DesktopNativeKey[] +export const DESKTOP_NATIVE_MAX_PAYLOAD_BYTES = 64 * 1024 + +export function createDesktopNativeBundle( + locale: DesktopNativeLocale, + translate: (key: DesktopNativeKey) => string, +): DesktopNativeBundle { + return { + locale, + messages: Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key) => [key, translate(key)])) as DesktopNativeMessages, + } +} + +export function parseDesktopNativeBundle(value: unknown): DesktopNativeBundle | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) return undefined + try { + if (new TextEncoder().encode(JSON.stringify(value)).byteLength > DESKTOP_NATIVE_MAX_PAYLOAD_BYTES) return undefined + } catch { + return undefined + } + const bundle = value as { locale?: unknown; messages?: unknown } + if (!DESKTOP_NATIVE_LOCALES.some((locale) => locale === bundle.locale)) return undefined + if (!bundle.messages || typeof bundle.messages !== "object" || Array.isArray(bundle.messages)) return undefined + const messages = bundle.messages as Record + const keys = Object.keys(messages) + if (keys.length !== DESKTOP_NATIVE_KEYS.length) return undefined + if (!DESKTOP_NATIVE_KEYS.every((key) => typeof messages[key] === "string")) return undefined + if (!keys.every((key) => key in DESKTOP_NATIVE_ENGLISH)) return undefined + return bundle as DesktopNativeBundle +} + +export function formatDesktopNativeMessage(message: string, params?: Record) { + if (!params) return message + return message.replace(/\{\{([^{}]+)\}\}/g, (match, key: string) => { + const value = params[key] + return value === undefined ? match : String(value) + }) +} diff --git a/packages/app/src/i18n/dv.ts b/packages/app/src/i18n/dv.ts new file mode 100644 index 0000000000000000000000000000000000000000..27b5fea869993d4bea44d487f8518ac9e5b8d209 --- /dev/null +++ b/packages/app/src/i18n/dv.ts @@ -0,0 +1,1177 @@ +export const dict = { + "desktop.menu.app": "OpenCode އެވެ", + "desktop.menu.file": "ފައިލް", + "desktop.menu.edit": "ބަދަލު ގެނައުން", + "desktop.menu.view": "މަންޒަރު", + "desktop.menu.go": "ދޭ", + "desktop.menu.window": "ކުޑަދޮރު", + "desktop.menu.help": "އެހީވުން", + "desktop.menu.checkForUpdates": "އަޕްޑޭޓްސް އަށް ޗެކް ކޮށްލައްވާ...", + "desktop.menu.settings": "ސެޓިންގސް", + "desktop.menu.reloadWebview": "ވެބްވިއު ރީލޯޑް ކުރާށެވެ", + "desktop.menu.restart": "އަލުން ފަށާށެވެ", + "desktop.menu.exportLogs": "އެކްސްޕޯޓް ލޮގްސް...", + "desktop.menu.newSession": "އައު ސެޝަން", + "desktop.menu.openProject": "އޯޕަން ޕްރޮޖެކްޓް...", + "desktop.menu.newWindow": "ނިއު ވިންޑޯ", + "desktop.menu.closeWindow": "ވިންޑޯ ބަންދުކުރުން", + "desktop.menu.undo": "އަންޑޯ", + "desktop.menu.redo": "ރީޑޯ", + "desktop.menu.cut": "ކެފުން", + "desktop.menu.copy": "ކޮޕީ", + "desktop.menu.paste": "ޕޭސްޓް ކޮށްލާށެވެ", + "desktop.menu.delete": "ފުހެލުން", + "desktop.menu.selectAll": "ހުރިހާ އެއްޗެއް ހޮވާށެވެ", + "desktop.menu.toggleSidebar": "ޓޮގްލް ސައިޑްބާރ", + "desktop.menu.toggleTerminal": "ޓާމިނަލް ޓޮގްލް ކުރުން", + "desktop.menu.toggleFileTree": "ޓޮގްލް ފައިލް ޓްރީ", + "desktop.menu.reload": "ރީލޯޑް", + "desktop.menu.toggleDeveloperTools": "ޑިވެލޮޕަރ ޓޫލްސް ޓޮގްލް ކުރުން", + "desktop.menu.actualSize": "އަސްލު ސައިޒެވެ", + "desktop.menu.zoomIn": "ޒޫމް އިން", + "desktop.menu.zoomOut": "ޒޫމް އައުޓް", + "desktop.menu.toggleFullScreen": "ފުލް ސްކްރީން ޓޮގްލް ކުރާށެވެ", + "desktop.menu.back": "ފަހަތް", + "desktop.menu.forward": "ކުރިޔަށް", + "desktop.menu.previousSession": "ކުރީ ސެޝަން", + "desktop.menu.nextSession": "ދެން އޮންނަ ސެޝަން", + "desktop.menu.previousProject": "ކުރީގެ މަޝްރޫޢު", + "desktop.menu.nextProject": "ދެން އޮތް މަޝްރޫޢު", + "desktop.menu.minimize": "އެންމެ ކުޑަކުރުން", + "desktop.menu.maximize": "މެކްސިމަޒް ކުރުން", + "desktop.menu.documentation": "OpenCode ޑޮކިއުމަންޓޭޝަން", + "desktop.menu.supportForum": "ސަޕޯޓް ފޯރަމް", + "desktop.menu.shareFeedback": "ޝެއާ ފީޑްބެކް", + "desktop.menu.reportBug": "ބަގެއް ރިޕޯޓްކުރުން", + "desktop.menu.ariaLabel": "OpenCode މެނޫ", + "desktop.updater.dialog.checkFailed.message": "އަޕްޑޭޓް ޗެކް ފެއިލްވެއްޖެއެވެ.", + "desktop.updater.dialog.checkFailed.title": "އަޕްޑޭޓް އެރަރ", + "desktop.updater.dialog.upToDate.message": "ޔޫ އާ އަޕް ޓު ޑޭޓް.", + "desktop.updater.dialog.upToDate.title": "ނޯ އަޕްޑޭޓްސް", + "desktop.updater.dialog.ready.message": "އަޕްޑޭޓް {{version}} ޑައުންލޯޑް ކޮށްފިއެވެ. މިހާރު ރީސްޓާޓް ކުރަންތަ؟", + "desktop.updater.dialog.ready.title": "އަޕްޑޭޓް ރެޑީ", + "desktop.updater.dialog.restart": "އަލުން ފަށާށެވެ", + "desktop.updater.dialog.later": "ފަހުން", + "desktop.recovery.action.relaunch": "ރީލޯންޗް ކުރުން", + "desktop.recovery.action.exportLogs": "އެކްސްޕޯޓް ލޮގްސް", + "desktop.recovery.action.keepWaiting": "އިންތިޒާރު ކުރަމުން ގެންދާށެވެ", + "desktop.recovery.action.quit": "ދޫކޮށްލުން", + "desktop.recovery.loadFailed": "OpenCode ލޯޑް ނުކުރެވިއްޖެއެވެ", + "desktop.recovery.terminated": "OpenCode ވިންޑޯ ހީވެސް ނުކުރާ ގޮތަކަށް ނިމުނީއެވެ", + "desktop.recovery.unresponsive": "OpenCode އިން ޖަވާބެއް ނުދެއެވެ", + "desktop.recovery.unresponsive.detail": + "އެޕް އަލުން ލޯންޗްކޮށް، ލޮގްތައް ހުޅުވައި، ނުވަތަ މަޑުކޮށްގެން ތިބެވޭނެ އެވެ.", + "desktop.recovery.loadFailed.detail": "ވިންޑޯ: {{window}}\nURL: {{url}}\nއެރަރ: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "ވިންޑޯ: {{window}}\nސަބަބު: {{reason}}\nކޯޑް: {{code}}", + "desktop.recovery.unknown": "<ނޭނގޭ>", + "desktop.dialog.chooseFolder": "ފޯލްޑަރެއް ހޮވާށެވެ", + "desktop.dialog.chooseFile": "ފައިލެއް ހޮވާށެވެ", + "desktop.dialog.saveFile": "ފައިލް ސޭވްކުރުން", + "desktop.dialog.files": "ފައިލްތައް", + "desktop.server.local": "ލޯކަލް ސަރވަރ އެވެ", + "desktop.wsl.error.windowsOnly": "WSL ލިބެން ހުރީ ހަމައެކަނި Windows އިންނެވެ", + "desktop.wsl.error.unavailable": "WSL އެއް ނުލިބެއެވެ", + "desktop.wsl.error.listInstalled": "އިންސްޓޯލްކޮށްފައިވާ WSL ޑިސްޓްރޯތައް ލިސްޓް ނުކުރެވިއްޖެއެވެ", + "desktop.wsl.error.listOnline": "އޮންލައިންކޮށް WSL ޑިސްޓްރޯސް ލިސްޓް ނުކުރެވިއްޖެއެވެ", + "desktop.wsl.error.executeDistro": "ޑިސްޓްރޯގައި ކޮމާންޑްތައް އެގްޒެކެޓް ނުކުރެވޭނެއެވެ", + "desktop.wsl.error.installWsl": "WSL އިންސްޓޯލް ކުރުން ނާކާމިޔާބުވެއްޖެ", + "desktop.wsl.error.installDistro": "ޑިސްޓްރޯ: {{distro}} އިންސްޓޯލް ކުރަން ނާކާމިޔާބު", + "desktop.wsl.error.installOpencode": "OpenCode އިންސްޓޯލް ކުރުން ނާކާމިޔާބުވެއްޖެ", + "desktop.wsl.error.alreadyAdded": "{{distro}} މިހާރުވެސް އިތުރުކޮށްފައިވެއެވެ", + "desktop.wsl.error.opencodeMissing": "މި ޑިސްޓްރޯގައި opencode އިންސްޓޯލްކޮށްފައެއް ނުވެއެވެ", + "desktop.wsl.error.opencodeCannotRun": "opencode އިންސްޓޯލް ކޮށްފައި އޮތް ނަމަވެސް ހިންގޭ ގޮތެއް ނުވިއެވެ", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode އަކީ {{distro}} ގައި އިންސްޓޯލް ކުރެވިފައިވާ އެއްޗެއް ނޫނެވެ", + "desktop.wsl.error.updateVersion": + "OpenCode އަޕްޑޭޓް ނިމުނު ނަމަވެސް {{distro}} އަދިވެސް ރިޕޯޓް ކުރަނީ {{installed}}؛ ލަފާކުރެވޭ ގޮތުގައި {{expected}}", + "desktop.wsl.error.noVersion": "އެއްވެސް ވަރޝަނެއް ނެތެވެ", + "desktop.wsl.error.serverExited": + "ސްޓާޓްއަޕް ކުރުމަށްފަހު WSL ސަރވަރ އިން ނުކުމެއްޖެ (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL ސަރވަރ އިން ނުކުތީ ދުޅަހެޔޮ ސިއްހަތެއްގައި ހުރުމުގެ ކުރިން (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "{{distro}} ހެލްތު ޗެކް ކުރުމަށް ސައިޑްކާރ ޓައިމް އައުޓް ކޮށްފައިވަނީ {{timeout}}ms އަށް ފަހުގައެވެ", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} ޓައިމް އައުޓް ކޮށްފައިވަނީ {{timeout}}ms އަށް ފަހުގައެވެ", + "desktop.wsl.error.failedPort": "ބަނދަރު ހޯދުމަށް ނާކާމިޔާބުވިއެވެ", + "desktop.picker.error.notSelected": "ފައިލް ހޮވާފައިވަނީ ޕިކަރ އިން ނޫނެވެ", + "desktop.picker.error.sizeLimit": "ހޮވާލެވޭ އެޓޭޗްމަންޓްތައް {{limit}} MB ލިމިޓަށްވުރެ މަތިވެއެވެ", + "command.category.suggested": "ލަފާދީފިއެވެ", + "command.category.view": "މަންޒަރު", + "command.category.project": "ޕްރޮޖެކްޓް", + "command.category.provider": "ޕްރޮވައިޑަރެވެ", + "command.category.server": "ސަރވަރ އެވެ", + "command.category.session": "ސެޝަން", + "command.category.theme": "ތީމް", + "command.category.language": "ބަސް", + "command.category.file": "ފައިލް", + "command.category.context": "ކޮންޓެކްސްޓް", + "command.category.terminal": "ޓާމިނަލް އެވެ", + "command.category.model": "މޮޑެލް", + "command.category.mcp": "MCP އެވެ", + "command.category.agent": "އޭޖެންޓު", + "command.category.permissions": "ހުއްދަތައް", + "command.category.workspace": "ވޯކްސްޕޭސް", + "command.category.settings": "ސެޓިންގސް", + "command.logs.export": "އެކްސްޕޯޓް ލޮގްސް", + "theme.scheme.system": "ސިސްޓަމް", + "theme.scheme.light": "އަލި", + "theme.scheme.dark": "އަނދިރި", + "command.sidebar.toggle": "ސައިޑްބާ ޓޮގްލް ކުރާށެވެ", + "command.project.open": "އޯޕަން ޕްރޮޖެކްޓް", + "command.project.previous": "ކުރީގެ މަޝްރޫޢު", + "command.project.next": "ދެން އޮތް މަޝްރޫޢެވެ", + "command.project.index": "ޕްރޮޖެކްޓް {{index}} އަށް ބަދަލުވާށެވެ", + "command.provider.connect": "ޕްރޮވައިޑަރ ގުޅުވާށެވެ", + "command.server.switch": "ސަރވަރ ބަދަލުކުރުން", + "command.settings.open": "ސެޓިންގސް ހުޅުވާލާށެވެ", + "command.session.previous": "ކުރީ ސެޝަން", + "command.session.next": "ދެން އޮންނަ ސެޝަން", + "command.session.previous.unseen": "ކުރީގެ ނުކިޔާ ސެޝަން", + "command.session.next.unseen": "ދެން ނުކިޔާ ސެޝަން", + "command.session.archive": "އަރުޝީފް ސެޝަން", + "command.palette": "ކޮމާންޑް ޕެލެޓް", + "command.theme.cycle": "ސައިކަލް ތީމް", + "command.theme.set": "ތީމް ބޭނުންކުރުން: {{theme}}", + "command.theme.scheme.cycle": "ސައިކަލް ކުލަ ސްކީމް", + "command.theme.scheme.set": "ކުލަ ސްކީމް ބޭނުންކުރުން: {{scheme}}", + "command.language.cycle": "ސައިކަލް ބަސް", + "command.language.set": "ބަސް ބޭނުންކުރުން: {{language}}", + "command.session.new": "އާ ޖަލްސާއެއް", + "command.file.open": "ފައިލް ހުޅުވާލާށެވެ", + "command.tab.close": "ޓެބް ބަންދުކުރުން", + "command.tab.reopenClosed": "ބަންދުކޮށްފައިވާ ޓެބް އަލުން ހުޅުވާލާށެވެ", + "command.context.addSelection": "ކޮންޓެކްސްޓަށް ސެލެކްޝަން އިތުރުކުރުން", + "command.context.addSelection.description": "މިހާރު ހުރި ފައިލް އިން ހޮވާފައިވާ ލައިންތައް އިތުރުކުރުން", + "command.input.focus": "ފޯކަސް އިންޕުޓް", + "command.terminal.toggle": "ޓޮގްލް ޓާމިނަލް", + "command.fileTree.toggle": "ޓޮގްލް ފައިލް ޓްރީ", + "command.review.toggle": "ރިވިއު ޓޮގްލް ކުރާށެވެ", + "command.terminal.new": "އައު ޓާމިނަލް", + "command.terminal.new.description": "އާ ޓާރމިނަލް ޓެބެއް އުފެއްދުން", + "command.steps.toggle": "ޓޮގްލް ސްޓެޕްސް", + "command.steps.toggle.description": "މިހާރު ބޭނުންކުރާ މެސެޖަށް ފިޔަވަޅުތައް ދައްކާ ނުވަތަ ފޮރުވުން", + "command.message.previous": "ކުރީގެ މެސެޖެވެ", + "command.message.previous.description": "ކުރީގެ ޔޫޒަރ މެސެޖަށް ދާށެވެ", + "command.message.next": "ދެން އޮތް މެސެޖެވެ", + "command.message.next.description": "ދެން އަންނަ ޔޫޒަރ މެސެޖަށް ދާށެވެ", + "command.model.choose": "މޮޑެލް ހޮވާށެވެ", + "command.model.choose.description": "ތަފާތު މޮޑެލްއެއް ހޮވާށެވެ", + "command.mcp.toggle": "MCPs ޓޮގްލް ކުރާށެވެ", + "command.mcp.toggle.description": "MCPs ޓޮގްލް ކުރާށެވެ", + "command.agent.cycle": "ސައިކަލް އޭޖެންޓެވެ", + "command.agent.cycle.description": "ދެން ހުރި އޭޖެންޓަކަށް ބަދަލުވާށެވެ", + "command.agent.cycle.reverse": "ސައިކަލް އޭޖެންޓް ފަހަތަށް", + "command.agent.cycle.reverse.description": "ކުރީގެ އޭޖެންޓަށް ބަދަލުވާށެވެ", + "command.model.variant.cycle": "ސައިކަލް ވިސްނުމުގެ މަސައްކަތެވެ", + "command.model.variant.cycle.description": "ދެން އޮންނަ މަސައްކަތުގެ ފެންވަރަށް ބަދަލުވާށެވެ", + "command.prompt.mode.shell": "ޝެލް", + "command.prompt.mode.normal": "ޕްރޮމްޕްޓް", + "command.permissions.autoaccept.enable": "އޮޓޯ އެކްސެޕްޓް ހުއްދަތައް", + "command.permissions.autoaccept.disable": "ހުއްދަތައް އޮޓޯ އެކްސެޕްޓްކުރުން ހުއްޓާލާށެވެ", + "command.workspace.toggle": "ވޯކްސްޕޭސްތައް ޓޮގްލްކުރުން", + "command.workspace.toggle.description": "ސައިޑްބާރ ގައި ގިނަ ވޯކްސްޕޭސްތައް އެނެބަލް ނުވަތަ ޑިސެބަލް ކުރުން", + "command.session.undo": "އަންޑޯ", + "command.session.undo.description": "އެންމެ ފަހު މެސެޖު އަންޑޯ ކުރާށެވެ", + "command.session.redo": "ރީޑޯ", + "command.session.redo.description": "އެންމެ ފަހުން އަންޑޯން މެސެޖު އަލުން ކުރާށެވެ", + "command.session.compact": "ކޮމްޕެކްޓް ސެޝަން", + "command.session.compact.description": "ކޮންޓެކްސްޓް ސައިޒު ކުޑަކުރުމަށްޓަކައި ސެޝަން ޚުލާޞާކުރުން", + "command.session.fork": "މެސެޖުން ފޯކް", + "command.session.fork.description": "ކުރީގެ މެސެޖަކުން އާ ސެޝަނެއް އުފެއްދުން", + "command.session.share": "ޝެއާ ސެޝަން", + "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": "އެއްވެސް ނަތީޖާއެއް ނުފެނެއެވެ", + "palette.group.commands": "އަމުރުތަކެވެ", + "palette.group.files": "ފައިލްތައް", + "dialog.provider.search.placeholder": "ޕްރޮވައިޑަރުން ހޯދުން", + "dialog.provider.empty": "އެއްވެސް ޕްރޮވައިޑަރެއް ނުފެނެއެވެ", + "dialog.provider.group.popular": "މަޝްހޫރު", + "dialog.provider.group.other": "އެހެން", + "dialog.provider.custom.label": "ކަސްޓަމް OpenAI އާއި އެއްގޮތްވާ ޕްރޮވައިޑަރެއް", + "dialog.provider.tag.recommended": "ލަފާދެވިފައިވުން", + "dialog.provider.opencode.note": + "ކިއުރެޓް ކުރެވިފައިވާ މޮޑެލްތަކުގެ ތެރޭގައި Claude، GPT، Gemini އަދި އެހެނިހެން މޮޑެލްތައް ހިމެނެއެވެ", + "dialog.provider.opencode.tagline": "އިތުބާރު ހުރި އޮޕްޓިމައިޒްޑް މޮޑެލްތަކެވެ", + "dialog.provider.opencodeGo.tagline": "އެންމެންނަށް ކުޑަ އަގެއްގައި ސަބްސްކްރިޕްޝަން ދިނުމެވެ", + "dialog.provider.anthropic.note": "ޕްރޯ އަދި މެކްސް ހިމެނޭ ގޮތަށް Claude މޮޑެލްތަކަށް ސީދާ އެކްސެސް ލިބޭނެ އެވެ", + "dialog.provider.copilot.note": "GitHub Copilot މެދުވެރިކޮށް ކޯޑިންގ އެހީތެރިވުމަށް AI މޮޑެލްތައް", + "dialog.provider.openai.note": "އަވަސް، ގާބިލް އާންމު AI ޓާސްކްތަކަށް GPT މޮޑެލްތަކެވެ", + "dialog.provider.google.note": "އަވަސް، ސްޓްރަކްޗަރޑް ރެސްޕޮންސްތަކަށް Gemini މޮޑެލްތަކެވެ", + "dialog.provider.openrouter.note": "އެއް ޕްރޮވައިޑަރަކުން ސަޕޯޓްކުރާ ހުރިހާ މޮޑެލްތަކަށް އެކްސެސްވުން", + "dialog.provider.vercel.note": "ސްމާޓް ރޫޓިންގ އާއި އެކު AI މޮޑެލްތަކަށް ޔުނިފައިޑް އެކްސެސް", + "dialog.model.select.title": "މޮޑެލް ހޮވުން", + "dialog.model.search.placeholder": "ސަރޗް މޮޑެލްސް", + "dialog.model.empty": "މޮޑެލް ނަތީޖާއެއް ނެތެވެ", + "dialog.model.manage": "މޮޑެލްތައް މެނޭޖްކުރުން", + "dialog.model.manage.description": "މޮޑެލް ސެލެކްޓަރގައި ފެންނަނީ ކޮން މޮޑެލްތަކެއްކަން ކަސްޓަމައިޒް ކުރުން.", + "dialog.model.manage.provider.toggle": "ހުރިހާ {{provider}} މޮޑެލްތަކެއް ޓޮގްލް ކުރާށެވެ", + "dialog.model.unpaid.freeModels.title": "ހިލޭ މޮޑެލްތައް ފޯރުކޮށްދެނީ OpenCode އިންނެވެ", + "dialog.model.unpaid.addMore.title": "މަޝްހޫރު ޕްރޮވައިޑަރުންގެ އިތުރު މޮޑެލްތައް އިތުރުކުރުން", + "dialog.model.unpaid.viewMoreProviders": "އިތުރު 70+ ޕްރޮވައިޑަރުން ބައްލަވާށެވެ", + "dialog.provider.viewAll": "އިތުރު ޕްރޮވައިޑަރުން ދައްކާށެވެ", + "provider.connect.title": "{{provider}} ގުޅުވާށެވެ", + "provider.connect.title.anthropicProMax": "Claude ޕްރޯ/މެކްސް އިން ލޮގިން ވާނެއެވެ", + "provider.connect.selectMethod": "{{provider}} އަށް ލޮގިން މެތޯޑް ހޮވާށެވެ.", + "provider.connect.method.apiKey": "API ކީ އެވެ", + "provider.connect.method.browser": "ބްރައުޒާ އެވެ", + "provider.connect.method.headless": "އިންޓަފޭސް ނެތި", + "provider.connect.status.inProgress": "ހުއްދަ ދިނުމުގެ މަސައްކަތް ކުރިއަށްދާ...", + "provider.connect.status.waiting": "ހުއްދައަށް އިންތިޒާރު ކުރަމުން...", + "provider.connect.status.failed": "ހުއްދަ ދިނުން ނާކާމިޔާބު: {{error}}", + "provider.connect.apiKey.description": + "ތިބާގެ އެކައުންޓް ގުޅުވައި، OpenCode ގައި {{provider}} މޮޑެލްތައް ބޭނުންކުރުމަށް {{provider}} API ކީ ލިޔުއްވާ.", + "provider.connect.apiKey.label": "{{provider}} API ކީ އެވެ", + "provider.connect.apiKey.placeholder": "API ކީ އެވެ", + "provider.connect.apiKey.required": "API ކީ ބޭނުންވެއެވެ", + "provider.connect.opencodeZen.line1": + "OpenCode Zen އިން ކޯޑިންގ އޭޖެންޓުންނަށް އިތުބާރު ހުރި އޮޕްޓިމައިޒްޑް މޮޑެލްތަކުގެ ކިއުރެޓެޑް ސެޓަކަށް އެކްސެސް ލިބިގެންދެއެވެ.", + "provider.connect.opencodeZen.line2": + "އެއް API ކީ އާއެކު Claude، GPT، Gemini، GLM އަދި އެހެނިހެން މޮޑެލްތަކަށް އެކްސެސް ލިބިގެންދާނެއެވެ.", + "provider.connect.opencodeZen.visit.prefix": "ޒިޔާރަތްކުރުން ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen އެވެ", + "provider.connect.opencodeZen.visit.suffix": " ތިބާގެ API ކީ އެއްކުރުމަށްޓަކައެވެ.", + "provider.connect.oauth.code.visit.prefix": "ޒިޔާރަތްކުރުން ", + "provider.connect.oauth.code.visit.link": "މި ލިންކެވެ", + "provider.connect.oauth.code.visit.suffix": + " އެކައުންޓް ގުޅުވައިދިނުމަށާއި OpenCode ގައި {{provider}} މޮޑެލްތައް ބޭނުންކުރުމަށް އޮތޯރައިޒޭޝަން ކޯޑް އެއްކުރުމަށްޓަކައެވެ.", + "provider.connect.oauth.code.label": "{{method}} ހުއްދަ ކޯޑް", + "provider.connect.oauth.code.placeholder": "ހުއްދަ ކޯޑް", + "provider.connect.oauth.code.required": "އޮތޯރިޒޭޝަން ކޯޑް ބޭނުންވެއެވެ", + "provider.connect.oauth.code.invalid": "ސައްހަ ނޫން އޮތޯރިޒޭޝަން ކޯޑެކެވެ", + "provider.connect.oauth.auto.visit.prefix": "ޒިޔާރަތްކުރުން ", + "provider.connect.oauth.auto.visit.link": "މި ލިންކެވެ", + "provider.connect.oauth.auto.visit.suffix": + " އަދި ތިރީގައިވާ ކޯޑް ޖައްސަވައިގެން އެކައުންޓް ގުޅުވައި، OpenCode ގައި {{provider}} މޮޑެލްތައް ބޭނުން ކުރައްވާށެވެ.", + "provider.connect.oauth.auto.confirmationCode": "ކޮންފަރމަންސް ކޯޑް", + "provider.connect.toast.connected.title": "{{provider}} ގުޅިފައިވެއެވެ", + "provider.connect.toast.connected.description": "{{provider}} މޮޑެލްތައް މިހާރު ބޭނުންކުރެވޭނެ އެވެ.", + "provider.custom.title": "ކަސްޓަމް ޕްރޮވައިޑަރެވެ", + "provider.custom.unavailable": "މި ސަރވަރގައި ކަސްޓަމް ޕްރޮވައިޑަރުން ނުލިބެއެވެ", + "provider.custom.description.prefix": "OpenAI އާއި އެއްގޮތްވާ ޕްރޮވައިޑަރެއް ކޮންފިގްރޭޓްކުރުން. ބަލާށެވެ ", + "provider.custom.description.link": "ޕްރޮވައިޑަރ ކޮންފިގް ޑޮކްސް", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ޕްރޮވައިޑަރ ID", + "provider.custom.field.providerID.placeholder": "myprovider އެވެ", + "provider.custom.field.providerID.description": "ކުދި އަކުރުތަކާއި، ނަންބަރުތަކާއި، ހައިފެން ނުވަތަ އަންޑަރސްކޯރސް", + "provider.custom.field.name.label": "ނަން ދައްކާށެވެ", + "provider.custom.field.name.placeholder": "އަހަރެންގެ AI ޕްރޮވައިޑަރ", + "provider.custom.field.baseURL.label": "ބޭސް URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1 އެވެ", + "provider.custom.field.apiKey.label": "API ކީ އެވެ", + "provider.custom.field.apiKey.placeholder": "API ކީ އެވެ", + "provider.custom.field.apiKey.description": + "އިޚްތިޔާރީ. ހެޑަރސް މެދުވެރިކޮށް އޮތު މެނޭޖް ކުރާނަމަ ހުސްކޮށް ދޫކޮށްލާށެވެ.", + "provider.custom.models.label": "މޮޑެލްތަކެވެ", + "provider.custom.models.id.label": "ID އެވެ", + "provider.custom.models.id.placeholder": "model-id އެވެ", + "provider.custom.models.name.label": "ނަން", + "provider.custom.models.name.placeholder": "ޑިސްޕްލޭ ނަން", + "provider.custom.models.remove": "މޮޑެލް ނައްތާލާށެވެ", + "provider.custom.models.add": "މޮޑެލް އިތުރުކުރުން", + "provider.custom.headers.label": "ހެޑަރސް (އިޚްތިޔާރީ)", + "provider.custom.headers.key.label": "ހެޑަރ", + "provider.custom.headers.key.placeholder": "Header-Name އެވެ", + "provider.custom.headers.value.label": "އަގު", + "provider.custom.headers.value.placeholder": "އަގު", + "provider.custom.headers.remove": "ހެޑަރ ނައްތާލާށެވެ", + "provider.custom.headers.add": "ހެޑަރ އިތުރުކުރުން", + "provider.custom.error.providerID.required": "ޕްރޮވައިޑަރ ID ބޭނުންވެއެވެ", + "provider.custom.error.providerID.format": + "ކުދި އަކުރުތަކާއި، ނަންބަރުތަކާއި، ހައިފެންސް ނުވަތަ އަންޑަރސްކޯރ ބޭނުންކުރުން", + "provider.custom.error.providerID.exists": "އެ ޕްރޮވައިޑަރ ID މިހާރުވެސް އެބަހުއްޓެވެ", + "provider.custom.error.name.required": "ޑިސްޕްލޭ ނަން ބޭނުންވެއެވެ", + "provider.custom.error.baseURL.required": "ބޭސް URL ބޭނުންވެއެވެ", + "provider.custom.error.baseURL.format": "ފަށަން ޖެހޭނީ http:// ނުވަތަ https:// އިންނެވެ.", + "provider.custom.error.required": "ބޭނުންވާ", + "provider.custom.error.duplicate": "ޑަބިލިކޭޓް ކުރުން", + "provider.disconnect.toast.disconnected.title": "{{provider}} ކަނެކްޓް ވެއްޖެއެވެ", + "provider.disconnect.toast.disconnected.description": "{{provider}} މޮޑެލްތައް މިހާރު ލިބެން ނެތެވެ.", + "model.tag.free": "ހިލޭ", + "model.tag.latest": "އެންމެފަހުގެ", + "model.provider.anthropic": "Anthropic އެވެ", + "model.provider.openai": "OpenAI އެވެ", + "model.provider.google": "Google އެވެ", + "model.provider.xai": "xAI އެވެ", + "model.provider.meta": "Meta އެވެ", + "model.input.text": "ލިޔުންކޮޅު", + "model.input.image": "ފޮޓޯ", + "model.input.audio": "އޯޑިއޯ އެވެ", + "model.input.video": "ވިޑިއޯ", + "model.input.pdf": "ޕީޑީއެފް", + "model.tooltip.allows": "ހުއްދަ ދެނީ: {{inputs}}", + "model.tooltip.context.label": "ކޮންޓެކްސްޓް", + "model.tooltip.inputs": "އިންޕުޓްސް", + "model.tooltip.model": "މޮޑެލް", + "model.tooltip.provider": "ޕްރޮވައިޑަރެވެ", + "model.tooltip.reasoning": "ރިޒަނިންގ އެވެ", + "model.tooltip.reasoning.allowed": "ރިޒަނިންގ އަށް ހުއްދަދެއެވެ", + "model.tooltip.reasoning.none": "އެއްވެސް ވިސްނުމެއް ނެތެވެ", + "model.tooltip.context": "ކޮންޓެކްސްޓް ލިމިޓް {{limit}}", + "common.search.placeholder": "ހޯދުން", + "common.goBack": "ފަހަތަށް ދަތުރުކޮށްލާށެވެ", + "common.goForward": "ކުރިއަށް ދަތުރުކޮށްލާށެވެ", + "common.loading": "ލޯޑިންގ", + "common.loading.ellipsis": "...", + "common.cancel": "ކެންސަލް", + "common.clear": "ސާފުކޮށް", + "common.open": "ހުޅުވުން", + "common.connect": "ގުޅުވުން", + "common.disconnect": "ގުޅުން ކަނޑާލާށެވެ", + "common.continue": "ކުރިއަށްގެންދިޔުން", + "common.submit": "ހުށަހެޅުން", + "common.save": "ރައްކާކުރުން", + "common.saving": "ސޭވިންގ...", + "common.default": "ޑީފޯލްޓް", + "common.attachment": "އެކުލެވުން", + "prompt.placeholder.shell": "ޝެލް ކޮމާންޑް ލިޔުއްވާ... {{example}}", + "prompt.placeholder.normal": 'ކޮންމެ އެއްޗަކާ އަހާލަން... "{{example}}".', + "prompt.placeholder.simple": "ކޮންމެ އެއްޗެއް އަހާލަން...", + "prompt.placeholder.summarizeComments": "ކޮމެންޓްތައް ޚުލާޞާކޮށްލައްވާ...", + "prompt.placeholder.summarizeComment": "ކޮމެންޓް ޚުލާޞާކޮށްލައްވާ...", + "prompt.mode.shell": "ޝެލް", + "prompt.mode.normal": "ޕްރޮމްޕްޓް", + "prompt.mode.shell.exit": "esc އިން ނުކުތުމަށްޓަކައެވެ", + "session.child.promptDisabled": "ސަބްއޭޖެންޓް ސެޝަންތަކަށް ޕްރޮމްޕްޓް ނުކުރެވޭނެއެވެ.", + "session.child.backToParent": "އަނބުރާ މައި ސެޝަނަށް.", + "prompt.example.1": "ކޯޑްބޭސްގައި ޓޯޑޯއެއް ރަނގަޅުކުރުން", + "prompt.example.2": "މި މަޝްރޫއުގެ ޓެކް ސްޓޭކަކީ ކޮބައި ހެއްޔެވެ؟", + "prompt.example.3": "ފަޅައިގެން ގޮސްފައިވާ ޓެސްޓްތައް ރަނގަޅުކުރުން", + "prompt.example.4": "އޮތެންޓިކޭޝަން މަސައްކަތްކުރާ ގޮތް ބަޔާންކުރުން", + "prompt.example.5": "ސެކިއުރިޓީ ވަލްނަރެބިލިޓީސް ހޯދައި ރަނގަޅުކުރުން", + "prompt.example.6": "ޔޫޒަރ ސާވިސްއަށް ޔުނިޓް ޓެސްޓްތައް އިތުރުކުރުން", + "prompt.example.7": "މި ފަންކްޝަން ކިޔަން ފަސޭހަ ކުރުމަށްޓަކައި ރީފެކްޓަރ ކުރާށެވެ", + "prompt.example.8": "މި ގޯހުގެ ​​މާނައަކީ ކޮބައި ހެއްޔެވެ؟", + "prompt.example.9": "މި މައްސަލަ ޑިބަގް ކުރުމަށް އެހީތެރިވެދެއްވާށެވެ", + "prompt.example.10": "API ޑޮކިއުމަންޓޭޝަން ޖެނެރޭޓްކުރުން", + "prompt.example.11": "ޑޭޓާބޭސް ކިއުއަރީތައް އޮޕްޓިމައިޒް ކުރުން", + "prompt.example.12": "އިންޕުޓް ވެލިޑޭޝަން އިތުރުކުރުން", + "prompt.example.13": "އަށް އާ ކޮމްޕޮނެންޓެއް އުފެއްދުން...", + "prompt.example.14": "މި ޕްރޮޖެކްޓް ޑިޕްލޮއި ކުރާނީ ކިހިނެއް ހެއްޔެވެ؟", + "prompt.example.15": "އެންމެ ރަނގަޅު އުސޫލުތަކަށް އަހަރެންގެ ކޯޑް ރިވިއުކުރުން", + "prompt.example.16": "މި ފަންކްޝަނަށް އެރަރ ހެންޑްލިންގ އިތުރުކުރުން", + "prompt.example.17": "މި ރެޖެކްސް ޕެޓަރން ކިޔައިދީބަލަ", + "prompt.example.18": "މިކަން TypeScript އަށް ބަދަލުކުރުން", + "prompt.example.19": "މުޅި ކޯޑްބޭސްގައި ލޮގިން އިތުރުކުރުން", + "prompt.example.20": "މުއްދަތު ހަމަނުވާ ޑިޕެންޑެންސީތަކަކީ ކޮބައި ހެއްޔެވެ؟", + "prompt.example.21": "މައިގްރޭޝަން ސްކްރިޕްޓެއް ލިޔުމުގައި އެހީތެރިވެދެއްވާށެވެ", + "prompt.example.22": "މި އެންޑްޕޮއިންޓަށް ކެޝިންގ ތަންފީޒުކުރުން", + "prompt.example.23": "މި ލިސްޓަށް ޕޭޖިނޭޝަން އިތުރުކުރުން", + "prompt.example.24": "އަށް CLI ކޮމާންޑެއް އުފައްދާށެވެ.", + "prompt.example.25": "މިތަނުގައި ތިމާވެށީގެ ބަދަލުތައް މަސައްކަތް ކުރަނީ ކިހިނެއް ހެއްޔެވެ؟", + "prompt.popover.emptyResults": "އެއްގޮތް ނަތީޖާއެއް ނެތެވެ", + "prompt.popover.emptyCommands": "އެއްވެސް މެޗިން ކޮމާންޑެއް ނެތެވެ", + "prompt.dropzone.label": "މިތަނަށް ތަސްވީރު، PDFs، ނުވަތަ ޓެކްސްޓް ފައިލްތައް ވައްޓާލާށެވެ", + "prompt.dropzone.file.label": "@mention ފައިލަށް ވައްޓާލާށެވެ", + "prompt.slash.badge.custom": "ސަޤާފަތް", + "prompt.slash.badge.skill": "ހުނަރު", + "prompt.slash.badge.mcp": "އެމްސީޕީ އެވެ", + "prompt.context.active": "އެކްޓިވް", + "prompt.context.includeActiveFile": "އެކްޓިވް ފައިލް ހިމަނާށެވެ", + "prompt.context.removeActiveFile": "ކޮންޓެކްސްޓުން އެކްޓިވް ފައިލް ނައްތާލުން", + "prompt.context.removeFile": "ކޮންޓެކްސްޓުން ފައިލް ނައްތާލުން", + "prompt.action.attachFile": "ފައިލްތައް އިތުރުކުރުން", + "prompt.menu.addImagesAndFiles": "ފައިލްތަކާއި އަދިވެސް އެތައް ކަމެއް އިތުރުކުރުން", + "prompt.menu.imagesAndFiles": "ތަސްވީރުތަކާއި ފައިލްތައް", + "prompt.menu.commands": "އަމުރުތަކެވެ", + "prompt.menu.context": "ކޮންޓެކްސްޓް", + "prompt.menu.shellCommand": "ޝެލް ކޮމާންޑެވެ", + "prompt.attachment.remove": "އެޓޭޗްމަންޓް ނަގާށެވެ", + "prompt.action.send": "ފޮނުވުން", + "prompt.action.stop": "ހުއްޓުން", + "prompt.toast.pasteUnsupported.title": "ސަޕޯޓް ނުކުރާ އެޓޭޗްމަންޓެވެ", + "prompt.toast.pasteUnsupported.description": + "މިތަނުގައި އެޓޭޗް ކުރެވޭނީ ހަމައެކަނި ތަސްވީރު، PDFs، ނުވަތަ ޓެކްސްޓް ފައިލްތަކެވެ.", + "prompt.toast.attachmentDuplicate.title": "މި ފައިލް މިހާރު ވަނީ އަޕްލޯޑްކޮށްފައެވެ", + "prompt.toast.modelAgentRequired.title": "އޭޖެންޓަކާއި މޮޑެލްއެއް ހޮވުން", + "prompt.toast.modelAgentRequired.description": "ޕްރޮމްޕްޓެއް ފޮނުވުމުގެ ކުރިން އޭޖެންޓަކާއި މޮޑެލްއެއް ހޮވުން.", + "prompt.toast.worktreeCreateFailed.title": "ވޯކްޓްރީ އުފެއްދުމަށް ނާކާމިޔާބުވިއެވެ", + "prompt.toast.sessionCreateFailed.title": "ސެޝަން އުފެއްދުމަށް ނާކާމިޔާބުވިއެވެ", + "prompt.toast.shellSendFailed.title": "ޝެލް ކޮމާންޑް ފޮނުވަން ނާކާމިޔާބުވިއެވެ", + "prompt.toast.commandSendFailed.title": "ކޮމާންޑް ފޮނުވަން ނާކާމިޔާބުވިއެވެ", + "prompt.toast.promptSendFailed.title": "ޕްރޮމްޕްޓް ފޮނުވަން ނާކާމިޔާބުވިއެވެ", + "prompt.toast.promptSendFailed.description": "ސެޝަން ހޯދުމަށް ނުކުޅެދިއްޖެއެވެ", + "dialog.mcp.title": "MCPs އެވެ", + "dialog.mcp.description": "{{enabled}} ގެ {{total}} އެނެބަލްކޮށްފައިވެއެވެ", + "dialog.mcp.empty": "އެއްވެސް MCPs ކޮންފިގްރޭޓް ކުރެވިފައެއް ނުވެއެވެ", + "dialog.lsp.empty": "LSPs ފައިލް ވައްތަރުތަކުން އޮޓޯ ޑިޓެކްޓް ކުރެވިފައިވެއެވެ", + "dialog.plugins.empty": "opencode.json ގައި ކޮންފިގްރޭޓް ކުރެވިފައިވާ ޕްލަގިންސް", + "mcp.status.connected": "ގުޅިއްޖެއެވެ", + "mcp.status.failed": "ފެއިލްވުން", + "mcp.status.needs_auth": "ބޭނުންވަނީ އޮތޯއެވެ", + "mcp.status.disabled": "ނުކުޅެދޭ", + "mcp.auth.clickToAuthenticate": "ސައްހަ ކުރުމަށް ކްލިކް ކުރާށެވެ", + "dialog.fork.empty": "ފޯކް ކުރާނެ މެސެޖެއް ނެތެވެ", + "dialog.directory.search.placeholder": "ފޯލްޑަރުތައް ހޯދުން", + "dialog.directory.empty": "ފޯލްޑަރެއް ނުފެނެއެވެ", + "dialog.directory.action.selectFile": "ފައިލް ހޮވާށެވެ", + "dialog.directory.action.selectFolder": "ފޯލްޑަރ ހޮވާށެވެ", + "dialog.directory.root": "މޫ", + "dialog.directory.parent": "ބެލެނިވެރިޔާއެވެ", + "dialog.directory.readError": "މި ފޯލްޑަރ ކިޔަން ނުކުޅެދިއްޖެއެވެ", + "app.server.unreachable": "{{server}} އަށް ވާސިލްވެވޭ ގޮތެއް ނުވިއެވެ", + "app.server.retrying": "އޮޓޮމެޓިކުން އަލުން މަސައްކަތް ކުރަނީ...", + "app.server.otherServers": "އެހެން ސަރވަރތަކެވެ", + "dialog.server.title": "ސަރވަރސް", + "dialog.server.description": "މި އެޕް ގުޅޭނީ ކޮން OpenCode ސަރވަރަކާ ބަދަލުކޮށްލާށެވެ.", + "dialog.server.search.placeholder": "ސަރވަރތައް ހޯދުން", + "dialog.server.empty": "އަދި ސާވަރެއް ނެތެވެ", + "dialog.server.add.title": "ސަރވަރ އިތުރުކުރުން", + "dialog.server.add.url": "ސަރވަރ އެޑްރެސް", + "dialog.server.add.placeholder": "http://localhost:4096 އެވެ", + "dialog.server.add.error": "ސަރވަރ އާއި ގުޅެވޭ ގޮތެއް ނުވިއެވެ", + "dialog.server.add.checking": "ޗެކް ކުރަމުން...", + "dialog.server.add.button": "ސަރވަރ އިތުރުކުރުން", + "dialog.server.add.name": "ސަރވަރ ނަން (އިޚްތިޔާރީ)", + "dialog.server.add.namePlaceholder": "Localhost އެވެ", + "dialog.server.add.username": "ޔޫޒަރނޭމް (އިޚްތިޔާރީ)", + "dialog.server.add.usernamePlaceholder": "ޔޫޒަރނޭމް", + "dialog.server.add.password": "ޕާސްވޯޑް (އިޚްތިޔާރީ)", + "dialog.server.add.passwordPlaceholder": "ޕާސްވަރޑް", + "dialog.server.edit.title": "އެޑިޓް ސަރވަރ", + "dialog.server.default.title": "ޑިފޯލްޓް ސަރވަރ އެވެ", + "dialog.server.default.description": + "ލޯކަލް ސަރވަރ އެއް ސްޓާޓް ކުރުމުގެ ބަދަލުގައި އެޕް ލޯންޗް ކުރުމުން މި ސަރވަރ އާއި ގުޅާށެވެ. ރީސްޓާޓް ކުރަން ޖެހެއެވެ.", + "dialog.server.default.none": "އެއްވެސް ސަރވަރެއް ހޮވާފައެއް ނުވެއެވެ", + "dialog.server.default.set": "މިހާރު ހުރި ސަރވަރ ޑިފޯލްޓް ގޮތުގައި ސެޓްކުރުން", + "dialog.server.default.clear": "ސާފުކޮށް", + "dialog.server.action.remove": "ސަރވަރ ނައްތާލާށެވެ", + "dialog.server.menu.edit": "ބަދަލު ގެނައުން", + "dialog.server.menu.default": "ޑިފޯލްޓް ގޮތުގައި ސެޓްކުރުން", + "dialog.server.menu.defaultRemove": "ޑިފޯލްޓް ނައްތާލާށެވެ", + "dialog.server.menu.delete": "ފުހެލުން", + "dialog.server.current": "މިހާރުގެ ސަރވަރ", + "dialog.server.status.default": "ޑީފޯލްޓް", + "wsl.server.add": "WSL ސަރވަރ އިތުރުކުރުން", + "wsl.server.addShort": "WSL އިތުރުކުރުން", + "wsl.server.label": "WSL އެވެ", + "wsl.server.menu.label": "WSL ސަރވަރ އެވެ", + "wsl.server.retryStart": "އަލުން ފަށަން މަސައްކަތް ކުރާށެވެ", + "wsl.server.updating": "އަޕްޑޭޓް ކުރަމުން...", + "wsl.onboarding.step.distro": "ޑިސްޓްރޯ ހޮވާށެވެ", + "wsl.onboarding.step.opencode": "OpenCode އެވެ", + "wsl.onboarding.checkingRuntime": "ޗެކް ކުރަމުންދަނީ WSL...", + "wsl.onboarding.restartRequired": "Windows އިންސްޓޯލް ކުރުން ނިންމުމަށް ރީސްޓާޓް އެއް ބޭނުންވެއެވެ.", + "wsl.onboarding.ready": "WSL ތައްޔާރުވެއްޖެއެވެ.", + "wsl.onboarding.required": "WSL ކުރިއަށް ގެންދަން ޖެހެ އެވެ.", + "wsl.onboarding.checkingDistros": "ޗެކިންގ ޑިސްޓްރޯސް...", + "wsl.onboarding.installingDistro": "{{distro}} އިންސްޓޯލް ކުރަމުން...", + "wsl.onboarding.checkingDistro": "ޗެކް ކުރަމުންދަނީ {{distro}}...", + "wsl.onboarding.listingDistros": "ލިސްޓިން ޑިސްޓްރޯސް...", + "wsl.onboarding.distroReady": "{{distro}} ތައްޔާރުވެއްޖެއެވެ.", + "wsl.onboarding.distroNotInstalled": "{{distro}} އަދި އިންސްޓޯލްކޮށްފައެއް ނުވެއެވެ.", + "wsl.onboarding.openDistroOnce": "ސެޓަޕް ނިންމާލުމަށްޓަކައި {{distro}} އެއްފަހަރު ހުޅުވާލާށެވެ.", + "wsl.onboarding.finishingDistro": "{{distro}} އަށް ފިނިޝިންގ ސެޓަޕް.", + "wsl.onboarding.pickDistro": "ޑިސްޓްރޯއެއް ހޮވާ ނުވަތަ ތިރީގައިވާ ޑިސްޓްރޯއެއް އިންސްޓޯލް ކުރާށެވެ.", + "wsl.onboarding.checkingOpencode": "ޗެކް ކުރަމުންދަނީ OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}} ގައި OpenCode ޗެކްކުރަނީ...", + "wsl.onboarding.updatingOpencode": "އަޕްޑޭޓް ކުރަމުންދަނީ OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}} ގައި OpenCode އަޕްޑޭޓް ކުރަމުންދާ...", + "wsl.onboarding.updateOpencodeIn": "{{distro}} ގައި OpenCode އަޕްޑޭޓް ކުރާށެވެ.", + "wsl.onboarding.updateOpencode": "OpenCode އަޕްޑޭޓް ކުރާށެވެ", + "wsl.onboarding.opencodeReadyIn": "OpenCode ތައްޔާރުކޮށްފައިވަނީ {{distro}} ގައެވެ.", + "wsl.onboarding.opencodeReady": "OpenCode ތައްޔާރުވެއްޖެއެވެ.", + "wsl.onboarding.installOpencodeIn": "{{distro}} ގައި OpenCode އިންސްޓޯލް ކުރާށެވެ.", + "wsl.onboarding.installOpencode": "OpenCode އިންސްޓޯލް ކުރާށެވެ", + "wsl.onboarding.chooseDistroFirst": "ފުރަތަމަ ޑިސްޓްރޯއެއް ހޮވާށެވެ.", + "wsl.onboarding.loadFailed": "WSL ސްޓޭޓް ލޯޑް ނުކުރެވިއްޖެއެވެ.", + "wsl.onboarding.loading": "ލޯޑިންގ...", + "wsl.onboarding.installedDistros": "އިންސްޓޯލް ކޮށްފައިވާ ޑިސްޓްރޯސް", + "wsl.onboarding.checkAgain": "އަނެއްކާވެސް ޗެކް ކުރާށެވެ", + "wsl.onboarding.distroStatus.ready": "ތައްޔާރު", + "wsl.onboarding.distroStatus.checking": "ޗެކް ކުރަމުން...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode އިންސްޓޯލްކޮށްފައެއް ނުވެއެވެ", + "wsl.onboarding.distroStatus.missingTools": "ގެއްލިފައިވަނީ bash، curl އެވެ", + "wsl.onboarding.distroStatus.unsupported": "ސަޕޯޓް ނުކުރެވޭ · WSL ބޭނުންކުރުން 2", + "wsl.onboarding.needAnotherDistro": "އަނެއްކާވެސް ޑިސްޓްރޯއެއް ބޭނުންތަ؟", + "wsl.onboarding.needAnotherDistroHint": "WSL ކެޓަލަގުން Linux ޑިސްޓްރިބިއުޝަނެއް އިންސްޓޯލް ކުރުން", + "wsl.onboarding.wslNotInstalled.title": "WSL އިންސްޓޯލްކޮށްފައެއް ނުވެއެވެ", + "wsl.onboarding.wslNotInstalled.description": + "WSL އަށް WSL ސަރވަރ އެއް އިތުރު ކުރުމުގެ ކުރިން WSL (Windows Subsystem for Linux) ބޭނުންވެއެވެ", + "wsl.onboarding.wslUnavailable.title": "WSL ލިބެން ނެތް", + "wsl.onboarding.wslUnavailable.description": "މި މެޝިނުގައި OpenCode އަށް WSL ޔަގީން ނުކުރެވުނެވެ.", + "wsl.onboarding.installWsl": "WSL އިންސްޓޯލް ކުރާށެވެ", + "wsl.onboarding.windowsRestartRequired": + "WSL އިންސްޓޯލް ކުރުން ނިންމުމަށް Windows އަލުން ސްޓާޓްކޮށް، ދެން OpenCode އަލުން ހުޅުވާށެވެ.", + "wsl.onboarding.next": "ދެން", + "wsl.onboarding.refresh": "ރިފްރެޝް ކޮށްލާށެވެ", + "wsl.onboarding.allDistrosAdded": "އިންސްޓޯލްކޮށްފައިވާ ހުރިހާ ޑިސްޓްރޯތަކެއް މިހާރުވެސް އިތުރުކޮށްފައިވެއެވެ.", + "wsl.onboarding.noDistros": "އަދި ޑިސްޓްރޯއެއް ހޯދިފައެއް ނުވެއެވެ.", + "wsl.onboarding.install": "އެޅުން", + "wsl.onboarding.installing": "އިންސްޓޯލް ކުރަމުން...", + "wsl.onboarding.installDistro": "ޑިސްޓްރޯ އިންސްޓޯލް ކުރާށެވެ", + "wsl.onboarding.searchDistros": "ޑިސްޓްރޯސް ހޯދުން", + "wsl.onboarding.wsl2Required": "WSL 2 ބޭނުންވެއެވެ.", + "wsl.onboarding.toolsRequired": "މި ޑިސްޓްރޯއަށް ބޭނުންވަނީ bash އަދި curl އެވެ.", + "wsl.onboarding.openTerminal": "އޯޕަން ޓާމިނަލް", + "wsl.onboarding.path": "މަގު: {{path}}", + "wsl.onboarding.notFound": "ނުފެނެއެވެ", + "wsl.onboarding.version": "ވަރޝަން: {{version}}", + "wsl.onboarding.unknown": "ނޭނގޭ އެއްޗެއް", + "wsl.onboarding.desktopVersion": "ޑެސްކްޓޮޕް {{version}} އެވެ", + "wsl.onboarding.versionMismatch": "އިންސްޓޯލް ކޮށްފައިވާ ވަރޝަން ޑެސްކްޓޮޕް އެޕް ވަރޝަން އާއި އެއްގޮތެއް ނުވެއެވެ.", + "wsl.onboarding.adding": "އިތުރުކުރަނީ...", + "help.tabs.toast.ariaLabel": + "Tabs ތަޢާރަފްކުރުން. ޓެބްތަކާއެކު ތިމާގެ މަސައްކަތާއި އެކްޓިވް ސެޝަންތައް އިންތިޒާމުކުރުން", + "help.tabs.toast.dismiss": "Tabs ގެ މައުލޫމާތު ބާތިލްކުރުން", + "help.tabs.title": "Tabs ތަޢާރަފްކުރުން", + "help.tabs.description": "ޓެބްތަކާއެކު ތިމާގެ މަސައްކަތާއި އެކްޓިވް ސެޝަންތައް އިންތިޒާމުކުރުން", + "help.tabs.date": "ޖުލައި 14", + "help.tabs.introduction": "OpenCode Desktop މިހާރު ބިނާކޮށްފައިވަނީ ޓެބްތަކުގެ ވަށައިގެންނެވެ.", + "help.tabs.sessions": + "ޓެބެއްގައި އާ ސެޝަނެއް ފަށާ، ނުވަތަ ތިމާގެ އެއްވެސް ޕްރޮޖެކްޓަކުން މިހާރު ހުރި ސެޝަނެއް ހުޅުވާށެވެ. އާ ކަމެއް ފެށުމުން އާ ޓެބެއް ހުޅުވާ، އަދި ނިމުމުން ބަންދު ކުރާށެވެ.", + "help.tabs.organize": + "މަދު ޓެބްތަކެއް ހުޅުވާފައި ބެހެއްޓުމުން އެކްޓިވް ސެޝަންތައް އިންތިޒާމު ކުރުމަށް ފަސޭހަވެގެންދެއެވެ. ޓެބްތައް ވަށައިގެން ބެހެއްޓުމަށް ރާވާނަމަ ހަނދާން އާވާ އެއްޗަކަށް ބަދަލުކޮށްލާށެވެ.", + "help.tabs.home": + "ތިބާގެ ހުރިހާ ސެޝަންތަކާއި ޕްރޮޖެކްޓްތައް އާ Home ސްކްރީނުން ފެންނާނެއެވެ. ސެޝަނެއް ހޮވުމުން ޓެބެއްގައި ހުޅުވޭނެއެވެ.", + "help.tabs.persistence": "އެޕް އަލުން ހުޅުވާލުމުން އަދިވެސް ޓެބްތައް ހުޅުވާފައި ހުރެއެވެ.", + "help.tabs.worktrees": + "އާ ޑިޒައިން އިން އަދި Git ވޯކްޓްރީސް އަށް ސަޕޯޓް ނުކުރާއިރު، ވަރަށް އަވަހަށް އަންނާނެ އެވެ. އެހެންކަމުން ކުރީގެ ލޭއައުޓް ބޭނުންކުރުން ކުރިއަށް ގެންދަން ބޭނުންނަމަ ސެޓިންގްސްގައިވާ ލޭއައުޓްތަކުގެ މެދުގައި ބަދަލުވެވޭނެއެވެ. ހަމައެކަނި ހަނދާނުގައި ބަހައްޓަންވީ އާ ލޭއައުޓް ދެތިން ހަފްތާއެއްހާ ދުވަހުގެ ތެރޭގައި ދާއިމީ ވެގެންދާނެ ކަމެވެ.", + "server.row.noUsername": "ޔޫޒަރނޭމް އެއް ނެތެވެ", + "dialog.project.edit.title": "އެޑިޓް ޕްރޮޖެކްޓް", + "dialog.project.edit.name": "ނަން", + "dialog.project.edit.icon": "އައިކޮން", + "dialog.project.edit.icon.alt": "ޕްރޮޖެކްޓް އައިކޮން", + "dialog.project.edit.icon.hint": "ތަސްވީރަކަށް ކްލިކް ނުވަތަ ދަމާލާށެވެ", + "dialog.project.edit.icon.recommended": "ލަފާދެނީ: 128x128px", + "dialog.project.edit.color": "ކުލަ", + "dialog.project.edit.color.select": "{{color}} ކުލަ ހޮވާށެވެ", + "dialog.project.edit.worktree.startup": "ވޯކްސްޕޭސް ސްޓާޓްއަޕް ސްކްރިޕްޓް", + "dialog.project.edit.worktree.startup.description": "އާ ވޯކްސްޕޭސް (ވޯކްޓްރީ) އުފެއްދުމަށްފަހު ހިނގައެވެ.", + "dialog.project.edit.worktree.startup.placeholder": "މ. bun install އެވެ", + "dialog.releaseNotes.action.getStarted": "ފަށާށެވެ", + "dialog.releaseNotes.action.next": "ދެން", + "dialog.releaseNotes.action.hideFuture": "ކުރިއަށް އޮތްތަނުގައި މިކަންކަން ނުދައްކާށެވެ", + "dialog.releaseNotes.media.alt": "ރިލީޒް ޕްރިވިއު", + "context.breakdown.title": "ކޮންޓެކްސްޓް ބްރޭކްޑައުން", + "context.breakdown.note": + 'އިންޕުޓް ޓޯކަންތަކުގެ ގާތްގަނޑަކަށް ބްރޭކްޑައުން. "އެހެން" ގައި ހިމެނެނީ ޓޫލް ޑެފިނިޝަންސް އާއި އޯވަހެޑް އެވެ.', + "context.breakdown.system": "ސިސްޓަމް", + "context.breakdown.user": "ޔޫޒަރ", + "context.breakdown.assistant": "އެސިސްޓަންޓް", + "context.breakdown.tool": "ޓޫލް ކޯލްސް", + "context.breakdown.other": "އެހެން", + "context.systemPrompt.title": "ސިސްޓަމް ޕްރޮމްޕްޓް", + "context.rawMessages.title": "ރޯ މެސެޖުތަކެވެ", + "context.export.session": "އެކްސްޕޯޓް ސެޝަން", + "context.stats.session": "ސެޝަން", + "context.stats.messages": "މެސެޖުތައް", + "context.stats.provider": "ޕްރޮވައިޑަރެވެ", + "context.stats.model": "މޮޑެލް", + "context.stats.limit": "ކޮންޓެކްސްޓް ލިމިޓް", + "context.stats.totalTokens": "ޖުމްލަ ޓޯކަންސް", + "context.stats.usage": "ބޭނުންކުރުން", + "context.stats.inputTokens": "އިންޕުޓް ޓޯކަންސް", + "context.stats.outputTokens": "އައުޓްޕުޓް ޓޯކަންސް", + "context.stats.reasoningTokens": "ރީޒަނިންގ ޓޯކަންސް", + "context.stats.cacheTokens": "ކެޝް ޓޯކަންސް (ކިޔުން/ލިޔުން)", + "context.stats.userMessages": "ޔޫޒަރ މެސެޖްސް", + "context.stats.assistantMessages": "އެސިސްޓެންޓް މެސެޖްތައް", + "context.stats.totalCost": "ޖުމްލަ ހަރަދު", + "context.stats.sessionCreated": "ސެޝަން އުފެދިއްޖެއެވެ", + "context.stats.lastActivity": "އެންމެ ފަހުގެ ހަރަކާތް", + "context.usage.tokens": "ޓޯކަންސް އެވެ", + "context.usage.usage": "ބޭނުންކުރުން", + "context.usage.cost": "ހަރަދު", + "context.usage.clickToView": "ކޮންޓެކްސްޓް ބަލާލުމަށް ކްލިކް ކުރާށެވެ", + "context.usage.view": "ކޮންޓެކްސްޓް ބޭނުންކުރުން ބަލާށެވެ", + "language.en": "އިނގިރޭސި", + "language.zh": "简体中文 ގެ ވާހަކައެވެ", + "language.zht": "繁體中文 ގެ ވާހަކައެވެ", + "language.ko": "한국어 ގެ ފަރާތުންނެވެ", + "language.de": "ޑޮއިޗް އެވެ", + "language.es": "Español", + "language.fr": "ފްރާންސެއިސް އެވެ", + "language.da": "ޑަންސްކް އެވެ", + "language.ja": "日本語 އެވެ", + "language.pl": "ޕޮލްސްކީ އެވެ", + "language.ru": "Русский އެވެ", + "language.ar": "العربية އެވެ", + "language.no": "ނޯސްކް އެވެ", + "language.br": "ޕޯޗުގަލްސް (ބްރެސިލް)", + "language.bs": "ބޮސަންސްކީ އެވެ", + "language.uk": "Українська ގެ ވާހަކައެވެ", + "language.th": "ไทย", + "language.tr": "ތުރުކްޗޭ އެވެ", + "toast.language.title": "ބަސް", + "toast.language.description": "{{language}} އަށް ބަދަލުވެއްޖެއެވެ", + "toast.theme.title": "ތީމް ބަދަލުވިއެވެ", + "toast.scheme.title": "ކުލަ ސްކީމް", + "toast.workspace.enabled.title": "ވޯކްސްޕޭސްތައް އެނެބަލްކޮށްފައިވެއެވެ", + "toast.workspace.enabled.description": "މިހާރު ސައިޑްބާރގައި އެތައް ވޯކްޓްރީއެއް ފެނެއެވެ", + "toast.workspace.disabled.title": "ވޯކްސްޕޭސްތައް ޑިސެބިލްކޮށްފައި", + "toast.workspace.disabled.description": "ސައިޑްބާރގައި ދައްކާފައިވަނީ ހަމައެކަނި މައިގަނޑު ވޯކްޓްރީއެވެ", + "toast.permissions.autoaccept.on.title": "އޮޓޯ އެކްސެޕްޓިންގ ހުއްދަތައް", + "toast.permissions.autoaccept.on.description": "ހުއްދައަށް އެދޭ ފަރާތްތަކުން އޮޓޮމެޓިކުން ފާސްކުރެވޭނެއެވެ", + "toast.permissions.autoaccept.off.title": "ހުއްދަތައް އޮޓޯ އެކްސެޕްޓްކުރުން ހުއްޓާލިއެވެ", + "toast.permissions.autoaccept.off.description": "ހުއްދައަށް އެދޭ ފަރާތްތަކަށް ރުހުން ހޯދަން ޖެހޭނެއެވެ", + "toast.model.none.title": "އެއްވެސް މޮޑެލްއެއް ހޮވިފައެއް ނުވެއެވެ", + "toast.model.none.description": "މި ސެޝަން ޚުލާޞާ ކުރުމަށް ޕްރޮވައިޑަރަކާ ގުޅުވާށެވެ", + "toast.file.loadFailed.title": "ފައިލް ލޯޑް ނުކުރެވުނެވެ", + "toast.file.listFailed.title": "ފައިލްތައް ލިސްޓް ނުކުރެވުނެވެ", + "toast.context.noLineSelection.title": "ލައިން ސެލެކްޝަނެއް ނެތެވެ", + "toast.context.noLineSelection.description": "ފުރަތަމަ ފައިލް ޓެބެއްގައި ލައިން ރޭންޖެއް ހޮވާށެވެ.", + "toast.session.share.copyFailed.title": "URL ކްލިޕްބޯޑަށް ކޮޕީ ނުކުރެވުނެވެ", + "toast.session.share.success.title": "ސެޝަން ޝެއާ ކުރިއެވެ", + "toast.session.share.success.description": "ޝެއާ URL ކްލިޕްބޯޑަށް ކޮޕީކޮށްފައި!", + "toast.session.share.failed.title": "ޝެއާ ސެޝަން ނާކާމިޔާބުވި", + "toast.session.share.failed.description": "ސެޝަން ހިއްސާ ކުރަމުން ދިޔައިރު ގޯހެއް ދިމާވިއެވެ", + "toast.session.unshare.success.title": "ސެޝަން އަންޝެއަރޑް", + "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": "އަޕްޑޭޓް ލިބެން ހުރެއެވެ", + "toast.update.description": "OpenCode ({{version}}) ގެ އާ ވަރޝަނެއް މިހާރު އިންސްޓޯލް ކުރެވޭނެއެވެ.", + "toast.update.action.installRestart": "އިންސްޓޯލްކޮށް އަލުން ސްޓާޓްކުރުން", + "toast.update.action.notYet": "އަދި ނޫން", + "error.page.title": "ކަމެއް ގޯސްވީއެވެ", + "error.page.description": "އެޕްލިކޭޝަން ލޯޑް ކުރަމުން ދިޔައިރު ގޯހެއް ދިމާވިއެވެ.", + "error.page.description.localServerStartup": "ލޯކަލް ސަރވަރ ސްޓާޓް ކުރަމުން ދިޔައިރު ގޯހެއް ދިމާވިއެވެ.", + "error.page.details.label": "އެރަރ ޑީޓެއިލްސް", + "error.page.action.restart": "އަލުން ފަށާށެވެ", + "error.page.action.report": "ރިޕޯޓް އެރަރ", + "error.page.action.reported": "އެރަރ ރިޕޯޓް ކުރެވިއްޖެއެވެ", + "error.page.action.exportLogs": "އެކްސްޕޯޓް ލޮގްސް", + "error.page.action.checking": "ޗެކް ކުރަމުން...", + "error.page.action.checkUpdates": "އަޕްޑޭޓްތައް ހުރިތޯ ބަލާށެވެ", + "error.page.action.updateTo": "{{version}} އަށް އަޕްޑޭޓް ކުރާށެވެ", + "error.page.circular": "[ސާކިއުލާ]", + "error.page.report.prefix": "މި އެރަރ OpenCode ޓީމަށް ރިޕޯޓް ކުރައްވާށެވެ", + "error.page.report.discord": "Discord ގައި", + "error.page.version": "ވަރޝަން: {{version}}", + "error.dev.rootNotFound": + "ރޫޓް އެލިމެންޓް ނުފެނެއެވެ. ތިބާގެ index.html އަށް އެޑް ކުރަން ހަނދާން ނެތުނީ ހެއްޔެވެ؟ ނުވަތަ id އެޓްރިބިއުޓް ގޯސްކޮށް އިމްތިހާނު ވެދާނެ ހެއްޔެވެ؟", + "error.serverSync.connectFailed": + "ސަރވަރ އާއި ގުޅެވޭ ގޮތެއް ނުވިއެވެ. `{{url}}` ގައި ސަރވަރެއް ހިނގަމުންދާ ހެއްޔެވެ؟", + "error.serverSDK.noServerAvailable": "ސަރވަރެއް ނުލިބެއެވެ", + "error.serverSDK.serverNotAvailable": "ސަރވަރ ނުލިބޭ", + "error.childStore.persistedCacheCreateFailed": "ޕަރސިސްޓެޑް ކެޝް އުފެއްދުމަށް ނާކާމިޔާބުވިއެވެ", + "error.childStore.persistedProjectMetadataCreateFailed": + "ޕަރސިސްޓެޑް ޕްރޮޖެކްޓް މެޓަޑޭޓާ އުފެއްދުމަށް ނާކާމިޔާބުވިއެވެ", + "error.childStore.persistedProjectIconCreateFailed": "ޕަރސިސްޓެޑް ޕްރޮޖެކްޓް އައިކޮން އުފެއްދުމަށް ނާކާމިޔާބުވިއެވެ", + "error.childStore.storeCreateFailed": "ސްޓޯރ އުފެއްދުމަށް ނާކާމިޔާބުވިއެވެ", + "directory.error.invalidUrl": "URL ގައި ސައްހަ ނޫން ޑައިރެކްޓަރީއެއް.", + "error.chain.unknown": "ނޭނގޭ ގޯހެއް", + "error.server.invalidConfiguration": "ސައްހަ ނޫން ކޮންފިގްރޭޝަން", + "error.chain.causedBy": "ސަބަބަކީ:", + "error.chain.apiError": "API ގެ ގޯހެއް", + "error.chain.status": "ސްޓޭޓަސް: {{status}}", + "error.chain.retryable": "އަލުން ޓްރައި ކުރެވޭނީ: {{retryable}}", + "error.chain.responseBody": "ޖަވާބުދާރީވާ ހަށިގަނޑު:\n{{body}} އެވެ", + "error.chain.didYouMean": "ތިޔަ މާނަކުރީ: {{suggestions}} ހެއްޔެވެ", + "error.chain.modelNotFound": "މޮޑެލް ނުފެނޭ: {{provider}}/{{model}}", + "error.chain.checkConfig": "ތިބާގެ ކޮންފިގް (opencode.json) ޕްރޮވައިޑަރ/މޮޑެލް ނަންތައް ޗެކްކުރުން", + "error.chain.mcpFailed": + 'MCP ސަރވަރ "{{name}}" ފޭލްވެއްޖެއެވެ. ނޯޓް، OpenCode އިން އަދި MCP އޮތެންޓިކޭޝަން އަށް ސަޕޯޓް ނުކުރެއެވެ.', + "error.chain.providerAuthFailed": "ޕްރޮވައިޑަރ އޮތެންޓިކޭޝަން ފެއިލްވެއްޖެ ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'ޕްރޮވައިޑަރ "{{provider}}" އިނިޝިއަލައިޒް ނުކުރެވުނެވެ. ކްރެޑެންޝިއަލްސް އަދި ކޮންފިގްރޭޝަން ޗެކްކުރުން.', + "error.chain.configJsonInvalid": "{{path}} ގައި ކޮންފިގް ފައިލަކީ ސައްހަ JSON(C) އެއް ނޫނެވެ", + "error.chain.configJsonInvalidWithMessage": + "{{path}} ގައި ކޮންފިގް ފައިލަކީ ސައްހަ ފައިލެއް ނޫން JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} ގައިވާ ޑައިރެކްޓަރީ "{{dir}}" އަކީ ސައްހަ އެއްޗެއް ނޫނެވެ. ޑައިރެކްޓަރީގެ ނަން "{{suggestion}}" އަށް ބަދަލުކޮށް ނުވަތަ ނައްތާލާށެވެ. މިއީ އާންމު ޓައިޕޯއެކެވެ.', + "error.chain.configFrontmatterError": "{{path}} ގައި ފްރޮންޓްމެޓަރ ޕާރސް ކުރަން ނާކާމިޔާބު:\n{{message}}", + "error.chain.configInvalid": "{{path}} ގައި ކޮންފިގް ފައިލް އަކީ ސައްހަ ފައިލްއެކެވެ", + "error.chain.configInvalidWithMessage": "{{path}} ގައި ކޮންފިގް ފައިލް ސައްހަ ނޫން: {{message}}", + "notification.permission.title": "ހުއްދަ ބޭނުންވެއެވެ", + "notification.permission.description": "{{sessionTitle}} އިން {{projectName}} އަށް ހުއްދަ ބޭނުންވެއެވެ", + "notification.question.title": "ސުވާލު", + "notification.question.description": "{{sessionTitle}} އިން {{projectName}} ގައި ސުވާލެއް އެބައޮތެވެ", + "notification.action.goToSession": "ސެޝަނަށް ދާށެވެ", + "notification.session.responseReady.title": "ޖަވާބުދާރީވާން ތައްޔާރު", + "notification.session.error.title": "ސެޝަން އެރަރ", + "notification.session.error.fallbackDescription": "ގޯހެއް ދިމާވިއެވެ", + "home.recentProjects": "ފަހުގެ މަޝްރޫއުތަކެވެ", + "home.empty.title": "ފަހުގެ މަޝްރޫއެއް ނުހިނގާ", + "home.empty.description": "ލޯކަލް ޕްރޮޖެކްޓެއް ހުޅުވައިގެން ފަށާށެވެ", + "home.title": "މައި ޞަފްޙާ", + "home.projects": "މަޝްރޫއުތައް", + "home.project.add": "ޕްރޮޖެކްޓް އިތުރުކުރުން", + "home.recentlyClosed": "ދާދިފަހުން ބަންދުކުރިއެވެ", + "home.server.collapse": "ސަރވަރ ޕްރޮޖެކްޓްތައް ކޮލަޕްސް ކުރުން", + "home.server.expand": "ސަރވަރ ޕްރޮޖެކްޓްތައް ފުޅާކުރުން", + "home.sessions.search.placeholder": "ސަރޗް ސެޝަންތަކެވެ", + "home.sessions.search.placeholder.scoped": "{{scope}} ގައި ސަރޗް ސެޝަންތައް", + "home.sessions.search.sessions": "ސެޝަންތައް", + "home.sessions.search.noResults": "{{query}} އަށް އެއްވެސް ސެޝަނެއް ނުފެނެއެވެ", + "home.sessions.empty": "މިތާ އަދި އެއްވެސް ކަމެއް ނެތެވެ", + "home.sessions.empty.description": "ފެށުމަށްޓަކައި ސެޝަނެއް އުފެއްދުން", + "home.sessions.group.today": "މިއަދު", + "home.sessions.group.yesterday": "އިއްޔެ", + "home.sessions.group.older": "އުމުރުން ދުވަސްވީއެވެ", + "home.providerTip": + "75+ ޕްރޮވައިޑަރުންނާ ގުޅިގެން އެހެން މޮޑެލްތައް ބޭނުންކުރުން، އޭގެ ތެރޭގައި Claude، GPT، Gemini ފަދަ މޮޑެލްތައް ހިމެނެއެވެ", + "session.tab.session": "ސެޝަން", + "session.tab.review": "ރިވިއު", + "session.tab.context": "ކޮންޓެކްސްޓް", + "session.tab.unknown": "ނޭނގޭ ސެޝަނެއް", + "session.panel.reviewAndFiles": "ރިވިއުކޮށް ފައިލްކުރުން", + "session.error.notFound": "މި ސެޝަން ހޯދިފައެއް ނުވެއެވެ", + "session.error.notFound.description": "މި ޓެބުން އިޝާރާތް ކުރަނީ މި ސަރވަރގައި މިހާރު ނެތް ސެޝަނަކަށެވެ.", + "session.error.notFound.closeTab": "Tab ބަންދުކުރުން", + "session.error.serverConnection": "މި ސަރވަރ އާއި ގުޅެވޭކަށް ނެތެވެ", + "session.review.filesChanged": "ފައިލްތައް ބަދަލުވެއްޖެ {{count}}", + "session.review.change.one": "ބަދަލު", + "session.review.change.other": "ބަދަލުތައް", + "session.review.loadingChanges": "ބަދަލުތައް ލޯޑް ކުރަނީ...", + "session.review.empty": "މި ޖަލްސާއަށް އަދި ބަދަލެއް ނާދެއެވެ", + "session.review.noVcs": "Git ވަރޝަން ކޮންޓްރޯލް ސިސްޓަމެއް ނުފެނި، ބަދަލުތައް ނުފެނެއެވެ", + "session.review.noVcs.createGit.title": "Git ރިޕޮޒިޓަރީއެއް އުފެއްދުން", + "session.review.noVcs.createGit.description": "މި މަޝްރޫޢަށް އަންނަ ބަދަލުތައް ޓްރެކްކޮށް، ރިވިއުކޮށް، އަންޑޯކުރުން", + "session.review.noVcs.createGit.actionLoading": "Git ރިޕޮޒިޓަރީ އުފެއްދުމުގެ މަސައްކަތް...", + "session.review.noVcs.createGit.action": "Git ރިޕޮޒިޓަރީ އުފެއްދުން", + "session.review.noSnapshot": + "ކޮންފިގްގައި ސްނެޕްޝޮޓް ޓްރެކިންގ ޑިސެބިލް ކޮށްފައި ހުރުމުން ސެޝަން ބަދަލުތައް ނުލިބެއެވެ", + "session.review.noChanges": "އެއްވެސް ބަދަލެއް ނާދެއެވެ", + "session.review.noUncommittedChanges": "އަދި ކޮމިޓް ނުކުރާ ބަދަލެއް ނާދެއެވެ", + "session.review.noBranchChanges": "އަދި އެއްވެސް ބްރާންޗަކަށް ބަދަލެއް ނާދެއެވެ", + "session.files.selectToOpen": "ހުޅުވަން ބޭނުންވާ ފައިލެއް ހޮވާށެވެ", + "session.files.all": "ހުރިހާ ފައިލްއެކެވެ", + "session.files.empty": "ފައިލްތަކެއް ނެތެވެ", + "session.files.binaryContent": "ބައިނަރީ ފައިލް (ކޮންޓެންޓް ނުދެއްކޭނެ)", + "session.messages.renderEarlier": "ކުރީގެ މެސެޖުތައް ރެންޑަރ ކުރާށެވެ", + "session.messages.loadingEarlier": "ކުރީގެ މެސެޖުތައް ލޯޑް ކުރަނީ...", + "session.messages.loadEarlier": "ކުރީގެ މެސެޖުތައް ލޯޑް ކުރާށެވެ", + "session.messages.loading": "މެސެޖުތައް ލޯޑް ކުރަނީ...", + "session.messages.jumpToLatest": "އެންމެ ފަހުގެ ވާހަކައަށް ފުންމާލާށެވެ", + "session.context.addToContext": "ކޮންޓެކްސްޓަށް {{selection}} އިތުރުކުރުން", + "session.todo.title": "ޓޮޑޯސް އެވެ", + "session.todo.collapse": "ހޭނެތިގެން ވެއްޓުން", + "session.todo.expand": "އިތުރުކުރުން", + "session.todo.progress": "{{done}} ގެ {{total}} ޓޯޑޯސް ނިމިއްޖެއެވެ", + "session.question.progress": "{{current}} ގެ {{total}} ސުވާލުތަކެވެ", + "session.question.minimize": "ސުވާލު އެންމެ ކުޑަކުރުން", + "session.question.restore": "ސުވާލު އަލުން އާލާކުރުން", + "session.question.pending.one": "{{count}} ކުރިއަށް އޮތް ސުވާލު", + "session.question.pending.other": "{{count}} ކުރިއަށް އޮތް ސުވާލުތައް", + "session.followupDock.summary.one": "{{count}} ކިއު ކޮށްފައިވާ މެސެޖެކެވެ", + "session.followupDock.summary.other": "{{count}} ކިއު ކުރެވިފައިވާ މެސެޖުތައް", + "session.followupDock.sendNow": "މިހާރު ފޮނުއްވާ", + "session.followupDock.edit": "ބަދަލު ގެނައުން", + "session.followupDock.collapse": "ކިއު ޖަހާފައިވާ މެސެޖުތައް ކޮލަޕްސް ކޮށްލާށެވެ", + "session.followupDock.expand": "ކިއު ކުރެވިފައިވާ މެސެޖުތައް ފުޅާކުރުން", + "session.revertDock.summary.one": "{{count}} ރޯލް ބެކް މެސެޖެއް", + "session.revertDock.summary.other": "{{count}} ރޯލްބެކް މެސެޖުތައް", + "session.revertDock.collapse": "ކޮލަޕްސް ރޯލް ބެކް މެސެޖުތަކެވެ", + "session.revertDock.expand": "ރޯލް ބެކް މެސެޖުތައް ފުޅާކުރުން", + "session.revertDock.restore": "މެސެޖު ރިސްޓޯރ ކުރާށެވެ", + "session.new.title": "ކޮންމެ އެއްޗެއް ބިނާކުރާށެވެ", + "session.new.project.new": "އާ މަޝްރޫއެއް", + "session.new.project.search": "ޕްރޮޖެކްޓްތައް ހޯދުން", + "session.new.project.add": "ޕްރޮޖެކްޓް އިތުރުކުރުން", + "session.new.worktree.main": "މައި ބްރާންޗް", + "session.new.worktree.mainWithBranch": "މައި ގޮފި ({{branch}})", + "session.new.worktree.create": "އާ ވޯކްޓްރީ އުފެއްދުން", + "session.new.workspace.runIn": "ރަން ސެޝަން އިން", + "session.new.workspace.triggerLocal": "ލޯކަލް", + "session.new.workspace.local": "ލޯކަލް ރިޕޮޒިޓަރީ", + "session.new.workspace.existing": "ވޯކްސްޕޭސް...", + "session.new.git.none": "އެއްވެސް Git އެއް ނެތެވެ", + "session.new.lastModified": "އެންމެ ފަހުން ބަދަލުކުރީ", + "session.header.search.placeholder": "{{project}} ހޯދުން", + "session.header.searchFiles": "ފައިލްތައް ހޯދުން", + "session.header.openIn": "ހުޅުވާފައި އިން", + "session.header.open.action": "{{app}} ހުޅުވާލާށެވެ", + "session.header.open.ariaLabel": "{{app}} ގައި ހުޅުވާފައި ހުރެއެވެ", + "session.header.open.menu": "އޮޕްޝަންސް ހުޅުވާލާށެވެ", + "session.header.open.copyPath": "ކޮޕީ މަގު", + "session.header.open.finder": "Finder އެވެ", + "session.header.open.fileExplorer": "File Explorer އެވެ", + "session.header.open.fileManager": "ފައިލް މެނޭޖަރ އެވެ", + "session.header.reveal.finder": "Finder ގައި ހާމަކުރުން", + "session.header.reveal.fileExplorer": "File Explorer ގައި ހާމަކުރުން", + "session.header.reveal.containingFolder": "ކޮންޓެއިން ފޯލްޑަރ ހުޅުވާށެވެ", + "session.header.open.app.vscode": "VS Code އެވެ", + "session.header.open.app.cursor": "Cursor އެވެ", + "session.header.open.app.zed": "Zed އެވެ", + "session.header.open.app.textmate": "TextMate އެވެ", + "session.header.open.app.antigravity": "Antigravity އެވެ", + "session.header.open.app.terminal": "ޓާމިނަލް އެވެ", + "session.header.open.app.iterm2": "iTerm2 އެވެ", + "session.header.open.app.ghostty": "Ghostty އެވެ", + "session.header.open.app.warp": "Warp އެވެ", + "session.header.open.app.xcode": "Xcode އެވެ", + "session.header.open.app.androidStudio": "Android Studio އެވެ", + "session.header.open.app.powershell": "PowerShell އެވެ", + "session.header.open.app.sublimeText": "Sublime Text އެވެ", + "status.popover.trigger": "ޙާލަތު", + "status.popover.ariaLabel": "ސަރވަރ ކޮންފިގްރޭޝަންސް", + "status.popover.tab.servers": "ސަރވަރސް", + "status.popover.tab.mcp": "MCP އެވެ", + "status.popover.tab.lsp": "LSP އެވެ", + "status.popover.tab.plugins": "ޕްލަގިންސް އެވެ", + "status.popover.action.manageServers": "ސަރވަރތައް މެނޭޖްކުރުން", + "session.share.popover.title": "ވެބްގައި ޝާއިއުކުރުން", + "session.share.popover.description.shared": + "މި ސެޝަން ވެބްގައި އާންމުކޮށް ކުރިއަށް ގެންދެވޭނެއެވެ. އެއީ ލިންކް އާއި އެކު ކޮންމެ މީހަކަށް ވެސް އެކްސެސް ކުރެވޭނެ އެއްޗެކެވެ.", + "session.share.popover.description.unshared": + "ސެޝަން އާންމުކޮށް ވެބްގައި ހިއްސާކުރުން. އަދި ލިންކް ހުރި ކޮންމެ މީހަކަށްވެސް އެތަނަށް ވަދެވޭނެއެވެ.", + "session.share.action.share": "ޙިއްސާ", + "session.share.action.publish": "ޝާއިއުކުރުން", + "session.share.action.publishing": "ޕަބްލިޝިންގ...", + "session.share.action.unpublish": "އަންޕަބްލިޝް ކުރުން", + "session.share.action.unpublishing": "އަންޕަބްލިޝް ކުރަމުންދާ...", + "session.share.action.view": "މަންޒަރު", + "session.share.copy.copied": "ކޮޕީކޮށްފައި", + "session.share.copy.copyLink": "ކޮޕީ ލިންކް", + "lsp.tooltip.none": "LSP ސަރވަރެއް ނެތެވެ", + "lsp.label.connected": "{{count}} LSP އެވެ", + "prompt.loading": "ލޯޑިންގ ޕްރޮމްޕްޓް...", + "terminal.loading": "ލޯޑިންގ ޓާމިނަލް...", + "terminal.title": "ޓާމިނަލް އެވެ", + "terminal.title.numbered": "ޓާމިނަލް {{number}} އެވެ", + "terminal.close": "ޓާމިނަލް ބަންދުކުރުން", + "terminal.connectionLost.title": "ގުޅުން ގެއްލިއްޖެއެވެ", + "terminal.connectionLost.abnormalClose": "WebSocket އާދަޔާ ހިލާފަށް ބަންދުވެފައި: {{code}}", + "terminal.connectionLost.description": + "ޓާމިނަލް ކަނެކްޝަން މެދުކެނޑުނެވެ. މިއީ ސަރވަރ އަލުން ސްޓާޓް ކުރުމުން ދިމާވެދާނެ ކަމެކެވެ.", + "terminal.connectTicket.csrfError": + "PTY ކަނެކްޓް ޓިކެޓް ރިޖެކްޓް ކޮށްފައިވަނީ އޮރިޖިން ނުވަތަ CSRF ޗެކްތަކުންނެވެ. ސަރވަރ CORS ކޮންފިގް ޗެކްކުރުން.", + "terminal.connectTicket.statusError": "PTY ކަނެކްޓް ޓިކެޓް {{status}} އާއެކު ފެއިލްވެއްޖެއެވެ", + "titlebar.update": "އަޕްޑޭޓް", + "titlebar.updateVersion": "{{version}} އަޕްޑޭޓް ކުރާށެވެ", + "common.closeTab": "ޓެބް ބަންދުކުރުން", + "common.dismiss": "ދުރުކޮށްލުން", + "common.moreCountSuffix": " (+{{count}} އިތުރަށް)", + "common.requestFailed": "ރިކުއެސްޓް ފެއިލްވިއެވެ", + "common.moreOptions": "އިތުރު އިޚްތިޔާރުތަކެކެވެ", + "common.learnMore": "އިތުރަށް ދަސްކުރައްވާށެވެ", + "common.rename": "ނަން ބަދަލުކޮށްލާށެވެ", + "common.export": "އެކްސްޕޯޓް", + "common.reset": "ރީސެޓް ކުރުން", + "common.archive": "އާކައިވް", + "common.delete": "ފުހެލުން", + "common.close": "ލެއްޕުން", + "common.edit": "ބަދަލު ގެނައުން", + "common.loadMore": "އިތުރަށް ލޯޑް ކުރާށެވެ", + "common.key.esc": "ESC އެވެ", + "common.key.ctrl": "Ctrl އެވެ", + "common.key.alt": "Alt އެވެ", + "common.key.shift": "Shift އެވެ", + "common.key.meta": "Meta އެވެ", + "common.key.space": "Space އެވެ", + "common.key.backspace": "Backspace އެވެ", + "common.key.enter": "Enter", + "common.key.tab": "Tab އެވެ", + "common.key.delete": "Delete އެވެ", + "common.key.home": "Home އެވެ", + "common.key.end": "End އެވެ", + "common.key.pageUp": "Page Up އެވެ", + "common.key.pageDown": "Page Down އެވެ", + "common.key.insert": "Insert އެވެ", + "common.unknown": "ނޭނގޭ އެއްޗެއް", + "common.time.justNow": "ހަމަ މިހާރު", + "common.time.minutesAgo.short": "{{count}}m މީގެ ކުރިން", + "common.time.hoursAgo.short": "{{count}}h މީގެ ކުރިން", + "common.time.daysAgo.short": "{{count}}d ކުރިން", + "sidebar.menu.toggle": "ޓޮގްލް މެނޫ", + "sidebar.nav.projectsAndSessions": "ޕްރޮޖެކްޓްތަކާއި ސެޝަންތައް", + "sidebar.settings": "ސެޓިންގސް", + "sidebar.help": "އެހީވުން", + "sidebar.workspaces.enable": "ވޯކްސްޕޭސްތައް އެނެބަލްކުރުން", + "sidebar.workspaces.disable": "ވޯކްސްޕޭސްތައް ޑިސެބަލްކުރުން", + "sidebar.gettingStarted.title": "ފަށަނީ", + "sidebar.gettingStarted.line1": "OpenCode ގައި ހިލޭ މޮޑެލްތައް ހިމެނޭތީ ވަގުތުން ފަށައިގަނެވޭނެ އެވެ.", + "sidebar.gettingStarted.line2": + "މޮޑެލްތައް ބޭނުން ކުރުމަށް އެއްވެސް ޕްރޮވައިޑަރެއް ގުޅުވާލުން، inc. Claude، GPT، Gemini ފަދަ ކަންކަމެވެ.", + "sidebar.project.recentSessions": "ފަހުގެ ސެޝަންތަކެވެ", + "sidebar.project.viewAllSessions": "ހުރިހާ ސެޝަންތަކެއް ބަލާށެވެ", + "sidebar.project.clearNotifications": "ނޮޓިފިކޭޝަންތައް ސާފުކުރުން", + "sidebar.empty.title": "އެއްވެސް މަޝްރޫއެއް ނުހުޅުވޭ", + "sidebar.empty.description": "ފަށަން ޕްރޮޖެކްޓެއް ހުޅުވާށެވެ", + "debugBar.ariaLabel": "ޑިވެލޮޕްމަންޓް ޕާފޯމަންސް ޑައިގްނޯސްޓިކްސް", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV އެވެ", + "debugBar.nav.tip": + "އެންމެ ފަހުން ފުރިހަމަ ކުރެވުނު ރޫޓް ޓްރާންސިޝަން ސެޝަން ޕޭޖެއްގައި އަތްލުމުން، ރައުޓަރ ފެށުނީއްސުރެ ސެޓްލްވުމަށްފަހު ފުރަތަމަ ކުލަ ޖެހުމާ ހަމައަށް މިންކުރެވެއެވެ.", + "debugBar.fps.label": "FPS އެވެ", + "debugBar.fps.tip": "ފަހު 5 ސިކުންތުގެ ތެރޭގައި ސިކުންތަކަށް ރޯލް ފްރޭމްތައް.", + "debugBar.frame.label": "FRAME އެވެ", + "debugBar.frame.tip": "އެންމެ ގޯސް ފްރޭމް ޓައިމް ފާއިތުވި 5 ސިކުންތުގެ ތެރޭގައި.", + "debugBar.jank.label": "JANK އެވެ", + "debugBar.jank.tip": "ފަހު 5 ސިކުންތުގައި 32ms އަށް ވުރެ ގިނަ ވަގުތު ފްރޭމް ކުރެއެވެ.", + "debugBar.long.label": "LONG އެވެ", + "debugBar.long.tip": + "އެންމެ ފަހު 5 ސިކުންތުގައި ބްލޮކް ކުރެވިފައިވާ ވަގުތާއި ލޯންގް-ޓާސްކް ކައުންޓް. މެކްސް ޓާސްކް: {{max}}.", + "debugBar.delay.label": "DELAY އެވެ", + "debugBar.delay.tip": "އެންމެ ގޯސްކޮށް ފެނުނު އިންޕުޓް ޑިލޭ ވޭތުވެދިޔަ 5 ސިކުންތުގެ ތެރޭގައި.", + "debugBar.inp.label": "INP އެވެ", + "debugBar.inp.tip": + "ފާއިތުވި 5 ސިކުންތުގެ ތެރޭގައި ގާތްގަނޑަކަށް މުޢާމަލާތްކުރާ މުއްދަތު. މިއީ INP ކަހަލަ އެއްޗެއް، ރަސްމީ ވެބް ވައިޓަލްސް INP އެއް ނޫނެވެ.", + "debugBar.cls.label": "CLS އެވެ", + "debugBar.cls.tip": "މިހާރު ބޭނުންކުރާ އެޕް ލައިފްޓައިމް އަށް ކިއުމިއުލެޓިވް ލޭއައުޓް ޝިފްޓް.", + "debugBar.mem.label": "MEM އެވެ", + "debugBar.mem.tipUnavailable": "ބޭނުންކޮށްފައިވަނީ ޖޭއެސް ހީޕް އާއި ހީޕް ލިމިޓެވެ. Chromium އެކަނި.", + "debugBar.mem.tip": "ބޭނުންކޮށްފައިވަނީ ޖޭއެސް ހީޕް އާއި ހީޕް ލިމިޓެވެ. {{used}} ގެ {{limit}} އެވެ.", + "debugBar.focus.label": "FOCUS އެވެ", + "debugBar.focus.tip": "ފޯސް ފޯކަސް ސްޓައިލްސް ހުރިހާ އިންޓަރެކްޓިވް އެލިމެންޓްތަކަށް", + "debugBar.focus.on": "ON އެވެ", + "debugBar.focus.off": "OFF އެވެ", + "debugBar.direction.label": "ޑރ", + "debugBar.direction.tip": "ހޮވާފައިވާ ބަހަށް ބަދަލެއް ނުގެނެސް ފުލް އެޕް ލޭއައުޓް ޑިރެކްޝަން ފޯސް ކުރާށެވެ", + "debugBar.direction.ltr": "LTR އެވެ", + "debugBar.direction.rtl": "RTL އެވެ", + "app.name.desktop": "OpenCode Desktop އެވެ", + "settings.section.desktop": "ޑެސްކްޓޮޕް", + "settings.section.server": "ސަރވަރ އެވެ", + "settings.tab.general": "އާންމު", + "settings.tab.shortcuts": "ޝޯޓްކާޓްސް", + "settings.desktop.section.wsl": "WSL އެވެ", + "settings.desktop.wsl.title": "WSL އިންޓަގްރޭޝަން", + "settings.desktop.wsl.description": "Windows ގައި WSL ގެ ތެރޭގައި OpenCode ސަރވަރ ހިންގާށެވެ.", + "settings.general.section.appearance": "މަލަމަތި", + "settings.general.section.advanced": "އެޑްވާންސްޑް", + "settings.general.section.notifications": "ސިސްޓަމް ނޮޓިފިކޭޝަންސް", + "settings.general.section.updates": "އަޕްޑޭޓްސް", + "settings.general.section.sounds": "އަޑު އިފެކްޓްސް", + "settings.general.section.feed": "ކާންދިނުން", + "settings.general.section.display": "ޑިސްޕްލޭ", + "settings.general.row.language.title": "ބަސް", + "settings.general.row.language.description": "OpenCode އަށް ޑިސްޕްލޭ ލޭންގުއޭޖް ބަދަލުކުރުން", + "settings.general.row.shell.title": "ޓާމިނަލް ޝެލް އެވެ", + "settings.general.row.shell.description": "ޓާމިނަލް އާއި އޭޖެންޓް ޓޫލްސް އިން ބޭނުންކުރާ ޝެލް", + "settings.general.row.shell.autoDefault": "އޮޓޯ (ޑިފޯލްޓް)", + "settings.general.row.shell.terminalOnly": "ޓާމިނަލް އެކަނި", + "settings.general.row.appearance.title": "މަލަމަތި", + "settings.general.row.appearance.description": "ޑިވައިސްގައި OpenCode ފެންނަ ގޮތް ކަސްޓަމައިޒް ކުރާށެވެ", + "settings.general.row.colorScheme.title": "ކުލަ ސްކީމް", + "settings.general.row.colorScheme.description": + "OpenCode އިން ސިސްޓަމް، ލައިޓް، ނުވަތަ ޑާކް ތީމް އަށް ތަބާވާތޯ ހޮވާށެވެ", + "settings.general.row.theme.title": "ތީމް", + "settings.general.row.theme.description": "OpenCode ތީމް ކުރެވިފައިވާ ގޮތް ކަސްޓަމައިޒް ކުރުން.", + "settings.general.row.font.title": "ކޯޑް ފޮންޓެވެ", + "settings.general.row.font.description": "ކޯޑް ބްލޮކްތަކުގައި ބޭނުންކުރާ ފޮންޓް ކަސްޓަމައިޒް ކުރުން", + "settings.general.row.terminalFont.title": "ޓާމިނަލް ފޮންޓެވެ", + "settings.general.row.terminalFont.description": "ޓާމިނަލްގައި ބޭނުންކުރާ ފޮންޓް ކަސްޓަމައިޒް ކުރުން", + "settings.general.row.uiFont.title": "UI ފޮންޓް", + "settings.general.row.uiFont.description": "މުޅި އިންޓަރފޭސްގައި ބޭނުންކުރާ ފޮންޓް ކަސްޓަމައިޒް ކުރުން", + "settings.general.row.followup.title": "ފޮލޯއަޕް ސުލޫކު", + "settings.general.row.followup.description": + "ފޮލޯއަޕް ޕްރޮމްޕްޓްސް ވަގުތުން ސްޓިއަރ ކުރުން ނުވަތަ ކިއުއެއްގައި މަޑުކުރުންތޯ ހޮވާށެވެ", + "settings.general.row.followup.option.queue": "ކިއު", + "settings.general.row.followup.option.steer": "ސްޓީއާ އެވެ", + "settings.general.row.showFileTree.title": "ފައިލް ގަހެވެ", + "settings.general.row.showFileTree.description": "ސެޝަންތަކުގައި ފައިލް ޓްރީ ޕެނަލް ދައްކާށެވެ", + "settings.general.row.showNavigation.title": "ނޭވިގޭޝަން ކޮންޓްރޯލްތައް", + "settings.general.row.showNavigation.description": "ޑެސްކްޓޮޕް ޓައިޓަލް ބާރ ގައި ފަހަތާއި ކުރިއަށް ބަޓަން ދައްކާށެވެ", + "settings.general.row.showSearch.title": "ކޮމާންޑް ޕެލެޓް", + "settings.general.row.showSearch.description": "ޓައިޓަލް ބާރ އިން ސަރޗް އެންޑް ކޮމާންޑް ޕެލެޓް ބަޓަން ދައްކާށެވެ", + "settings.general.row.showTerminal.title": "ޓާމިނަލް އެވެ", + "settings.general.row.showTerminal.description": "ޑެސްކްޓޮޕް ޓައިޓަލް ބާރ އިން ޓާރމިނަލް ބަޓަން ދައްކާށެވެ", + "settings.general.row.showStatus.title": "ސަރވަރ ސްޓޭޓަސް", + "settings.general.row.showStatus.description": "ޓައިޓަލް ބާރ ގައި ސަރވަރ ސްޓޭޓަސް ބަޓަން ދައްކާށެވެ", + "settings.general.row.mobileTitlebarBottom.title": "ތިރީގައި ދަތުރުކުރުން", + "settings.general.row.mobileTitlebarBottom.description": + "މޯބައިލްގައި ސްކްރީންގެ ތިރީގައި ޓައިޓަލް ބާރ އާއި ސެޝަން ޓެބްތައް ބަހައްޓާށެވެ", + "settings.general.row.showCustomAgents.title": "ޝޯ އޭޖެންޓް", + "settings.general.row.showCustomAgents.description": + "ކޮމްޕޯސަރުގައި ތިބި އޭޖެންޓުންގެ މެދުގައި ބަދަލުވުން. ފޮރުވާއިރު، ޑިފޯލްޓް ވާނީ ބިލްޑް އޭޖެންޓަށެވެ.", + "settings.general.row.reasoningSummaries.title": "ރިޒަނިންގ ސުމާރީސް ދައްކާށެވެ", + "settings.general.row.reasoningSummaries.description": "ޓައިމްލައިންގައި މޮޑެލް ރިޒަނިންގ ސުމާރީތައް ދައްކާލުން", + "settings.general.row.shellToolPartsExpanded.title": "ޝެލް ޓޫލް ބައިތައް ފުޅާކުރުން", + "settings.general.row.shellToolPartsExpanded.description": + "ޓައިމްލައިންގައި ޑިފޯލްޓްކޮށް ފުޅާކޮށްފައިވާ ޝެލް ޓޫލް ބައިތައް ދައްކާށެވެ", + "settings.general.row.editToolPartsExpanded.title": "އެޑިޓް ޓޫލްގެ ބައިތައް ފުޅާކުރުން", + "settings.general.row.editToolPartsExpanded.description": + "ޓައިމްލައިންގައި ޑިފޯލްޓްކޮށް ފުޅާކޮށްފައިވާ އެޑިޓް، ރައިޓް، އަދި ޕެޗް ޓޫލް ބައިތައް ދައްކާށެވެ", + "settings.general.row.newInterface.title": "އާ ލޭއައުޓެއް", + "settings.general.row.newInterface.badge": "އާ", + "settings.general.row.newInterface.description": + "އާ ޓެބްތަކާއި ހޯމް ލޭއައުޓް ބޭނުން ކުރާށެވެ. ލޭއައުޓްތަކުގެ މެދުގައި ވަކި މުއްދަތަކަށް ބަދަލުވުން.", + "settings.general.row.newInterfaceNotice.title": "މިހާރު ތިޔަ ބޭނުން ކުރަނީ އާ ލޭއައުޓެކެވެ", + "settings.general.row.newInterfaceNotice.description": "ކުރީގެ ލޭއައުޓް މިހާރު ނުލިބެއެވެ", + "settings.general.row.newInterfaceNotice.dismiss": "ދުރުކޮށްލުން", + "settings.general.row.pinchZoom.title": "ޒޫމް ކުރަން ޕިންޗް ކޮށްލާށެވެ", + "settings.general.row.pinchZoom.description": + "ޓްރެކްޕެޑް ޕިންޗް އަދި Ctrl-ސްކްރޯލް ޖެޝަރސް ޒޫމް ކުރުމަށް ހުއްދަ ދިނުން", + "settings.general.row.wayland.title": "ނެޓިވް Wayland ބޭނުން ކުރާށެވެ", + "settings.general.row.wayland.description": "Wayland ގައި X11 ފޯލްބެކް ޑިސެބަލް ކުރާށެވެ. ރީސްޓާޓް ކުރަން ޖެހެއެވެ.", + "settings.general.row.wayland.tooltip": + "މިކްސްޑް ރިފްރެޝް-ރޭޓް މޮނިޓަރުތަކާއެކު Linux ގައި ނެޓިވް Wayland އަށް ވުރެ ސްޓޭބަލް ވެދާނެއެވެ.", + "settings.general.row.releaseNotes.title": "ރިލީޒް ނޯޓްސް", + "settings.general.row.releaseNotes.description": "އަޕްޑޭޓްތަކަށްފަހު އައު ޕޮޕްއަޕްތައް ދައްކާށެވެ", + "settings.updates.row.startup.title": "ސްޓާޓްއަޕް ކުރުމުގައި އަޕްޑޭޓްތައް ހުރިތޯ ބަލާށެވެ", + "settings.updates.row.startup.description": "OpenCode ލޯންޗް ކުރާއިރު އޮޓޮމެޓިކުން އަޕްޑޭޓްތައް ހުރިތޯ ބަލާށެވެ", + "settings.updates.row.check.title": "އަޕްޑޭޓްތައް ހުރިތޯ ބަލާށެވެ", + "settings.updates.row.check.description": "އަޕްޑޭޓްތައް މެނުއަލްކޮށް ޗެކްކޮށް، ލިބޭނަމަ އިންސްޓޯލް ކުރާށެވެ", + "settings.updates.action.checkNow": "މިހާރު ޗެކް ކޮށްލައްވާ", + "settings.updates.action.checking": "ޗެކް ކުރަމުން...", + "settings.updates.action.downloading": "ޑައުންލޯޑް ކުރަމުން...", + "settings.updates.action.installing": "އިންސްޓޯލް ކުރަމުން...", + "settings.updates.toast.latest.title": "ޔޫ އާ އަޕް ޓު ޑޭޓް", + "settings.updates.toast.latest.description": "ތިޔަ ހިންގަނީ OpenCode ގެ އެންމެ ފަހުގެ ވަރޝަން އެވެ.", + "sound.option.none": "އެއްޗެއްނޫން", + "sound.option.alert01": "އެލާޓް 01", + "sound.option.alert02": "އެލާޓް 02", + "sound.option.alert03": "އެލާޓް 03", + "sound.option.alert04": "އެލާޓް 04", + "sound.option.alert05": "އެލާޓް 05", + "sound.option.alert06": "އެލާޓް 06", + "sound.option.alert07": "އެލާޓް 07", + "sound.option.alert08": "އެލާޓް 08", + "sound.option.alert09": "އެލާޓް 09", + "sound.option.alert10": "އެލާޓް 10", + "sound.option.bipbop01": "ބިޕް-ބޮޕް 01", + "sound.option.bipbop02": "ބިޕް-ބޮޕް 02", + "sound.option.bipbop03": "ބިޕް-ބޮޕް 03", + "sound.option.bipbop04": "ބިޕް-ބޮޕް 04", + "sound.option.bipbop05": "ބިޕް-ބޮޕް 05", + "sound.option.bipbop06": "ބިޕް-ބޮޕް 06", + "sound.option.bipbop07": "ބިޕް-ބޮޕް 07", + "sound.option.bipbop08": "ބިޕް-ބޮޕް 08", + "sound.option.bipbop09": "ބިޕް-ބޮޕް 09", + "sound.option.bipbop10": "ބިޕް-ބޮޕް 10", + "sound.option.staplebops01": "ސްޓޭޕްލްބޮޕްސް 01", + "sound.option.staplebops02": "ސްޓޭޕްލްބޮޕްސް 02", + "sound.option.staplebops03": "ސްޓޭޕްލްބޮޕްސް 03", + "sound.option.staplebops04": "ސްޓޭޕްލްބޮޕްސް 04", + "sound.option.staplebops05": "ސްޓޭޕްލްބޮޕްސް 05", + "sound.option.staplebops06": "ސްޓޭޕްލްބޮޕްސް 06", + "sound.option.staplebops07": "ސްޓޭޕްލްބޮޕްސް 07", + "sound.option.nope01": "ނޫން 01", + "sound.option.nope02": "ނޫން 02", + "sound.option.nope03": "ނޫން 03", + "sound.option.nope04": "ނޫން 04", + "sound.option.nope05": "ނޫން 05", + "sound.option.nope06": "ނޫން 06", + "sound.option.nope07": "ނޫން 07", + "sound.option.nope08": "ނޫން 08", + "sound.option.nope09": "ނޫން 09", + "sound.option.nope10": "ނޫން 10", + "sound.option.nope11": "ނޫން 11", + "sound.option.nope12": "ނޫން 12", + "sound.option.yup01": "ޔޫޕް 01", + "sound.option.yup02": "ޔޫޕް 02", + "sound.option.yup03": "ޔޫޕް 03", + "sound.option.yup04": "ޔޫޕް 04", + "sound.option.yup05": "ޔޫޕް 05", + "sound.option.yup06": "ޔޫޕް 06", + "settings.general.notifications.agent.title": "އޭޖެންޓު", + "settings.general.notifications.agent.description": + "އޭޖެންޓް ފުރިހަމަވެފައި ނުވަތަ ސަމާލުކަން ބޭނުންވާ ވަގުތުތަކުގައި ސިސްޓަމް ނޮޓިފިކޭޝަން ދައްކާށެވެ", + "settings.general.notifications.permissions.title": "ހުއްދަތައް", + "settings.general.notifications.permissions.description": + "ހުއްދައެއް ބޭނުންވާ ވަގުތު ސިސްޓަމް ނޮޓިފިކޭޝަން ދައްކާށެވެ", + "settings.general.notifications.errors.title": "ގޯސްތައް", + "settings.general.notifications.errors.description": "ގޯހެއް ދިމާވުމުން ސިސްޓަމް ނޮޓިފިކޭޝަން ދައްކާށެވެ", + "settings.general.sounds.agent.title": "އޭޖެންޓު", + "settings.general.sounds.agent.description": + "އޭޖެންޓް ފުރިހަމަވެފައި ނުވަތަ ސަމާލުކަން ބޭނުންވާ ވަގުތުތަކުގައި އަޑު ކުޅެން", + "settings.general.sounds.permissions.title": "ހުއްދަތައް", + "settings.general.sounds.permissions.description": "ހުއްދައެއް ބޭނުންވާ ވަގުތުތަކުގައި އަޑު ކުޅެން", + "settings.general.sounds.errors.title": "ގޯސްތައް", + "settings.general.sounds.errors.description": "ގޯހެއް ދިމާވުމުން އަޑު ކުޅެން", + "settings.shortcuts.title": "ކީބޯޑް ޝޯޓްކަޓްސް", + "settings.shortcuts.reset.button": "ޑިފޯލްޓްތަކަށް ރީސެޓްކުރުން", + "settings.shortcuts.reset.toast.title": "ޝޯޓްކަޓްސް ރީސެޓް ކުރުން", + "settings.shortcuts.reset.toast.description": "ކީބޯޑް ޝޯޓްކަޓްތައް ޑިފޯލްޓްތަކަށް ރީސެޓްކޮށްފައިވެއެވެ.", + "settings.shortcuts.conflict.title": "މިހާރުވެސް ބޭނުން ކުރަމުންދާ ޝޯޓްކަޓެވެ", + "settings.shortcuts.conflict.description": "{{keybind}} މިހާރުވެސް ޙަވާލުކުރެވިފައިވަނީ {{titles}} އަށެވެ.", + "settings.shortcuts.unassigned": "ޙަވާލުނުކުރެވިގެންނެވެ", + "settings.shortcuts.pressKeys": "ކީސް އަށް ފިތާލާށެވެ", + "settings.shortcuts.search.placeholder": "ސަރޗް ޝޯޓްކަޓްސް", + "settings.shortcuts.search.empty": "ޝޯޓްކަޓެއް ނުފެނެއެވެ", + "settings.shortcuts.group.general": "އާންމު", + "settings.shortcuts.group.session": "ސެޝަން", + "settings.shortcuts.group.navigation": "ނޭވިގޭޝަން", + "settings.shortcuts.group.modelAndAgent": "މޮޑެލް އަދި އޭޖެންޓެވެ", + "settings.shortcuts.group.terminal": "ޓާމިނަލް އެވެ", + "settings.shortcuts.group.prompt": "ޕްރޮމްޕްޓް", + "settings.providers.title": "ޕްރޮވައިޑަރުން", + "settings.providers.description": "ޕްރޮވައިޑަރ ސެޓިންގސް މިތަނުގައި ކޮންފިގްރޭޓް ކުރެވޭނެއެވެ.", + "settings.providers.section.connected": "ގުޅިފައިވާ ޕްރޮވައިޑަރުންނެވެ", + "settings.providers.connected.empty": "ގުޅިފައިވާ ޕްރޮވައިޑަރުންތަކެއް ނެތެވެ", + "settings.providers.connected.environmentDescription": "ތިބާގެ މާހައުލުގެ ވެރިއޭބަލްތަކުން ގުޅިފައިވެއެވެ", + "settings.providers.section.popular": "މަޝްހޫރު ޕްރޮވައިޑަރުންނެވެ", + "settings.providers.custom.description": "ބޭސް URL އިން OpenAI އާއި އެއްގޮތްވާ ޕްރޮވައިޑަރެއް އިތުރުކުރުން.", + "settings.providers.tag.environment": "މާޙައުލު", + "settings.providers.tag.config": "ކޮންފިގް", + "settings.providers.tag.custom": "ސަޤާފަތް", + "settings.providers.tag.other": "އެހެން", + "settings.models.title": "މޮޑެލްތަކެވެ", + "settings.models.description": "މޮޑެލް ސެޓިންގސް މިތަނުގައި ކޮންފިގްރޭޓް ކުރެވޭނެއެވެ.", + "settings.agents.title": "އޭޖެންޓުންނެވެ", + "settings.agents.description": "އޭޖެންޓް ސެޓިންގސް މިތަނުގައި ކޮންފިގްރޭޓް ކުރެވޭނެއެވެ.", + "settings.commands.title": "އަމުރުތަކެވެ", + "settings.commands.description": "ކޮމާންޑް ސެޓިންގސް މިތަނުގައި ކޮންފިގްރޭޓް ކުރެވޭނެއެވެ.", + "settings.mcp.title": "MCP އެވެ", + "settings.mcp.description": "MCP ސެޓިންގސް މިތަނުގައި ކޮންފިގްރޭޓް ކުރެވޭނެއެވެ.", + "settings.permissions.title": "ހުއްދަތައް", + "settings.permissions.description": "ސަރވަރ އަށް ޑިފޯލްޓްކޮށް ބޭނުންކުރެވޭނީ ކޮން ޓޫލްތަކެއްކަން ކޮންޓްރޯލްކުރުން.", + "settings.permissions.section.tools": "ސާމާނުތައް", + "settings.permissions.toast.updateFailed.title": "ހުއްދަތައް އަޕްޑޭޓް ނުކުރެވުނެވެ", + "settings.permissions.action.allow": "ހުއްދަ ދިނުން", + "settings.permissions.action.ask": "އެހުން", + "settings.permissions.action.deny": "ދޮގުކުރުން", + "settings.permissions.tool.read.title": "ކިޔުން", + "settings.permissions.tool.read.description": "ފައިލެއް ކިޔުން (ފައިލް މަގާއި އެއްގޮތްވާ)", + "settings.permissions.tool.edit.title": "ބަދަލު ގެނައުން", + "settings.permissions.tool.edit.description": + "އެޑިޓްކުރުމާއި، ލިޔުންތަކާއި، ޕެޗްތައް ހިމެނޭ ގޮތަށް ފައިލްތައް ބަދަލުކުރުން", + "settings.permissions.tool.glob.title": "ގްލޯބް އެވެ", + "settings.permissions.tool.glob.description": "ގްލޯބް ޕެޓަރންސް ބޭނުންކޮށްގެން ފައިލްތައް މެޗްކުރުން", + "settings.permissions.tool.grep.title": "ގްރެޕް އެވެ", + "settings.permissions.tool.grep.description": "ރެގިއުލާ އެކްސްޕްރެޝަންސް ބޭނުންކޮށްގެން ފައިލް ކޮންޓެންޓްސް ހޯދުން", + "settings.permissions.tool.list.title": "ލިސްޓް", + "settings.permissions.tool.list.description": "ޑައިރެކްޓަރީއެއްގެ ތެރޭގައި ފައިލްތައް ލިސްޓްކުރުން", + "settings.permissions.tool.bash.title": "ބާޝް", + "settings.permissions.tool.bash.description": "ޝެލް ކޮމާންޑްތައް ހިންގާށެވެ", + "settings.permissions.tool.task.title": "މަސައްކަތެއް", + "settings.permissions.tool.task.description": "ސަބް އޭޖެންޓުން ލޯންޗުކުރުން", + "settings.permissions.tool.skill.title": "ހުނަރު", + "settings.permissions.tool.skill.description": "ނަމުން ހުނަރެއް ލޯޑް ކުރާށެވެ", + "settings.permissions.tool.lsp.title": "LSP އެވެ", + "settings.permissions.tool.lsp.description": "ލޭންގުއޭޖް ސަރވަރ ކިއުއަރީތައް ހިންގުން", + "settings.permissions.tool.todowrite.title": "ޓޯޑޯ ރައިޓް", + "settings.permissions.tool.todowrite.description": "ޓޯޑޯ ލިސްޓް އަޕްޑޭޓް ކުރާށެވެ", + "settings.permissions.tool.webfetch.title": "ވެބް ފެޗް", + "settings.permissions.tool.webfetch.description": "URL އިން ކޮންޓެންޓް ހޯދުން", + "settings.permissions.tool.websearch.title": "ވެބް ސަރޗް", + "settings.permissions.tool.websearch.description": "ވެބްގައި ހޯދާށެވެ", + "settings.permissions.tool.external_directory.title": "އެކްސްޓަރނަލް ޑައިރެކްޓަރީ", + "settings.permissions.tool.external_directory.description": + "ޕްރޮޖެކްޓް ޑައިރެކްޓަރީން ބޭރުގައި ހުންނަ ފައިލްތަކަށް ވަދެވޭނެއެވެ", + "settings.permissions.tool.doom_loop.title": "ޑޫމް ލޫޕް އެވެ", + "settings.permissions.tool.doom_loop.description": "އެއްގޮތް އިންޕުޓެއް އެކުލެވޭ ތަކުރާރުކޮށް ޓޫލް ކޯލްތައް ހޯދުން", + "session.delete.failed.title": "ސެޝަން ޑިލީޓް ކުރަން ނާކާމިޔާބުވިއެވެ", + "session.delete.title": "ސެޝަން ފުހެލުން", + "session.delete.confirm": 'ސެޝަން "{{name}}" ފުހެލަންތަ؟', + "session.delete.button": "ސެޝަން ފުހެލުން", + "workspace.new": "އާ ވޯކްސްޕޭސް", + "workspace.type.local": "ލޯކަލް", + "workspace.type.sandbox": "ސޭންޑްބޮކްސް އެވެ", + "workspace.create.failed.title": "ވޯކްސްޕޭސް އުފެއްދުމަށް ނާކާމިޔާބުވިއެވެ", + "workspace.delete.failed.title": "ވޯކްސްޕޭސް ޑިލީޓް ނުކުރެވުނެވެ", + "workspace.resetting.title": "ވޯކްސްޕޭސް ރީސެޓްކުރުން", + "workspace.resetting.description": "މިކަމަށް މިނިޓެއް ނަގާފާނެއެވެ.", + "workspace.reset.failed.title": "ވޯކްސްޕޭސް ރީސެޓް ނުކުރެވުނެވެ", + "workspace.reset.success.title": "ވޯކްސްޕޭސް ރީސެޓް ކުރުން", + "workspace.reset.success.description": "ވޯކްސްޕޭސް މިހާރު ޑިފޯލްޓް ބްރާންޗާ ދިމާވެއެވެ.", + "workspace.error.stillPreparing": "ވޯކްސްޕޭސް އަދިވެސް ދަނީ ތައްޔާރުވަމުންނެވެ", + "workspace.status.checking": "އެއް ނުކުރެވި ހުރި ބަދަލުތައް ޗެކް ކުރަމުން...", + "workspace.status.error": "ގިޓް ސްޓޭޓަސް ޔަގީން ނުކުރެވެއެވެ.", + "workspace.status.clean": "އެއްނުވާ ބަދަލެއް ފާހަގަކުރެވިފައެއް ނުވެއެވެ.", + "workspace.status.dirty": "މި ވޯކްސްޕޭސްގައި ފާހަގަ ނުކުރެވި ހުރި ބަދަލުތައް.", + "workspace.delete.title": "ވޯކްސްޕޭސް ފުހެލުން", + "workspace.delete.confirm": 'ވޯކްސްޕޭސް "{{name}}" ފުހެލަންތަ؟', + "workspace.delete.button": "ވޯކްސްޕޭސް ފުހެލުން", + "workspace.reset.title": "ވޯކްސްޕޭސް ރީސެޓްކުރުން", + "workspace.reset.confirm": 'ވޯކްސްޕޭސް "{{name}}" ރީސެޓްކުރުން؟', + "workspace.reset.button": "ވޯކްސްޕޭސް ރީސެޓްކުރުން", + "workspace.reset.archived.none": "އެއްވެސް އެކްޓިވް ސެޝަނެއް އަރުޝީފް ނުކުރެވޭނެއެވެ.", + "workspace.reset.archived.one": "1 ސެޝަން އަރުޝީފް ކުރެވޭނެއެވެ.", + "workspace.reset.archived.many": "{{count}} ސެޝަންތައް އަރުޝީފް ކުރެވޭނެއެވެ.", + "workspace.reset.note": "މިއީ ޑިފޯލްޓް ބްރާންޗާ އެއްގޮތްވާ ގޮތަށް ވޯކްސްޕޭސް ރީސެޓް ކުރާނެ ކަމެކެވެ.", + "dialog.usageExceeded.dontShowAgain": "އަލުން ނުދައްކާ", +} diff --git a/packages/app/src/i18n/dz.ts b/packages/app/src/i18n/dz.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b2ad46ca3045256bfd323a48ef89d18adcbc4f7 --- /dev/null +++ b/packages/app/src/i18n/dz.ts @@ -0,0 +1,1181 @@ +export const dict: Record = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ཡིག༌སྣོད", + "desktop.menu.edit": "ཞུན༌དག", + "desktop.menu.view": "བསམ༌འཆར", + "desktop.menu.go": "འགྱོ", + "desktop.menu.window": "སྒོང༌སྒྲིག", + "desktop.menu.help": "ཆ་རོགས་འབད་ནི", + "desktop.menu.checkForUpdates": "དུས་མཐུན་ཚུ་གི་དོན་ལུ་ཞིབ་དཔྱད་འབད།", + "desktop.menu.settings": "སྒྲིག་སྟངས་ཚུ།", + "desktop.menu.reloadWebview": "ཡང་བསྐྱར་མངོན་གསལ་ Webview།", + "desktop.menu.restart": "ལོག་འགོ་བཙུགས།", + "desktop.menu.exportLogs": "ཕྱིར་འདྲེན་དྲན་ཐོ་...", + "desktop.menu.newSession": "ལཱ་ཡུན་གསརཔ།", + "desktop.menu.openProject": "ལས་འགུལ་ཁ་ཕྱེ།...", + "desktop.menu.newWindow": "སྒོ་སྒྲིག་གསརཔ།", + "desktop.menu.closeWindow": "སྒོ་སྒྲིག་ཁ་བསྡམས།", + "desktop.menu.undo": "འབད་བཤོལ་འབད།", + "desktop.menu.redo": "ཡང་བསྐྱར་འབད།", + "desktop.menu.cut": "བཏོག་ནི", + "desktop.menu.copy": "འདྲ་བཤུས།", + "desktop.menu.paste": "སྦྱར།", + "desktop.menu.delete": "བཏོན༌གཏང༌བ", + "desktop.menu.selectAll": "ཆ་མཉམ་སེལ་འཐུ་འབད།", + "desktop.menu.toggleSidebar": "ཟུར་ཁའི་ཕྲ་རིང་སོར་སྟོན་འབད།", + "desktop.menu.toggleTerminal": "ཊར་མི་ནཱལ་སོར་སྟོན་འབད།", + "desktop.menu.toggleFileTree": "ཡིག་སྣོད་ཤིང་སོར་སྟོན་འབད།", + "desktop.menu.reload": "ཡང་བསྐྱར་མངོན་གསལ།", + "desktop.menu.toggleDeveloperTools": "གོང་འཕེལ་གཏང་མིའི་ལག་ཆས་ཚུ་ སོར་སྟོན་འབད།", + "desktop.menu.actualSize": "ཚད་ངོ་མ།", + "desktop.menu.zoomIn": "རྒྱས་ཟུམ།", + "desktop.menu.zoomOut": "རྒྱས་ཟུམ།", + "desktop.menu.toggleFullScreen": "གསལ་གཞི་ཆ་ཚང་སོར་བསྒྱུར་འབད།", + "desktop.menu.back": "ལོག་འོང་ནི", + "desktop.menu.forward": "གྲོགས༌རམ༌འབད༌ནི", + "desktop.menu.previousSession": "ཧེ་མའི་ལཱ་ཡུན།", + "desktop.menu.nextSession": "ལཱ་ཡུན་ཤུལ་མམ།", + "desktop.menu.previousProject": "ལས་གཞི་སྔོན་མ།", + "desktop.menu.nextProject": "ལས་གཞི་རྗེས་མ།", + "desktop.menu.minimize": "ཉུང་ཉུང་བཟོ།", + "desktop.menu.maximize": "ཆེར་བསྐྱེད་འབད།", + "desktop.menu.documentation": "OpenCode ཡིག་ཆ།", + "desktop.menu.supportForum": "རྒྱབ་སྐྱོར་གླེང་སྟེགས།", + "desktop.menu.shareFeedback": "བསམ་འཆར་བརྗེ་རེས།", + "desktop.menu.reportBug": "རྐྱེན་ཅིག་སྙན་ཞུ་འབད།", + "desktop.menu.ariaLabel": "OpenCode དཀར་ཆག།", + "desktop.updater.dialog.checkFailed.message": "དུས་མཐུན་ཞིབ་དཔྱད་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "desktop.updater.dialog.checkFailed.title": "དུས་མཐུན་འཛོལ་བ།", + "desktop.updater.dialog.upToDate.message": "ཁྱོད་དུས་དང་བསྟུན།", + "desktop.updater.dialog.upToDate.title": "དུས་མཐུན་མེད།", + "desktop.updater.dialog.ready.message": "དུས་མཐུན་ {{version}} ཕབ་ལེན་འབད་ཡོདཔ། ད་ལྟོ་ལོག་འགོ་བཙུགས?", + "desktop.updater.dialog.ready.title": "དུས་མཐུན་བཟོ་ནི་གྲ་སྒྲིག།", + "desktop.updater.dialog.restart": "ལོག་འགོ་བཙུགས།", + "desktop.updater.dialog.later": "ཤུལ་ལས", + "desktop.recovery.action.relaunch": "བསྐྱར་འགོ་བཙུགས།", + "desktop.recovery.action.exportLogs": "ཕྱིར་འདྲེན་དྲན་དེབ་ཚུ།", + "desktop.recovery.action.keepWaiting": "བསྒུག་སྡོད།", + "desktop.recovery.action.quit": "བཞག༌ནི", + "desktop.recovery.loadFailed": "OpenCode མངོན་གསལ་འབད་ནི་ལུ་འཐུས་ཤོར་བྱུང་ཡོདཔ།", + "desktop.recovery.terminated": "OpenCode སྒོ་སྒྲིག་འདི་ རེ་བ་མེད་པར་ མཇུག་བསྡུ་ཡོདཔ།", + "desktop.recovery.unresponsive": "OpenCodeགིས་ ལན་གསལ་འབད་མི་བཏུབ།", + "desktop.recovery.unresponsive.detail": + "ཁྱོད་ཀྱིས་གློག་རིམ་འདི་ལོག་སྟེ་འགོ་བཙུགས་ཚུགས། དྲན་ཐོ་ཚུ་ཁ་ཕྱེ་ཚུགས། ཡང་ན་བསྒུག་སྡོད་ཚུགས།", + "desktop.recovery.loadFailed.detail": "སྒོ་སྒྲིག་: {{window}}\nURL: {{url}}\nའཛོལ་བ་: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "སྒོ་སྒྲིག་: {{window}}\nརྒྱུ་མཚན་: {{reason}}\nཨང་རྟགས་: {{code}}", + "desktop.recovery.unknown": "<མ་ཤེས་པ>", + "desktop.dialog.chooseFolder": "སྣོད་འཛིན་ཅིག་གདམ་ཁ་རྐྱབས།", + "desktop.dialog.chooseFile": "ཡིག་སྣོད་གདམ་ཁ་རྐྱབས།", + "desktop.dialog.saveFile": "ཡིག་སྣོད་སྲུངས།", + "desktop.dialog.files": "ཡིག་སྣོད་ཚུ།", + "desktop.server.local": "ཉེ་གནས་སར་བར།", + "desktop.wsl.error.windowsOnly": "WSLའདི་ Windowsནང་རྐྱངམ་ཅིག་འཐོབ་ཚུགས།", + "desktop.wsl.error.unavailable": "WSL ཐོབ་མི་ཚུགས།", + "desktop.wsl.error.listInstalled": "གཞི་བཙུགས་འབད་ཡོད་མི་ WSL ཌིསི་ཊོ་ཚུ་ཐོ་ཡིག་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "desktop.wsl.error.listOnline": "ཡོངས་འབྲེལ་ཐོག་ལས་ WSL ཌིསི་ཊོ་ཚུ་ཐོ་བཀོད་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "desktop.wsl.error.executeDistro": "ཌིསི་ཊོ་ནང་བརྡ་བཀོད་ཚུ་ལག་ལེན་འཐབ་མི་ཚུགས།", + "desktop.wsl.error.installWsl": "WSLགཞི་བཙུགས་འཐུས་ཤོར་བྱུང་ཡོདཔ།", + "desktop.wsl.error.installDistro": "ཌིསི་ཊོ་གཞི་བཙུགས་འབད་ནི་ལུ་འཐུས་ཤོར་བྱུང་ཡོདཔ།: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCodeགཞི་བཙུགས་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "desktop.wsl.error.alreadyAdded": "{{distro}} འདི་ ཧེ་མ་ལས་རང་ཁ་སྐོང་བརྐྱབ་ཟིན།", + "desktop.wsl.error.opencodeMissing": "ཌིསི་ཊོ་འདི་ནང་ opencode གཞི་བཙུགས་མ་འབད་བས།", + "desktop.wsl.error.opencodeCannotRun": "opencode འདི་གཞི་བཙུགས་འབད་ཡོད་རུང་ གཡོག་བཀོལ་མ་ཚུགས།", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode འདི་ {{distro}} ནང་གཞི་བཙུགས་མ་འབད་བས།", + "desktop.wsl.error.updateVersion": + "OpenCode དུས་མཐུན་བཟོ་ཚར་ཡི་ དེ་འབདཝ་ད་ {{distro}}གིས་ ད་ལྟོ་ཡང་ {{installed}} སྙན་ཞུ་འབདཝ་ཨིན། རེ་བ་བསྐྱེད་པའི་ {{expected}}", + "desktop.wsl.error.noVersion": "ཐོན་རིམ་མེད།", + "desktop.wsl.error.serverExited": + "WSL སར་བར་འདི་ འགོ་བཙུགས་པའི་ཤུལ་ལས་ ཕྱིར་འཐོན་འབད་ཡོདཔ་ཨིན་ (ཨང་རྟགས་={{code}} བརྡ་རྟགས་={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL སར་བར་འདི་ གསོ་བའི་ཧེ་མ་ ཕྱིར་འཐོན་འབད་ཡོདཔ་ཨིན་ (ཨང་རྟགས་={{code}} བརྡ་རྟགས་={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "{{timeout}}ms གི་ཤུལ་ལས་ {{distro}} གི་དོན་ལུ་ ཟུར་འཁོར་འདི་ གསོ་བའི་བརྟག་དཔྱད་དུས་ཚོད་རྫོགས་སོངཔ་ཨིན།", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} {{timeout}}ms གི་ཤུལ་ལས་ དུས་ཚོད་རྫོགས་སོང།", + "desktop.wsl.error.failedPort": "འདྲེན་ལམ་ཐོབ་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "desktop.picker.error.notSelected": "ཡིག་སྣོད་འདི་ སེལ་འཐུ་འབད་མི་གིས་ སེལ་འཐུ་མ་འབད་བས།", + "desktop.picker.error.sizeLimit": "སེལ་འཐུ་འབད་ཡོད་པའི་མཉམ་སྦྲགས་ཚུ་གིས་ {{limit}} ཨེམ་བི་ཚད་ལས་བརྒལཝ་ཨིན།", + "command.category.suggested": "བསམ་འཆར།", + "command.category.view": "བསམ༌འཆར", + "command.category.project": "ལས༌འགུལ", + "command.category.provider": "བྱིན་མི།", + "command.category.server": "སར་བར།", + "command.category.session": "ལཱ་ཡུན།", + "command.category.theme": "བརྗོད༌དོན", + "command.category.language": "སྐད༌ཡིག", + "command.category.file": "ཡིག༌སྣོད", + "command.category.context": "སྐབས་དོན།", + "command.category.terminal": "ཊར་མི་ནཱལ།", + "command.category.model": "དཔེ་ཚད།", + "command.category.mcp": "MCP།", + "command.category.agent": "ལས་ཚབ།", + "command.category.permissions": "གནང་བ་ཚུ།", + "command.category.workspace": "ལཱ་གི་ས་སྒོ།", + "command.category.settings": "སྒྲིག་སྟངས་ཚུ།", + "command.logs.export": "ཕྱིར་འདྲེན་དྲན་དེབ་ཚུ།", + "theme.scheme.system": "ལམ༌ལུགས", + "theme.scheme.light": "ཡང་འཕྲོས་འཕྲོས", + "theme.scheme.dark": "གནག་དུང་དུ", + "command.sidebar.toggle": "ཟུར་ཁའི་ཕྲ་རིང་སོར་སྟོན་འབད།", + "command.project.open": "ལས་གཞི་ཁ་ཕྱེ།", + "command.project.previous": "ལས་གཞི་སྔོན་མ།", + "command.project.next": "ལས་གཞི་རྗེས་མ།", + "command.project.index": "ལས་འགུལ་ {{index}} ལུ་སོར་བསྒྱུར་འབད།", + "command.provider.connect": "བྱིན་མི་མཐུད།", + "command.server.switch": "སར་བར་སོར་བསྒྱུར་འབད།", + "command.settings.open": "སྒྲིག་སྟངས་ཁ་ཕྱེ།", + "command.session.previous": "ཧེ་མའི་ལཱ་ཐེངས།", + "command.session.next": "ལཱ་ཡུན་ཤུལ་མམ།", + "command.session.previous.unseen": "ཧེ་མའི་མ་ལྷག་པའི་ལཱ་ཡུན།", + "command.session.next.unseen": "ལྷག་མ་ཚར་བའི་ལཱ་ཡུན་ཤུལ་མམ།", + "command.session.archive": "ཡིག་མཛོད་ལཱ་ཡན།", + "command.palette": "བརྡ་བཀོད་པེ་ལེཊི།", + "command.theme.cycle": "འཁོར་བའི་བརྗོད་གཞི།", + "command.theme.set": "བརྗོད་དོན་ལག་ལེན་འཐབ།: {{theme}}", + "command.theme.scheme.cycle": "འཁོར་བའི་ཚོས་གཞིའི་འཆར་གཞི།", + "command.theme.scheme.set": "ཚོས་གཞིའི་ལས་འཆར་ལག་ལེན་འཐབ།: {{scheme}}", + "command.language.cycle": "འཁོར་བའི་སྐད་ཡིག།", + "command.language.set": "སྐད་ཡིག་ལག་ལེན་འཐབ།: {{language}}", + "command.session.new": "ལཱ་ཡུན་གསརཔ།", + "command.file.open": "ཡིག་སྣོད་ཁ་ཕྱེ།", + "command.tab.close": "མཆོང་ལྡེ་ཁ་བསྡམས།", + "command.tab.reopenClosed": "ཁ་བསྡམས་ཡོད་པའི་མཆོང་ལྡེ་ལོག་ཁ་ཕྱེ།", + "command.context.addSelection": "སྐབས་དོན་ལུ་སེལ་འཐུ་ཁ་སྐོང་རྐྱབས།", + "command.context.addSelection.description": "ད་ལྟོའི་ཡིག་སྣོད་ལས་སེལ་འཐུ་འབད་ཡོད་པའི་གྲལ་ཐིག་ཚུ་ཁ་སྐོང་རྐྱབས།", + "command.input.focus": "ཆེད་དམིགས་ཨིན་པུཊི།", + "command.terminal.toggle": "ཊར་མི་ནཱལ་སོར་བསྒྱུར་འབད།", + "command.fileTree.toggle": "ཡིག་སྣོད་ཤིང་སོར་སྟོན་འབད།", + "command.review.toggle": "བསྐྱར་ཞིབ་སོར་སྟོན་འབད།", + "command.terminal.new": "ཊར་མི་ནཱལ་གསརཔ།", + "command.terminal.new.description": "ཊར་མི་ནཱལ་མཆོང་ལྡེ་གསརཔ་ཅིག་གསར་བསྐྲུན་འབད།", + "command.steps.toggle": "གོམ་པ་སོར་བསྒྱུར་འབད།", + "command.steps.toggle.description": "ད་ལྟོའི་འཕྲིན་དོན་གྱི་དོན་ལུ་ གོམ་པ་སྟོན་ཡང་ན་སྦ།", + "command.message.previous": "ཧེ་མའི་བརྡ་འཕྲིན།", + "command.message.previous.description": "ཧེ་མའི་ལག་ལེན་པའི་འཕྲིན་དོན་ལུ་འགྱོ།", + "command.message.next": "འཕྲིན་དོན།", + "command.message.next.description": "ལག་ལེན་པའི་འཕྲིན་དོན་ཤུལ་མམ་ལུ་འགྱོ།", + "command.model.choose": "དཔེ་ཚད་གདམ་ཁ་རྐྱབས།", + "command.model.choose.description": "དཔེ་ཚད་སོ་སོ་ཅིག་སེལ་འཐུ་འབད།", + "command.mcp.toggle": "MCPsསོར་སྟོན་འབད།", + "command.mcp.toggle.description": "MCPsསོར་སྟོན་འབད།", + "command.agent.cycle": "ལས་ཚབ་ཤུལ་མམ།", + "command.agent.cycle.description": "ཤུལ་མམ་གྱི་ལས་ཚབ་ལུ་སོར་བསྒྱུར་འབད།", + "command.agent.cycle.reverse": "རྐང་འཁོར་ལས་ཚབ་རྒྱབ་ལུ་བསྒྱུར།", + "command.agent.cycle.reverse.description": "ཧེ་མའི་ལས་ཚབ་ལུ་སོར་བསྒྱུར་འབད།", + "command.model.variant.cycle": "འཁོར་སྐྱོད་བསམ་བློའི་འབད་བརྩོན།", + "command.model.variant.cycle.description": "འབད་བརྩོན་གནས་རིམ་ཤུལ་མམ་ལུ་སོར་བསྒྱུར་འབད།", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "འདི་འཕྲོ་ལས", + "command.permissions.autoaccept.enable": "རང་བཞིན་ངོས་ལེན་གནང་བ་ཚུ།", + "command.permissions.autoaccept.disable": "རང་བཞིན་དང་ལེན་གནང་བ་ཚུ་བཀག།", + "command.workspace.toggle": "ལཱ་གི་ས་སྒོ་ཚུ་སོར་སྟོན་འབད།", + "command.workspace.toggle.description": "ཟུར་ཁའི་ཕྲ་རིང་ནང་ལཱ་གི་ས་སྒོ་སྣ་མང་ལྕོགས་ཅན་ཡང་ན་ལྕོགས་མིན་བཟོ།", + "command.session.undo": "འབད་བཤོལ་འབད།", + "command.session.undo.description": "མཇུག་གི་འཕྲིན་དོན་ རྒྱབ་བཤུད་འབད།", + "command.session.redo": "ཡང་བསྐྱར་འབད།", + "command.session.redo.description": "མཐའ་མཇུག་གི་འབད་བཤོལ་འབད་ཡོད་པའི་འཕྲིན་དོན་འདི་ལོག་འབད།", + "command.session.compact": "ལཱ་ཡུན་བསྡུ་སྒྲིག་འབད་ནི།", + "command.session.compact.description": "སྐབས་དོན་གྱི་ཚད་མར་ཕབ་འབད་ནིའི་དོན་ལུ་ ལཱ་ཡུན་བཅུད་བསྡུས།", + "command.session.fork": "འཕྲིན་དོན་ལས་ཕོརཀ།", + "command.session.fork.description": "ཧེ་མའི་འཕྲིན་དོན་ལས་ ལཱ་ཡུན་གསརཔ་ཅིག་གསར་བསྐྲུན་འབད།", + "command.session.share": "བརྗེ་སོར་གྱི་ལཱ་ཡུན།", + "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": "གྲུབ་འབྲས་གང་ཡང་མ་ཐོབ།", + "palette.group.commands": "བརྡ་བཀོད་ཚུ།", + "palette.group.files": "ཡིག་སྣོད་ཚུ།", + "dialog.provider.search.placeholder": "འཚོལ་ཞིབ་བྱིན་མི་ཚུ།", + "dialog.provider.empty": "བྱིན་མི་འཚོལ་མ་ཐོབ།", + "dialog.provider.group.popular": "ཡོངས༌གྲགས", + "dialog.provider.group.other": "གཞན", + "dialog.provider.custom.label": "སྲོལ་སྒྲིག OpenAI-མཐུན་འགྱུར་སྤྲོད་མཁན།", + "dialog.provider.tag.recommended": "རྒྱབ་སྣོན་འབད་ཡི", + "dialog.provider.opencode.note": "Claude དང་ GPT དེ་ལས་ Gemini དང་དེ་ལས་ལྷག་སྟེ་ བཀོད་སྒྲིག་འབད་ཡོད་པའི་དཔེ་ཚད།", + "dialog.provider.opencode.tagline": "བློ་གཏད་ཅན་གྱི་ཡར་རྒྱས་ཅན་གྱི་དཔེ་ཚད་ཚུ།", + "dialog.provider.opencodeGo.tagline": "མི་ཚང་མའི་དོན་ལུ་ གོང་ཚད་དམའ་བའི་ མཁོ་མངགས་འབད་ནི།", + "dialog.provider.anthropic.note": "པྲོ་དང་མེགསི་ཚུ་རྩིས་ཏེ་ Claudeདཔེ་ཚད་ཚུ་ལུ་ཐད་ཀར་དུ་འཛུལ་སྤྱོད།", + "dialog.provider.copilot.note": "GitHub Copilot བརྒྱུད་དེ་ ཀོ་ཌིང་གྲོགས་རམ་གྱི་དོན་ལུ་ AI དཔེ་ཚད།", + "dialog.provider.openai.note": "GPT མགྱོགས་དྲགས་དང་ལྕོགས་གྲུབ་ཅན་གྱི་སྤྱིར་བཏང་ཨེ་ཨའི་ལས་འགན་ཚུ་གི་དོན་ལུ་དཔེ་ཚད།", + "dialog.provider.google.note": "Gemini མགྱོགས་དྲགས་དང་སྒྲིག་བཀོད་ཅན་གྱི་ལན་འདེབས་ཀྱི་དཔེ་ཚད།", + "dialog.provider.openrouter.note": "བྱིན་མི་གཅིག་ལས་ རྒྱབ་སྐྱོར་འབད་ཡོད་པའི་དཔེ་ཚད་ཚུ་ཆ་མཉམ་འཛུལ་སྤྱོད་འབད།", + "dialog.provider.vercel.note": + "རིག་རྩལ་ཅན་གྱི་འགྲུལ་ལམ་དང་གཅིག་ཁར་ བཅོས་མའི་བློ་རིག་དཔེ་ཚད་ཚུ་ལུ་ མཉམ་བསྡོམས་འཛུལ་སྤྱོད་འབད་ནི།", + "dialog.model.select.title": "དཔེ་ཚད་སེལ་འཐུ་འབད།", + "dialog.model.search.placeholder": "དཔེ་ཚད་འཚོལ་ཞིབ་འབད།", + "dialog.model.empty": "དཔེ་ཚད་གྲུབ་འབྲས་མེད།", + "dialog.model.manage": "དཔེ་ཚད་ཚུ་འཛིན་སྐྱོང་འཐབ།", + "dialog.model.manage.description": "དཔེ་ཚད་སེལ་འཐུ་འབད་མི་ནང་ དཔེ་ཚད་ག་འདི་འབྱུངམ་ཨིན་ན་ སྲོལ་སྒྲིག་འབད།", + "dialog.model.manage.provider.toggle": "{{provider}} དཔེ་ཚད་ཚུ་ཆ་མཉམ་སོར་སྟོན་འབད།", + "dialog.model.unpaid.freeModels.title": "OpenCodeགིས་བྱིན་མི་རིན་མེད་དཔེ་ཚད་ཚུ།", + "dialog.model.unpaid.addMore.title": "ཡོངས་གྲགས་ཅན་གྱི་བྱིན་མི་ཚུ་ལས་ དཔེ་ཚད་མངམ་ཁ་སྐོང་བརྐྱབ།", + "dialog.model.unpaid.viewMoreProviders": "70+ མཁོ་སྤྲོད་འབད་མི་ཚུ་བལྟ།", + "dialog.provider.viewAll": "བྱིན་མི་མངམ་སྟོན།", + "provider.connect.title": "{{provider}}མཐུད།", + "provider.connect.title.anthropicProMax": "Claude Pro/Max དང་ཅིག་ཁར་ནང་བསྐྱོན།", + "provider.connect.selectMethod": "{{provider}}གི་དོན་ལུ་ ནང་བསྐྱོད་ཐབས་ལམ་སེལ་འཐུ་འབད།", + "provider.connect.method.apiKey": "APIལྡེ་མིག།", + "provider.connect.method.browser": "བརའུ་ཟར།", + "provider.connect.method.headless": "མགོ་མེད་པ།", + "provider.connect.status.inProgress": "དབང་སྤྲོད་འབད་བའི་བསྒང་ཡོད།", + "provider.connect.status.waiting": "གནང་བ་ལུ་སྒུག་སྡོདཔ་ཨིན།", + "provider.connect.status.failed": "དབང་སྤྲོད་འཐུས་ཤོར་བྱུང་ཡོདཔ།: {{error}}", + "provider.connect.apiKey.description": + "ཁྱོད་རའི་རྩིས་ཐོ་མཐུད་ནིའི་དོན་ལུ་ ཁྱོད་རའི་ {{provider}} API ལྡེ་མིག་བཙུགས་ཞིནམ་ལས་ OpenCode ནང་ལུ་ {{provider}} དཔེ་ཚད་ཚུ་ལག་ལེན་འཐབ།", + "provider.connect.apiKey.label": "{{provider}} APIལྡེ་མིག།", + "provider.connect.apiKey.placeholder": "APIལྡེ་མིག།", + "provider.connect.apiKey.required": "APIལྡེ་མིག་དགོས་མཁོ་ཡོད།", + "provider.connect.opencodeZen.line1": + "OpenCode ཟེན་གྱིས་ཁྱོད་ལུ་ གསང་ཡིག་ལས་ཚབ་ཚུ་གི་དོན་ལུ་ བློ་གཏད་ཅན་གྱི་ཡར་འཕེལ་ཅན་གྱི་དཔེ་ཚད་ཚུ་གི་ བཀོད་སྒྲིག་འབད་ཡོད་པའི་ཆ་ཚན་ཅིག་ལུ་འཛུལ་སྤྱོད་བྱིནམ་ཨིན།", + "provider.connect.opencodeZen.line2": + "API ལྡེ་མིག་རྐྱང་པ་གཅིག་གིས་ ཁྱོད་ཀྱིས་ Claude དང་ GPT དེ་ལས་ Gemini དང་ GLM དེ་ལས་མངམ་བཟུམ་གྱི་དཔེ་ཚད་ཚུ་ལུ་འཛུལ་སྤྱོད་འབད་ཚུགས།", + "provider.connect.opencodeZen.visit.prefix": "འགྱོ་ནི", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "ཁྱོད་རའི་ API ལྡེ་མིག་བསྡུ་ལེན་འབད་ནིའི་དོན་ལུ།", + "provider.connect.oauth.code.visit.prefix": "འགྱོ་ནི", + "provider.connect.oauth.code.visit.link": "འབྲེལ་མཐུད་འདི།", + "provider.connect.oauth.code.visit.suffix": + "ཁྱོད་རའི་རྩིས་ཐོ་མཐུད་ནིའི་དོན་ལུ་ ཁྱོད་རའི་གནང་བ་ཨང་རྟགས་བསྡུ་ལེན་འབད་ནི་དང་ OpenCode ནང་ལུ་ {{provider}} དཔེ་ཚད་ཚུ་ལག་ལེན་འཐབ་ནི།", + "provider.connect.oauth.code.label": "{{method}} དབང་སྤྲོད་ཨང་རྟགས།", + "provider.connect.oauth.code.placeholder": "དབང་སྤྲོད་ཨང་རྟགས།", + "provider.connect.oauth.code.required": "དབང་སྤྲོད་ཨང་རྟགས་དགོཔ་ཨིན།", + "provider.connect.oauth.code.invalid": "ནུས་མེད་དབང་སྤྲོད་ཨང་རྟགས།", + "provider.connect.oauth.auto.visit.prefix": "འགྱོ་ནི", + "provider.connect.oauth.auto.visit.link": "འབྲེལ་མཐུད་འདི།", + "provider.connect.oauth.auto.visit.suffix": + "དེ་ལས་ ཁྱོད་རའི་རྩིས་ཐོ་མཐུད་ནི་དང་ OpenCode ནང་ལུ་ {{provider}} དཔེ་ཚད་ཚུ་ལག་ལེན་འཐབ།", + "provider.connect.oauth.auto.confirmationCode": "ངེས་གཏན་ཨང་རྟགས།", + "provider.connect.toast.connected.title": "{{provider}}མཐུད་ཡོད།", + "provider.connect.toast.connected.description": "{{provider}} དཔེ་ཚད་ཚུ་ད་ལག་ལེན་འཐབ་བཏུབ་ཨིན།", + "provider.custom.title": "སྲོལ་སྒྲིག་བྱིན་མི།", + "provider.custom.unavailable": "སྲོལ་སྒྲིག་བྱིན་མི་ཚུ་ སར་བར་འདི་གུ་འཐོབ་མི་ཚུགས།", + "provider.custom.description.prefix": "OpenAI-མཐུན་འགྱུར་ཅན་གྱི་བྱིན་མི་ཅིག་རིམ་སྒྲིག་འབད། བལྟ།", + "provider.custom.description.link": "བྱིན་མི་རིམ་སྒྲིག་ཡིག་ཆ།", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "བྱིན་མི་ཨའི་ཌི།", + "provider.custom.field.providerID.placeholder": "ངེ་གི་བྱིན་མི།", + "provider.custom.field.providerID.description": "ཆུང་ཡིག་གི་ཡི་གུ་ ཨང་གྲངས་ སྦྲེལ་རྟགས་ ཡང་ན་ འོག་ཐིག་ཚུ།", + "provider.custom.field.name.label": "བཀྲམ་སྟོན་མིང་།", + "provider.custom.field.name.placeholder": "ངའི་ཨེ་ཨའི་སྤྲོད་མཁན།", + "provider.custom.field.baseURL.label": "གཞི་རྟེན་ URL།", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "APIལྡེ་མིག།", + "provider.custom.field.apiKey.placeholder": "APIལྡེ་མིག།", + "provider.custom.field.apiKey.description": + "གདམ་ཁ་ཅན། ཁྱོད་ཀྱིས་མགོ་ཡིག་ཚུ་བརྒྱུད་དེ་ བདེན་བཤད་འཛིན་སྐྱོང་འཐབ་པ་ཅིན་ སྟོངམ་བཞག།", + "provider.custom.models.label": "དཔེ་ཚད།", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "དཔེ་ཚད་-ཨའི་ཌི།", + "provider.custom.models.name.label": "མིང", + "provider.custom.models.name.placeholder": "བཀྲམ་སྟོན་མིང་།", + "provider.custom.models.remove": "དཔེ་ཚད་རྩ་བསྐྲད་གཏང་།", + "provider.custom.models.add": "དཔེ་ཚད་ཁ་སྐོང་འབད།", + "provider.custom.headers.label": "མགོ་ཡིག་ཚུ་ (གདམ་ཁ་ཅན་)།", + "provider.custom.headers.key.label": "མགོ་ཡིག།", + "provider.custom.headers.key.placeholder": "མགོ་ཡིག་-མིང་།", + "provider.custom.headers.value.label": "རིན་གོང", + "provider.custom.headers.value.placeholder": "རིན་གོང་", + "provider.custom.headers.remove": "མགོ་ཡིག་རྩ་བསྐྲད་གཏང་།", + "provider.custom.headers.add": "མགོ་ཡིག་ཁ་སྐོང་འབད།", + "provider.custom.error.providerID.required": "བྱིན་མི་ཨའི་ཌི་དགོཔ་ཨིན།", + "provider.custom.error.providerID.format": "ཆུང་ཡིག་ཡི་གུ་ ཨང་གྲངས་ སྦྲེལ་རྟགས་ ཡང་ན་ འོག་ཐིག་ཚུ་ལག་ལེན་འཐབ།", + "provider.custom.error.providerID.exists": "བྱིན་མི་ཨའི་ཌི་དེ་ཧེ་མ་ལས་རང་ཡོདཔ་ཨིན།", + "provider.custom.error.name.required": "བཀྲམ་སྟོན་མིང་དགོཔ་ཨིན།", + "provider.custom.error.baseURL.required": "གཞི་རྟེན་ URLའདི་དགོཔ་ཨིན།", + "provider.custom.error.baseURL.format": "http:// ཡང་ན་ https:// ལས་འགོ་བཙུགས་དགོ།", + "provider.custom.error.required": "དགོས་མཁོ", + "provider.custom.error.duplicate": "འདྲ་བཤུས།", + "provider.disconnect.toast.disconnected.title": "{{provider}} མཐུད་ལམ་ཆད་ཡོདཔ།", + "provider.disconnect.toast.disconnected.description": "{{provider}} དཔེ་ཚད་ཚུ་ད་ལས་ཕར་འཐོབ་མི་ཚུགས།", + "model.tag.free": "རིན་མེད་སྟོང་པ", + "model.tag.latest": "ད༌རེས༌ནངས༌པ", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI།", + "model.provider.meta": "Meta།", + "model.input.text": "ཡིག༌གུ༌", + "model.input.image": "པར་", + "model.input.audio": "སྒྲ།", + "model.input.video": "གློག༌བརྙན༌", + "model.input.pdf": "pdf .", + "model.tooltip.allows": "འབད་བཅུགཔ་ཨིན་: {{inputs}}", + "model.tooltip.context.label": "སྐབས་དོན།", + "model.tooltip.inputs": "ཨིན་པུཊི་ཚུ།", + "model.tooltip.model": "དཔེ་ཚད།", + "model.tooltip.provider": "བྱིན་མི།", + "model.tooltip.reasoning": "རྒྱུ་མཚན།", + "model.tooltip.reasoning.allowed": "རྒྱུ་མཚན་བཀོད་བཅུགཔ་ཨིན།", + "model.tooltip.reasoning.none": "རྒྱུ་མཚན་མེད།", + "model.tooltip.context": "སྐབས་དོན་ཚད་ {{limit}}", + "common.search.placeholder": "འཚོལ༌ནི", + "common.goBack": "ལོག་འགྲུལ་བསྐྱོད་འབད།", + "common.goForward": "གདོང་ཕྱོགས་ལུ་འགྲུལ་བསྐྱོད་འབད།", + "common.loading": "མངོན་གསལ་འབད་དོ།", + "common.loading.ellipsis": "...", + "common.cancel": "ཆ་མེད་བཏང་པ་", + "common.clear": "གསལ་ཏོག་ཏོ", + "common.open": "ཕྱེ་ཕྱེཝ", + "common.connect": "འབྲེལ༌མཐུད", + "common.disconnect": "མཐུད་ལམ་བཏོག་གཏང་།", + "common.continue": "འཕྲོ༌མཐུད", + "common.submit": "སྤྲོད་ནི", + "common.save": "སྲུང༌སྐྱོབས༌འབད༌བ", + "common.saving": "གསོག་འཇོག་འབད་དོ།", + "common.default": "འཐུས་ཤོར", + "common.attachment": "མཉམ༌སྦྲགས༌", + "prompt.placeholder.shell": "Shell བརྡ་བཀོད་བཙུགས།... {{example}}", + "prompt.placeholder.normal": 'ག་ཅི་ཡང་དྲིས... "{{example}}"', + "prompt.placeholder.simple": "ག་ཅི་ཡང་དྲིས།", + "prompt.placeholder.summarizeComments": "བསམ་འཆར་ཚུ་བཅུད་བསྡུས་འབད།", + "prompt.placeholder.summarizeComment": "བསམ་འཆར་བཅུད་བསྡུས།...", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "འདི་འཕྲོ་ལས", + "prompt.mode.shell.exit": "esc ཕྱིར་འཐོན་འབད་ནི།", + "session.child.promptDisabled": "ཡན་ལག་ལས་ཚབ་ལཱ་ཡུན་ཚུ་ བརྡ་སྟོན་འབད་མི་བཏུབ།", + "session.child.backToParent": "ལཱ་ཡུན་ངོ་མ་ལུ་ལོག་འགྱོ།", + "prompt.example.1": "གསང་གྲངས་གཞི་རྟེན་ནང་ ཊོ་ཌོ་ཅིག་ བདེ་སྒྲིག་འབད།", + "prompt.example.2": "ལས་འགུལ་འདི་གི་འཕྲུལ་རིག་བང་རིམ་ག་ཅི་སྨོ?", + "prompt.example.3": "ཆད་པའི་བརྟག་ཞིབ་ཚུ་བཅོ་ཁ་རྐྱབ་ནི།", + "prompt.example.4": "བདེན་བཤད་ག་དེ་སྦེ་འབདཝ་ཨིན་ན་ འགྲེལ་བཤད་རྐྱབ།", + "prompt.example.5": "ཉེན་སྲུང་གི་ཉེན་ཁ་ཚུ་འཚོལ་ཞིབ་འབད་དེ་བཅོ་ཁ་རྐྱབས།", + "prompt.example.6": "ལག་ལེན་པའི་ཞབས་ཏོག་དོན་ལུ་ ཡུ་ནིཊི་བརྟག་ཞིབ་ཚུ་ཁ་སྐོང་བརྐྱབ།", + "prompt.example.7": "ལས་འགན་འདི་ལྷག་ཚུགསཔ་སྦེ་ རི་ཕེག་ཊོར།", + "prompt.example.8": "འཛོལ་བ་འདི་གིས་ག་ཅི་སྟོན་སྨོ?", + "prompt.example.9": "གནད་དོན་འདི་རྐྱེན་སེལ་འབད་ནི་ལུ་རོགས་རམ་འབད།", + "prompt.example.10": "APIཡིག་ཆ་བཟོ་བཏོན་འབད།", + "prompt.example.11": "གནད་སྡུད་གཞི་རྟེན་གྱི་འདྲི་དཔྱད་ཚུ་ལེགས་བཅོས་འབད།", + "prompt.example.12": "ཨིན་པུཊི་བདེན་དཔྱད་ཁ་སྐོང་བརྐྱབ།", + "prompt.example.13": "གི་དོན་ལུ་ ཆ་ཤས་གསརཔ་ཅིག་གསར་བསྐྲུན་འབད།", + "prompt.example.14": "ལས་འགུལ་འདི་ག་དེ་སྦེ་བཀྲམ་སྤེལ་འབད་ནི་སྨོ?", + "prompt.example.15": "ལག་ལེན་ཡག་ཤོས་ཀྱི་དོན་ལུ་ ངེ་གི་ཨང་རྟགས་བསྐྱར་ཞིབ་འབད།", + "prompt.example.16": "ལས་འགན་འདི་ལུ་འཛོལ་བ་ལེགས་སྐྱོང་འབད་ནི་ཁ་སྐོང་བརྐྱབ།", + "prompt.example.17": "རི་ཇེགསི་དཔེ་རིས་འདི་འགྲེལ་བཤད་རྐྱབ།", + "prompt.example.18": "འདི་ TypeScript ལུ་གཞི་བསྒྱུར་འབད།", + "prompt.example.19": "གསང་གྲངས་གཞི་རྟེན་ཆ་མཉམ་ནང་དྲན་ཐོ་ཁ་སྐོང་བརྐྱབ།", + "prompt.example.20": "བརྟེན་པ་ག་ཅི་ཚུ་དུས་ཚོད་རྫོགས་ཡོདཔ་སྨོ?", + "prompt.example.21": "ང་ལུ་གནས་སོར་ཡིག་ཚུགས་ཅིག་འབྲི་ནི་ལུ་གྲོགས་རམ་འབད།", + "prompt.example.22": "མཇུག་སྣོད་འདི་གི་དོན་ལུ་འདྲ་མཛོད་ལག་ལེན་འཐབ།", + "prompt.example.23": "ཐོ་ཡིག་འདི་ལུ་ཤོག་ལེབ་ཁ་སྐོང་འབད།", + "prompt.example.24": "གི་དོན་ལུ་ CLI བརྡ་བཀོད་ཅིག་གསར་བསྐྲུན་འབད།", + "prompt.example.25": "མཐའ་འཁོར་འགྱུར་ཅན་ཚུ་ནཱ་ལུ་ག་དེ་སྦེ་ལཱ་འབདཝ་ཨིན་ན།", + "prompt.popover.emptyResults": "མཐུན་པའི་གྲུབ་འབྲས་མེད།", + "prompt.popover.emptyCommands": "མཐུན་སྒྲིག་བརྡ་བཀོད་མེད།", + "prompt.dropzone.label": "པར་རིས་ཚུ་དང་ པི་ཌི་ཨེཕ་ཚུ་ ཡང་ན་ ཚིག་ཡིག་ཡིག་སྣོད་ཚུ་ ནཱ་ལུ་བཀོག་བཞག།", + "prompt.dropzone.file.label": "@mention ཡིག་སྣོད་ལུ་བཀོག་བཞག།", + "prompt.slash.badge.custom": "ལུགས་སྲོལ་", + "prompt.slash.badge.skill": "རིག་རྩལ་", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "ངར་ཡེང་ཡེ་", + "prompt.context.includeActiveFile": "ཤུགས་ལྡན་ཡིག་སྣོད་གྲངས་སུ་བཙུགས།", + "prompt.context.removeActiveFile": "སྐབས་དོན་ལས་ཤུགས་ལྡན་ཡིག་སྣོད་རྩ་བསྐྲད་གཏང་།", + "prompt.context.removeFile": "སྐབས་དོན་ལས་ཡིག་སྣོད་རྩ་བསྐྲད་གཏང་།", + "prompt.action.attachFile": "ཡིག་སྣོད་ཚུ་ཁ་སྐོང་རྐྱབས།", + "prompt.menu.addImagesAndFiles": "ཡིག་སྣོད་ཚུ་དང་དེ་ལས་ལྷག་སྟེ་ཁ་སྐོང་རྐྱབས།", + "prompt.menu.imagesAndFiles": "གཟུགས་བརྙན་དང་ཡིག་སྣོད།", + "prompt.menu.commands": "བརྡ་བཀོད་ཚུ།", + "prompt.menu.context": "སྐབས་དོན།", + "prompt.menu.shellCommand": "Shell བརྡ་བཀོད།", + "prompt.attachment.remove": "མཉམ་སྦྲགས་རྩ་བསྐྲད་གཏང་།", + "prompt.action.send": "བཏང༌ནི", + "prompt.action.stop": "བཀག་པ", + "prompt.toast.pasteUnsupported.title": "རྒྱབ་སྐྱོར་མེད་པའི་མཉམ་སྦྲགས།", + "prompt.toast.pasteUnsupported.description": + "པར་རིས་དང་པི་ཌི་ཨེཕ་ ཡང་ན་ ཚིག་ཡིག་ཡིག་སྣོད་ཚུ་རྐྱངམ་ཅིག་ ནཱ་ལུ་མཉམ་སྦྲགས་འབད་བཏུབ།", + "prompt.toast.attachmentDuplicate.title": "ཡིག་སྣོད་འདི་ཧེ་མ་ལས་སྐྱེལ་བཙུགས་འབད་ཡི།", + "prompt.toast.modelAgentRequired.title": "ལས་ཚབ་དང་དཔེ་ཚད་ཅིག་སེལ་འཐུ་འབད།", + "prompt.toast.modelAgentRequired.description": "བརྡ་སྟོན་མ་གཏང་པའི་ཧེ་མ་ ལས་ཚབ་དང་དཔེ་ཚད་གདམ་ཁ་རྐྱབས།", + "prompt.toast.worktreeCreateFailed.title": "Git worktree གསར་བསྐྲུན་འབད་མ་ཚུགས།", + "prompt.toast.sessionCreateFailed.title": "ལཱ་ཡུན་གསར་བསྐྲུན་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "prompt.toast.shellSendFailed.title": "Shell བརྡ་བཀོད་གཏང་མ་ཚུགས།", + "prompt.toast.commandSendFailed.title": "བརྡ་བཀོད་གཏང་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "prompt.toast.promptSendFailed.title": "བརྡ་སྟོན་གཏང་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "prompt.toast.promptSendFailed.description": "ལཱ་ཡུན་སླར་འདྲེན་འབད་མ་ཚུགས།", + "dialog.mcp.title": "MCPs།", + "dialog.mcp.description": "{{enabled}} གི་ {{total}} ལྕོགས་ཅན་བཟོ་ཡོདཔ།", + "dialog.mcp.empty": "MCPsརིམ་སྒྲིག་མ་འབད་བས།", + "dialog.lsp.empty": "LSPs ཡིག་སྣོད་དབྱེ་བ་ཚུ་ལས་རང་བཞིན་སྐྱོན་འཛིན་འབད་ཡོདཔ།", + "dialog.plugins.empty": "opencode.json ནང་རིམ་སྒྲིག་འབད་ཡོད་པའི་པ་ལག་ཨིན་ཚུ།", + "mcp.status.connected": "མཐུད་ཡོད།", + "mcp.status.failed": "མཐར་མ་འཁྱོལ་བ་", + "mcp.status.needs_auth": "auth དགོཔ་ཨིན།", + "mcp.status.disabled": "དབང༌པོ༌སྐྱོན༌ཅན༌", + "mcp.auth.clickToAuthenticate": "བདེན་བཤད་འབད་ནི་ལུ་ཨེབ་གཏང་།", + "dialog.fork.empty": "ལས་ཕོརཀ་འབད་ནི་ལུ་འཕྲིན་དོན་མེད།", + "dialog.directory.search.placeholder": "སྣོད་འཛིན་ཚུ་འཚོལ།", + "dialog.directory.empty": "སྣོད་འཛིན་འཚོལ་མ་ཐོབ།", + "dialog.directory.action.selectFile": "ཡིག་སྣོད་སེལ་འཐུ་འབད།", + "dialog.directory.action.selectFolder": "སྣོད་འཛིན་སེལ་འཐུ་འབད།", + "dialog.directory.root": "ར༌ཏོག", + "dialog.directory.parent": "ཕ་མ།", + "dialog.directory.readError": "སྣོད་འཛིན་འདི་ལྷག་མི་ཚུགས།", + "app.server.unreachable": "{{server}} ལུ་ལྷོད་མ་ཚུགས།", + "app.server.retrying": "རང་བཞིན་གྱིས་ལོག་འབད་རྩོལ་བསྐྱེད་དོ།", + "app.server.otherServers": "སར་བར་གཞན་ཚུ།", + "dialog.server.title": "སར་བར་ཚུ།", + "dialog.server.description": "གློག་རིམ་འདི་གིས་OpenCodeསར་བར་ག་ལུ་མཐུདཔ་ཨིན་ན་སོར་བསྒྱུར་འབད།", + "dialog.server.search.placeholder": "སར་བར་ཚུ་འཚོལ་ཞིབ་འབད།", + "dialog.server.empty": "ད་ལྟོ་སར་བར་ཚུ་མེད།", + "dialog.server.add.title": "སར་བར་ཁ་སྐོང་འབད།", + "dialog.server.add.url": "སར་བར་ཁ་བྱང་།", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "སར་བར་ལུ་མཐུད་མ་ཚུགས།", + "dialog.server.add.checking": "ཞིབ་དཔྱད་འབད་དོ།", + "dialog.server.add.button": "སར་བར་ཁ་སྐོང་འབད།", + "dialog.server.add.name": "སར་བར་གྱི་མིང་ (གདམ་ཁ་ཅན།)", + "dialog.server.add.namePlaceholder": "Localhost།", + "dialog.server.add.username": "ལག་ལེན་པའི་མིང་ (གདམ་ཁ་ཅན་)།", + "dialog.server.add.usernamePlaceholder": "ལག་ལེན་པའི་མིང་།", + "dialog.server.add.password": "ཆོག་ཡིག་ (གདམ་ཁ་ཅན།)", + "dialog.server.add.passwordPlaceholder": "གསང༌ཨང", + "dialog.server.edit.title": "སར་བར་ཞུན་དག་འབད།", + "dialog.server.default.title": "སྔོན་སྒྲིག་སར་བར།", + "dialog.server.default.description": + "ཉེ་གནས་སར་བར་འགོ་བཙུགས་ནིའི་ཚབ་ལུ་ གློག་རིམ་འགོ་བཙུགས་པའི་སྐབས་ སར་བར་འདི་ལུ་མཐུད། ལོག་འགོ་བཙུགས་དགོཔ་ཨིན།", + "dialog.server.default.none": "སར་བར་སེལ་འཐུ་མ་འབད་བས།", + "dialog.server.default.set": "ད་ལྟོའི་སར་བར་འདི་སྔོན་སྒྲིག་སྦེ་གཞི་སྒྲིག་འབད།", + "dialog.server.default.clear": "གསལ་ཏོག་ཏོ", + "dialog.server.action.remove": "སར་བར་རྩ་བསྐྲད་གཏང་།", + "dialog.server.menu.edit": "ཞུན༌དག", + "dialog.server.menu.default": "སྔོན་སྒྲིག་སྦེ་གཞི་སྒྲིག་འབད།", + "dialog.server.menu.defaultRemove": "སྔོན་སྒྲིག་རྩ་བསྐྲད་གཏང་།", + "dialog.server.menu.delete": "བཏོན༌གཏང༌བ", + "dialog.server.current": "ད་ལྟོའི་སར་བར།", + "dialog.server.status.default": "འཐུས་ཤོར", + "wsl.server.add": "WSLསར་བར་ཁ་སྐོང་འབད།", + "wsl.server.addShort": "WSLཁ་སྐོང་བརྐྱབ།", + "wsl.server.label": "WSL།", + "wsl.server.menu.label": "WSL སར་བར།", + "wsl.server.retryStart": "འགོ་བཙུགས་བསྐྱར་འབད་འབད་རྩོལ་བསྐྱེད།", + "wsl.server.updating": "དུས་མཐུན་བཟོ་དོ།", + "wsl.onboarding.step.distro": "བཀྲམ་སྤེལ་གདམ་ཁ་རྐྱབས།", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "ཞིབ་དཔྱད་འབད་དོ WSL...", + "wsl.onboarding.restartRequired": "Windowsལུ་ WSLགཞི་བཙུགས་མཇུག་བསྡུ་ནིའི་དོན་ལུ་ ལོག་འགོ་བཙུགས་དགོཔ་ཨིན།", + "wsl.onboarding.ready": "WSL གྲ་སྒྲིག་ཡོད།", + "wsl.onboarding.required": "WSL འདི་འཕྲོ་མཐུད་འབད་དགོཔ་ཨིན།", + "wsl.onboarding.checkingDistros": "བཀྲམ་སྤེལ་ཚུ་ཞིབ་དཔྱད་འབད་དོ།", + "wsl.onboarding.installingDistro": "{{distro}}གཞི་བཙུགས་འབད་དོ།...", + "wsl.onboarding.checkingDistro": "ཞིབ་དཔྱད་འབད་དོ་ {{distro}}...", + "wsl.onboarding.listingDistros": "ཐོ་བཀོད་འབད་ནི།", + "wsl.onboarding.distroReady": "{{distro}} གྲ་སྒྲིག་ཡོད།", + "wsl.onboarding.distroNotInstalled": "{{distro}} ད་ལྟོ་གཞི་བཙུགས་མ་འབད་བས།", + "wsl.onboarding.openDistroOnce": "གཞི་སྒྲིག་མཇུག་བསྡུ་ནིའི་དོན་ལུ་ {{distro}} ཚར་གཅིག་ཁ་ཕྱེ།", + "wsl.onboarding.finishingDistro": "{{distro}}གི་དོན་ལུ་གཞི་སྒྲིག་མཇུག་བསྡུ་དོ།", + "wsl.onboarding.pickDistro": "ཌིསི་ཊོ་ཅིག་གདམ་ཁ་རྐྱབ་ནི་ཡང་ན་ འོག་ལུ་གཅིག་གཞི་བཙུགས་འབད།", + "wsl.onboarding.checkingOpencode": "OpenCodeཞིབ་དཔྱད་འབད་དོ།...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}}ནང་ OpenCode ཞིབ་དཔྱད་འབད་དོ།...", + "wsl.onboarding.updatingOpencode": "OpenCodeདུས་མཐུན་བཟོ་དོ།...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}}ནང་ OpenCodeདུས་མཐུན་བཟོ་དོ།...", + "wsl.onboarding.updateOpencodeIn": "OpenCodeའདི་ {{distro}}ནང་དུས་མཐུན་བཟོ།", + "wsl.onboarding.updateOpencode": "OpenCodeདུས་མཐུན་བཟོ།", + "wsl.onboarding.opencodeReadyIn": "OpenCode འདི་ {{distro}} ནང་ལུ་གྲ་སྒྲིག་ཡོདཔ་ཨིན།", + "wsl.onboarding.opencodeReady": "OpenCode གྲ་སྒྲིག་ཡོད།", + "wsl.onboarding.installOpencodeIn": "OpenCode འདི་ {{distro}} ནང་ལུ་གཞི་བཙུགས་འབད།", + "wsl.onboarding.installOpencode": "OpenCodeགཞི་བཙུགས་འབད།", + "wsl.onboarding.chooseDistroFirst": "དང་པ་ བཀྲམ་སྤེལ་ཅིག་གདམ་ཁ་རྐྱབས།", + "wsl.onboarding.loadFailed": "WSL གནས་སྟངས་མངོན་གསལ་འབད་ནི་ལུ་འཐུས་ཤོར་བྱུང་ཡོདཔ།", + "wsl.onboarding.loading": "མངོན་གསལ་འབད་དོ།", + "wsl.onboarding.installedDistros": "ཌིསི་ཊོ་ཚུ་གཞི་བཙུགས་འབད་ཡོདཔ།", + "wsl.onboarding.checkAgain": "ལོག་སྟེ་ཞིབ་དཔྱད་འབད།", + "wsl.onboarding.distroStatus.ready": "གྲ་སྒྲིག་ཚུད་ཡོདཔ", + "wsl.onboarding.distroStatus.checking": "ཞིབ་དཔྱད་འབད་དོ།", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode གཞི་བཙུགས་མ་འབད་བས།", + "wsl.onboarding.distroStatus.missingTools": "བཱཤ་, ཀརལ་བརླག་སྟོར་ཞུགསཔ་ཨིན།", + "wsl.onboarding.distroStatus.unsupported": "རྒྱབ་སྐྱོར་མ་འབད་བ · WSL 2 ལག་ལེན་འཐབ།", + "wsl.onboarding.needAnotherDistro": "གཞན་མི་ཌི་སི་ཊོ་དགོཔ་ཨིན་ན?", + "wsl.onboarding.needAnotherDistroHint": "WSL ཐོ་གཞུང་ལས་ Linux བགོ་བཀྲམ་ཅིག་གཞི་བཙུགས་འབད།", + "wsl.onboarding.wslNotInstalled.title": "WSL གཞི་བཙུགས་མ་འབད་བས།", + "wsl.onboarding.wslNotInstalled.description": + "WSL (WindowsLinuxཡན་ལག་རིམ་ལུགས་)འདི་ OpenCodeགིས་ WSLསར་བར་ཁ་སྐོང་མ་འབད་བའི་ཧེ་མ་དགོཔ་ཨིན།", + "wsl.onboarding.wslUnavailable.title": "WSL ཐོབ་མི་ཚུགས།", + "wsl.onboarding.wslUnavailable.description": "OpenCodeགིས་ འཕྲུལ་ཆས་འདི་གུ་ WSL བདེན་སྦྱོར་འབད་མ་ཚུགས།", + "wsl.onboarding.installWsl": "WSLགཞི་བཙུགས་འབད།", + "wsl.onboarding.windowsRestartRequired": + "WSL གཞི་བཙུགས་འབད་ནི་མཇུག་བསྡུ་ནིའི་དོན་ལུ་ Windows ལོག་འགོ་བཙུགསཔ་དང་ དེ་ལས་ OpenCode ལོག་ཁ་ཕྱེ།", + "wsl.onboarding.next": "ཤུལ༌མའི", + "wsl.onboarding.refresh": "གསརཔ་བཟོ།", + "wsl.onboarding.allDistrosAdded": "གཞི་བཙུགས་འབད་ཡོད་པའི་ཌིསི་ཊོ་ཚུ་ཆ་མཉམ་ཧེ་མ་ལས་ཁ་སྐོང་བརྐྱབ་ཡོདཔ་ཨིན།", + "wsl.onboarding.noDistros": "ད་ལྟོ་ཡང་ བཀྲམ་སྤེལ་འབད་མི་ཚུ་ ཤེས་རྟོགས་མ་བྱུང་པས།", + "wsl.onboarding.install": "བཙུགས་ནི", + "wsl.onboarding.installing": "གཞི་བཙུགས་འབད་དོ།", + "wsl.onboarding.installDistro": "ཌི་སི་ཊོ་གཞི་བཙུགས་འབད།", + "wsl.onboarding.searchDistros": "བཀྲམ་སྤེལ་ཚུ་འཚོལ་ཞིབ་འབད།", + "wsl.onboarding.wsl2Required": "WSL ༢ དགོས་མཁོ་ཡོད།", + "wsl.onboarding.toolsRequired": "བཀྲམ་སྤེལ་འདི་ལུ་ bash དང་ curl དགོཔ་ཨིན།", + "wsl.onboarding.openTerminal": "ཊར་མི་ནཱལ་ཁ་ཕྱེ།", + "wsl.onboarding.path": "འགྲུལ་ལམ་: {{path}}", + "wsl.onboarding.notFound": "མ་རྙེད།", + "wsl.onboarding.version": "ཐོན་རིམ: {{version}}", + "wsl.onboarding.unknown": "མ་ཤེསཔ་", + "wsl.onboarding.desktopVersion": "ཌེཀསི་ཊོཔ་ {{version}}", + "wsl.onboarding.versionMismatch": "གཞི་བཙུགས་འབད་ཡོད་པའི་ཐོན་རིམ་འདི་ ཌེཀསི་ཊོཔ་ཨེཔ་ཐོན་རིམ་དང་མཐུན་སྒྲིག་མི་འབད།", + "wsl.onboarding.adding": "ཁ་སྐོང་བརྐྱབ་དོ།", + "help.tabs.toast.ariaLabel": + "མཆོང་ལྡེ་ཚུ་ངོ་སྤྲོད་འབད་དོ། མཆོང་ལྡེ་ཚུ་དང་གཅིག་ཁར་ ཁྱོད་རའི་ལཱ་དང་ ཤུགས་ལྡན་ལཱ་ཡུན་ཚུ་ གོ་སྒྲིག་འབད།", + "help.tabs.toast.dismiss": "མཆོང་ལྡེ་བརྡ་དོན་བཏོན་གཏང་།", + "help.tabs.title": "མཆོང་ལྡེ་ངོ་སྤྲོད་འབད་དོ།", + "help.tabs.description": "མཆོང་ལྡེ་ཚུ་དང་གཅིག་ཁར་ ཁྱོད་རའི་ལཱ་དང་ ཤུགས་ལྡན་ལཱ་ཡུན་ཚུ་ གོ་སྒྲིག་འབད།", + "help.tabs.date": "སྤྱི་ཟླ་༧ པའི་ཚེས་༡༤།", + "help.tabs.introduction": "OpenCode ཌེཀསི་ཊོཔ་འདི་ད་ལྟོ་མཆོང་ལྡེ་ཚུ་གི་མཐའ་འཁོར་ལུ་བཟོ་བསྐྲུན་འབད་ཡོདཔ་ཨིན།", + "help.tabs.sessions": + "མཆོང་ལྡེ་ཅིག་ནང་ལཱ་ཡུན་གསརཔ་འགོ་བཙུགས་ ཡང་ན་ ཁྱོད་རའི་ལས་འགུལ་གང་རུང་ཅིག་ལས་ ད་ལྟོ་ཡོད་པའི་ལཱ་ཡུན་ཅིག་ཁ་ཕྱེ། ཁྱོད་ཀྱིས་ག་ཅི་ཡང་གསརཔ་འགོ་བཙུགས་པའི་སྐབས་ མཆོང་ལྡེ་གསརཔ་ཅིག་ཁ་ཕྱེ་ཞིནམ་ལས་ ཁྱོད་ཀྱིས་འབད་ཚརཝ་ད་ ཁ་བསྡམས།", + "help.tabs.organize": + "མཆོང་ལྡེ་དག་པ་ཅིག་ཁ་ཕྱེ་བཞག་མི་འདི་གིས་ ཁྱོད་རའི་ཤུགས་ལྡན་ལཱ་ཡུན་ཚུ་གོ་རིམ་སྒྲིག་ནི་ལུ་འཇམ་ཏོང་ཏོ་བཟོཝ་ཨིན། ཁྱོད་ཀྱིས་ མཆོང་ལྡེ་ཚུ་ མཐའ་འཁོར་ལུ་བཞག་ནི་གི་འཆར་གཞི་བརྩམས་ཏེ་ཡོད་པ་ཅིན་ དྲན་འཛིན་ཅན་གྱི་ཅ་ལ་ཅིག་ལུ་ མིང་བསྒྱུར་འབད།", + "help.tabs.home": + "ཁྱོད་ཀྱིས་ ཁྱོད་རའི་ལཱ་ཡུན་དང་ལས་འགུལ་ཚུ་ཆ་མཉམ་ ཁྱིམ་གྱི་གསལ་གཞི་གསརཔ་གུ་མཐོང་ཚུགས། ལཱ་ཡུན་ཅིག་སེལ་འཐུ་འབད་མི་འདི་གིས་ མཆོང་ལྡེ་ཅིག་ནང་ཁ་ཕྱེཝ་ཨིན།", + "help.tabs.persistence": + "ཁྱོད་ཀྱིས་གློག་རིམ་འདི་ལོག་ཁ་ཕྱེ་བའི་སྐབས་ ཁྱོད་ཀྱི་མཆོང་ལྡེ་ཚུ་ད་ལྟོ་ཡང་ཁ་ཕྱེ་སྟེ་ཡོདཔ་ཨིན།", + "help.tabs.worktrees": + "བཟོ་བཀོད་གསརཔ་འདི་གིས་ ད་ལྟོ་ཡང་ Git Worktrees ལུ་རྒྱབ་སྐྱོར་མི་འབད་ འདི་མགྱོགས་པ་རང་འོང་ནི་ཨིན། དེ་འབདཝ་ལས་ ཁྱོད་ཀྱིས་ ཧེ་མའི་སྒྲིག་བཀོད་འདི་འཕྲོ་མཐུད་ལག་ལེན་འཐབ་དགོ་པ་ཅིན་ ཁྱོད་ཀྱིས་ སྒྲིག་སྟངས་ཚུ་ནང་ སྒྲིག་བཀོད་ཚུ་གི་བར་ན་ སོར་བསྒྱུར་འབད་ཚུགས། བདུན་ཕྲག་དག་པ་ཅིག་གི་ནང་འཁོད་ལུ་ བཀོད་སྒྲིག་གསརཔ་འདི་ གཏན་འཇགས་འགྱོ་འོང་ཟེར་ སེམས་ཁར་བཞག་དགོ།", + "server.row.noUsername": "སྤྱོད་མིང་མེད།", + "dialog.project.edit.title": "ཞུན་དག་ལས་གཞི།", + "dialog.project.edit.name": "མིང", + "dialog.project.edit.icon": "ངོས་དཔར།", + "dialog.project.edit.icon.alt": "ལས་འགུལ་ངོས་དཔར།", + "dialog.project.edit.icon.hint": "གཟུགས་བརྙན་ཅིག་ཨེབ་གཏང་ཡང་ན་འདྲུད།", + "dialog.project.edit.icon.recommended": "འོས་སྦྱོར་འབད་ཡོདཔ།: ༡༢༨x༡༢༨px", + "dialog.project.edit.color": "ཚོས་གཞི", + "dialog.project.edit.color.select": "{{color}}ཚོས་གཞི་སེལ་འཐུ་འབད།", + "dialog.project.edit.worktree.startup": "ལཱ་གི་ས་སྒོ་འགོ་བཙུགས་ཡིག་ཚུགས་ཚུ།", + "dialog.project.edit.worktree.startup.description": + "ལཱ་གི་ས་སྒོ་གསརཔ་ (Git worktree) ཅིག་གསར་བསྐྲུན་འབད་བའི་ཤུལ་ལས་གཡོག་བཀོལཝ་ཨིན།", + "dialog.project.edit.worktree.startup.placeholder": "e.g. bun བཙུགས།", + "dialog.releaseNotes.action.getStarted": "འགོ་བཙུགས།", + "dialog.releaseNotes.action.next": "ཤུལ༌མའི", + "dialog.releaseNotes.action.hideFuture": "མ་འོངས་པར་འདི་དག་མ་སྟོན།", + "dialog.releaseNotes.media.alt": "སྔོན་ལྟ་གསར་བཏོན་འབད།", + "context.breakdown.title": "སྐབས་དོན་བགོ་བཤའ་རྐྱབ་ནི།", + "context.breakdown.note": + 'ཨིན་པུཊི་ཊོ་ཀེན་ཚུ་གི་ ཚོད་དཔག་བགོ་བཀྲམ། "Other" གིས་ ལག་ཆས་ངེས་ཚིག་དང་ མགོ་ཡིག་ཚུ་ཚུདཔ་ཨིན།', + "context.breakdown.system": "ལམ༌ལུགས", + "context.breakdown.user": "ལག་ལེན་པ།", + "context.breakdown.assistant": "ཞིབ༌རོགས", + "context.breakdown.tool": "ལག་ཆས་འབོད་བརྡ་ཚུ།", + "context.breakdown.other": "གཞན", + "context.systemPrompt.title": "རིམ་ལུགས་བརྡ་སྟོན།", + "context.rawMessages.title": "འཕྲིན་དོན་སྔོ་མོ།", + "context.export.session": "ལཱ་ཡུན་ཕྱིར་འདྲེན་འབད་ནི།", + "context.stats.session": "ལཱ་ཡུན།", + "context.stats.messages": "བརྡ་འཕྲིན།", + "context.stats.provider": "བྱིན་མི།", + "context.stats.model": "དཔེ་ཚད།", + "context.stats.limit": "སྐབས་དོན་ཚད་གཞི།", + "context.stats.totalTokens": "ཊོ་ཀེན་ཡོངས་བསྡོམས།", + "context.stats.usage": "བེད་སྤྱོད།", + "context.stats.inputTokens": "ཨིན་པུཊི་ཊོ་ཀེན་ཚུ།", + "context.stats.outputTokens": "ཨའུཊི་པུཊི་རྟགས་མཚན།", + "context.stats.reasoningTokens": "རྒྱུ་མཚན་བརྡ་རྟགས།", + "context.stats.cacheTokens": "འདྲ་མཛོད་ཊོ་ཀེན་ཚུ་ (ལྷག/བྲིས)", + "context.stats.userMessages": "ལག་ལེན་པའི་འཕྲིན་དོན།", + "context.stats.assistantMessages": "ལས་རོགས་འཕྲིན་དོན།", + "context.stats.totalCost": "འགྲོ་སོང་ཡོངས་བསྡོམས།", + "context.stats.sessionCreated": "ལཱ་ཡུན་གསར་བསྐྲུན་འབད་ཡོདཔ།", + "context.stats.lastActivity": "ལས་རིམ།", + "context.usage.tokens": "ཊོ་ཀེན་ཚུ།", + "context.usage.usage": "བེད་སྤྱོད།", + "context.usage.cost": "གོང་ཚད", + "context.usage.clickToView": "སྐབས་དོན་བལྟ་ནི་ལུ་ཨེབ་གཏང་།", + "context.usage.view": "སྐབས་དོན་ལག་ལེན་བལྟ།", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "སྐད༌ཡིག", + "toast.language.description": "{{language}}ལུ་ སོར་བསྒྱུར་འབད་ཡོདཔ།", + "toast.theme.title": "བརྗོད་དོན་སོར་བསྒྱུར་འབད་ཡོདཔ།", + "toast.scheme.title": "ཚོས་གཞིའི་འཆར་གཞི།", + "toast.workspace.enabled.title": "ལཱ་གི་ས་སྒོ་ཚུ་ལྕོགས་ཅན་བཟོ་ཡོདཔ།", + "toast.workspace.enabled.description": "Git worktree སྣ་མང་ད་ལྟོ་ཟུར་ཁའི་ཕྲ་རིང་ནང་སྟོནམ་ཨིན།", + "toast.workspace.disabled.title": "ལཱ་གི་ས་སྒོ་ཚུ་ ལྕོགས་མིན་བཟོ་ཡོདཔ།", + "toast.workspace.disabled.description": "ཟུར་ཁའི་ཕྲ་རིང་ནང་ Git worktree ངོ་མ་རྐྱངམ་ཅིག་སྟོནམ་ཨིན།", + "toast.permissions.autoaccept.on.title": "རང་བཞིན་དང་ལེན་གནང་བ་ཚུ།", + "toast.permissions.autoaccept.on.description": "གནང་བ་ཞུ་བ་ཚུ་ རང་བཞིན་གྱིས་ ཆ་འཇོག་འབད་འོང་།", + "toast.permissions.autoaccept.off.title": "རང་བཞིན་དང་ལེན་གནང་བ་ཚུ་ བཀག་བཞག་ཡོདཔ།", + "toast.permissions.autoaccept.off.description": "གནང་བ་ཞུ་བ་ཚུ་ལུ་ ཆ་འཇོག་དགོཔ་ཨིན།", + "toast.model.none.title": "དཔེ་ཚད་སེལ་འཐུ་མ་འབད་བས།", + "toast.model.none.description": "ལཱ་ཡུན་འདི་བཅུད་བསྡུས་འབད་ནིའི་དོན་ལུ་ བྱིན་མི་ཅིག་མཐུད།", + "toast.file.loadFailed.title": "ཡིག་སྣོད་མངོན་གསལ་འབད་ནི་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "toast.file.listFailed.title": "ཡིག་སྣོད་ཚུ་ཐོ་བཀོད་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "toast.context.noLineSelection.title": "གྲལ་ཐིག་སེལ་འཐུ་མེད།", + "toast.context.noLineSelection.description": "དང་པ་རང་ ཡིག་སྣོད་མཆོང་ལྡེ་ཅིག་ནང་གྲལ་ཐིག་ཁྱབ་ཚད་ཅིག་སེལ་འཐུ་འབད།", + "toast.session.share.copyFailed.title": "URLའདི་འཛིན་པང་ལུ་འདྲ་བཤུས་རྐྱབ་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "toast.session.share.success.title": "ལཱ་ཡུན་བརྗེ་སོར་འབད་ཡོདཔ།", + "toast.session.share.success.description": "བརྗེ་སོར་ URLའཛིན་པང་ལུ་འདྲ་བཤུས་རྐྱབས་ནུག་!", + "toast.session.share.failed.title": "ལཱ་ཡུན་རུབ་སྤྱོད་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "toast.session.share.failed.description": "ལཱ་ཡུན་བརྗེ་སོར་འབད་བའི་སྐབས་ འཛོལ་བ་ཅིག་བྱུང་ཡོདཔ།", + "toast.session.unshare.success.title": "ལཱ་ཡུན་བརྗེ་སོར་མ་འབད་བ།", + "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": "དུས་མཐུན་བཟོ་ཚུགས།", + "toast.update.description": "OpenCode ({{version}}) གི་ཐོན་རིམ་གསརཔ་ཅིག་ ད་ལྟོ་གཞི་བཙུགས་འབད་ནི་ལུ་འཐོབ་ཚུགས།", + "toast.update.action.installRestart": "གཞི་བཙུགས་དང་ལོག་འགོ་བཙུགས།", + "toast.update.action.notYet": "ད༌ལྟོ༌མེན༌པའི", + "error.page.title": "ག་ཅི་ཅིག་འཛོལ་སོང་ནུག།", + "error.page.description": "གློག་རིམ་མངོན་གསལ་འབད་བའི་སྐབས་ འཛོལ་བ་ཅིག་བྱུང་ནུག།", + "error.page.description.localServerStartup": "ཉེ་གནས་སར་བར་ འགོ་བཙུགས་པའི་སྐབས་ འཛོལ་བ་ཅིག་བྱུང་ནུག།", + "error.page.details.label": "འཛོལ་བའི་ཁ་གསལ།", + "error.page.action.restart": "ལོག་འགོ་བཙུགས།", + "error.page.action.report": "འཛོལ་བ་སྙན་ཞུ།", + "error.page.action.reported": "འཛོལ་བ་སྙན་ཞུ་འབད་ཡོདཔ།", + "error.page.action.exportLogs": "ཕྱིར་འདྲེན་དྲན་དེབ་ཚུ།", + "error.page.action.checking": "ཞིབ་དཔྱད་འབད་དོ།", + "error.page.action.checkUpdates": "དུས་མཐུན་ཚུ་ཞིབ་དཔྱད་འབད།", + "error.page.action.updateTo": "{{version}} ལུ་དུས་མཐུན་བཟོ།", + "error.page.circular": "[སྒོར་ཐིག་] .", + "error.page.report.prefix": "འཛོལ་བ་འདི་OpenCodeསྡེ་ཚན་ལུ་སྙན་ཞུ་འབད་གནང་།", + "error.page.report.discord": "on Discordལུ།", + "error.page.version": "ཐོན་རིམ: {{version}}", + "error.dev.rootNotFound": + "རྩ་བའི་ཆ་ཤས་འཚོལ་མ་ཐོབ། ཁྱོད་ཀྱི་ index.html ལུ་ཁ་སྐོང་འབད་ནི་བརྗེད་སོང་ག? ཡང་ན་ id ཁྱད་ཆོས་འདི་ཡིག་སྡེབ་འཛོལ་བ་འོང་ག?", + "error.serverSync.connectFailed": "སར་བར་ལུ་མཐུད་མ་ཚུགས། `{{url}}`ལུ་ སར་བར་གཡོག་བཀོལ་དོ་ཡོདཔ་ཨིན་ན?", + "error.serverSDK.noServerAvailable": "སར་བར་ཐོབ་མི་ཚུགས།", + "error.serverSDK.serverNotAvailable": "སར་བར་འཐོབ་མི་ཚུགས།", + "error.childStore.persistedCacheCreateFailed": "རྟག་བརྟན་འདྲ་མཛོད་གསར་བསྐྲུན་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "error.childStore.persistedProjectMetadataCreateFailed": + "ཡུན་བརྟན་ལས་འགུལ་མེ་ཊ་གནད་སྡུད་གསར་བསྐྲུན་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "error.childStore.persistedProjectIconCreateFailed": + "རྟག་བརྟན་ལས་འགུལ་གྱི་ངོས་དཔར་གསར་བསྐྲུན་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "error.childStore.storeCreateFailed": "མཛོད་ཁང་གསར་བསྐྲུན་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "directory.error.invalidUrl": "URLནང་གི་སྣོད་ཐོ་ནུས་མེད་ཨིན།", + "error.chain.unknown": "མ་ཤེས་པའི་འཛོལ་བ།", + "error.server.invalidConfiguration": "ནུས་མེད་རིམ་སྒྲིག།", + "error.chain.causedBy": "རྒྱུ་རྐྱེན་གྱིས་:", + "error.chain.apiError": "API འཛོལ་བ།", + "error.chain.status": "གནས་ཚད།: {{status}}", + "error.chain.retryable": "ཡང་བསྐྱར་འབད་བཏུབ་: {{retryable}}", + "error.chain.responseBody": "ལན་འདེབས་ཕུང་པོ།:\n{{body}}", + "error.chain.didYouMean": "ཁྱོད་ཀྱིས་: {{suggestions}} ཟེར་སླབ་དགོཔ་ཨིན་ན?", + "error.chain.modelNotFound": "དཔེ་ཚད་འཚོལ་མ་ཐོབ།: {{provider}}/{{model}}", + "error.chain.checkConfig": "ཁྱོད་ཀྱི་རིམ་སྒྲིག་ (opencode.json) བྱིན་མི་/དཔེ་ཚད་མིང་ཚུ་ཞིབ་དཔྱད་འབད།", + "error.chain.mcpFailed": + 'MCP སར་བར་ "{{name}}" འཐུས་ཤོར་བྱུང་ཡོདཔ། དྲན་འཛིན་ OpenCode གིས་ ད་ལྟོ་ཡང་ MCP བདེན་བཤད་ལུ་རྒྱབ་སྐྱོར་མི་འབད།', + "error.chain.providerAuthFailed": "བྱིན་མི་བདེན་བཤད་འཐུས་ཤོར་བྱུང་ཡོདཔ།({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'བྱིན་མི་ "{{provider}}" འགོ་བཙུགས་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ། ངོ་རྟགས་དང་རིམ་སྒྲིག་ཞིབ་དཔྱད་འབད།', + "error.chain.configJsonInvalid": "{{path}} ལུ་ཡོད་པའི་རིམ་སྒྲིག་ཡིག་སྣོད་འདི་ JSON(C)ནུས་ཅན་མེན།", + "error.chain.configJsonInvalidWithMessage": + "{{path}} ལུ་ཡོད་པའི་རིམ་སྒྲིག་ཡིག་སྣོད་འདི་ JSON(C): {{message}} ལུ་ནུས་ཅན་མེན།", + "error.chain.configDirectoryTypo": + '{{path}} ནང་གི་སྣོད་ཐོ་ "{{dir}}" ནུས་ཅན་མིན་འདུག། སྣོད་ཐོ་འདི་ "{{suggestion}}" ལུ་བསྐྱར་མིང་བཏགས་ ཡང་ན་ རྩ་བསྐྲད་གཏང་། འདི་སྤྱིར་བཏང་གི་ཡིག་ནོར་ཅིག་ཨིན།', + "error.chain.configFrontmatterError": "{{path}}:\n{{message}} ནང་གདོང་དོན་མིང་དཔྱད་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "error.chain.configInvalid": "{{path}}ལུ་ཡོད་པའི་རིམ་སྒྲིག་ཡིག་སྣོད་འདི་ནུས་མེད་ཨིན།", + "error.chain.configInvalidWithMessage": "{{path}} ལུ་ཡོད་པའི་རིམ་སྒྲིག་ཡིག་སྣོད་འདི་ནུས་མེད་ཨིན་: {{message}}", + "notification.permission.title": "གནང་བ་དགོཔ།", + "notification.permission.description": "{{sessionTitle}}ནང་ {{projectName}}ལུ་གནང་བ་དགོཔ་ཨིན།", + "notification.question.title": "དྲི་བ", + "notification.question.description": "{{sessionTitle}} ནང་{{projectName}}ནང་དྲི་བ་ཅིག་ཡོད།", + "notification.action.goToSession": "ལཱ་ཡུན་ལུ་འགྱོ།", + "notification.session.responseReady.title": "ལན་འདེབས་གྲ་སྒྲིག།", + "notification.session.error.title": "ལཱ་ཡུན་འཛོལ་བ།", + "notification.session.error.fallbackDescription": "འཛོལ་བ་བྱུང་ཡོདཔ།", + "home.recentProjects": "འཕྲལ་གྱི་ལས་གཞི།", + "home.empty.title": "འཕྲལ་གྱི་ལས་གཞི་མེད།", + "home.empty.description": "ས་གནས་ཀྱི་ལས་འགུལ་ཅིག་ཁ་ཕྱེ་སྟེ་འགོ་བཙུགས།", + "home.title": "ཁྱིམ", + "home.projects": "ལས་གཞི།", + "home.project.add": "ལས་འགུལ་ཁ་སྐོང་རྐྱབས།", + "home.recentlyClosed": "འཕྲལ་ཁམས་ཅིག་ཁར་ སྒོ་བསྡམས་ཡོདཔ།", + "home.server.collapse": "སར་བར་ལས་འགུལ་ཚུ་ བརྡབ་གཏང་།", + "home.server.expand": "སར་བར་ལས་འགུལ་ཚུ་རྒྱ་བསྐྱེད་འབད།", + "home.sessions.search.placeholder": "འཚོལ་ཞིབ་ལཱ་ཡུན་ཚུ།", + "home.sessions.search.placeholder.scoped": "{{scope}}ནང་འཚོལ་ཞིབ་ལཱ་ཡུན་ཚུ།", + "home.sessions.search.sessions": "ལཱ་ཡུན་ཚུ།", + "home.sessions.search.noResults": "{{query}}གི་དོན་ལུ་ལཱ་ཡུན་འཚོལ་མ་ཐོབ།", + "home.sessions.empty": "ད་དུང་འདིར་གང་ཡང་མེད།", + "home.sessions.empty.description": "འགོ་བཙུགས་ནིའི་དོན་ལུ་ ལཱ་ཡུན་ཅིག་གསར་བསྐྲུན་འབད།", + "home.sessions.group.today": "ད་རེས", + "home.sessions.group.yesterday": "ཁ༌ཙ", + "home.sessions.group.older": "རྒས་ཤོས།", + "home.providerTip": "Claude, GPT, Gemini, ལ་སོགས་པ་ཚུ་རྩིས་ཏེ་ དཔེ་ཚད་གཞན་ལག་ལེན་འཐབ་ནི་ལུ་ ༧༥+ བྱིན་མི་ལུ་མཐུད།", + "session.tab.session": "ལཱ་ཡུན།", + "session.tab.review": "བསྐྱར་ཞིབ", + "session.tab.context": "སྐབས་དོན།", + "session.tab.unknown": "མ་ཤེས་པའི་ལཱ་ཡུན།", + "session.panel.reviewAndFiles": "བསྐྱར་ཞིབ་དང་ཡིག་སྣོད།", + "session.error.notFound": "ལཱ་ཡུན་འདི་འཚོལ་མ་ཐོབ།", + "session.error.notFound.description": "མཆོང་ལྡེ་འདི་གིས་ སར་བར་འདི་གུ་ད་ལས་ཕར་མེད་པའི་ལཱ་ཡུན་ཅིག་ལུ་དཔག་འབདཝ་ཨིན།", + "session.error.notFound.closeTab": "མཆོང་ལྡེ་ཁ་བསྡམས།", + "session.error.serverConnection": "སར་བར་འདི་ལུ་མཐུད་མི་ཚུགས།", + "session.review.filesChanged": "ཡིག་སྣོད་ཚུ་བསྒྱུར་བཅོས་འབད་ཡོདཔ། {{count}}", + "session.review.change.one": "འགྱུར་བ", + "session.review.change.other": "བསྒྱུར་བཅོས་ཚུ།", + "session.review.loadingChanges": "བསྒྱུར་བཅོས་ཚུ་མངོན་གསལ་འབད་དོ...", + "session.review.empty": "ལཱ་ཡུན་འདི་ནང་ བསྒྱུར་བཅོས་ག་ནི་ཡང་མེདཔ།", + "session.review.noVcs": "མེན་ Git ཐོན་རིམ་ཚད་འཛིན་རིམ་ལུགས་སྐྱོན་འཛིན་འབད་ནུག བསྒྱུར་བཅོས་ཚུ་བཀྲམ་སྟོན་མ་འབད་བས།", + "session.review.noVcs.createGit.title": "Gitམཛོད་ཅིག་གསར་བསྐྲུན་འབད།", + "session.review.noVcs.createGit.description": + "ལས་འགུལ་འདི་ནང་ བསྒྱུར་བཅོས་ཚུ་ བརྟག་ཞིབ་འབད་ནི་དང་ བསྐྱར་ཞིབ་འབད་ནི་ དེ་ལས་ འབད་བཤོལ་འབད་ནི།", + "session.review.noVcs.createGit.actionLoading": "Git མཛོད་ཁང་གསར་བསྐྲུན་འབད་དོ།...", + "session.review.noVcs.createGit.action": "Gitམཛོད་ཁང་གསར་བསྐྲུན་འབད།", + "session.review.noSnapshot": + "རིམ་སྒྲིག་ནང་ལུ་ པར་རིས་རྗེས་འདེད་འབད་ནི་འདི་ལྕོགས་མིན་བཟོ་ཡོདཔ་ལས་ ལཱ་ཡུན་བསྒྱུར་བཅོས་ཚུ་འཐོབ་མི་ཚུགས།", + "session.review.noChanges": "བསྒྱུར་བཅོས་མེད།", + "session.review.noUncommittedChanges": "ཁས་ལེན་མ་འབད་བའི་བསྒྱུར་བཅོས་ཚུ་ད་ལྟོ་མེད།", + "session.review.noBranchChanges": "ཡན་ལག་བསྒྱུར་བཅོས་ད་ལྟོ་མེད།", + "session.files.selectToOpen": "ཁ་ཕྱེ་ནི་ལུ་ཡིག་སྣོད་ཅིག་སེལ་འཐུ་འབད།", + "session.files.all": "ཡིག་སྣོད་ཆ་མཉམ།", + "session.files.empty": "ཡིག་སྣོད་ཚུ་མེད།", + "session.files.binaryContent": "ཟུང་ལྡན་ཡིག་སྣོད་ (ནང་དོན་བཀྲམ་སྟོན་འབད་མི་བཏུབ།)", + "session.messages.renderEarlier": "ཧེ་མའི་འཕྲིན་དོན་ཚུ་བཀྲམ་སྟོན་འབད།", + "session.messages.loadingEarlier": "ཧེ་མའི་འཕྲིན་དོན་ཚུ་མངོན་གསལ་འབད་དོ།...", + "session.messages.loadEarlier": "ཧེ་མའི་འཕྲིན་དོན་ཚུ་མངོན་གསལ་འབད།", + "session.messages.loading": "འཕྲིན་དོན་ཚུ་མངོན་གསལ་འབད་དོ།...", + "session.messages.jumpToLatest": "གསར་ཤོས་ལུ་མཆོངས།", + "session.context.addToContext": "{{selection}} སྐབས་དོན་ལུ་ཁ་སྐོང་བརྐྱབ།", + "session.todo.title": "ཊོ་ཌོས།", + "session.todo.collapse": "རམ་པ", + "session.todo.expand": "རྒྱ་སྐྱེད་གཏང་ནི", + "session.todo.progress": "{{done}} གི་ {{total}} ཊོ་ཌོས་མཇུག་བསྡུ་ཡོདཔ།", + "session.question.progress": "{{current}}གི་{{total}}དྲི་བ།", + "session.question.minimize": "དྲི་བ་ཉུང་སུ་བཟོ།", + "session.question.restore": "དྲི་བ་སླར་གསོ་འབད།", + "session.question.pending.one": "{{count}} བསྒུག་པའི་དྲི་བ།", + "session.question.pending.other": "{{count}} སྒུག་པའི་དྲི་བ།", + "session.followupDock.summary.one": "{{count}} གྲལ་ཐིག་ཅན་གྱི་འཕྲིན་འཕྲིན།", + "session.followupDock.summary.other": "{{count}} གྲལ་སྒྲིག་ཡོད་པའི་འཕྲིན་འཕྲིན།", + "session.followupDock.sendNow": "ད་ལྟོ་གཏང་།", + "session.followupDock.edit": "ཞུན༌དག", + "session.followupDock.collapse": "གྲལ་ཐིག་ནང་བཙུགས་ཡོད་པའི་འཕྲིན་དོན་ཚུ་མར་བསྡམས།", + "session.followupDock.expand": "གྲལ་ཐིག་ནང་ཡོད་པའི་འཕྲིན་དོན་ཚུ་རྒྱ་བསྐྱེད་འབད།", + "session.revertDock.summary.one": "{{count}} ཕྱིར་ལོག་འཕྲིན་ཡིག།", + "session.revertDock.summary.other": "{{count}} ཕྱིར་ལོག་འབད་ཡོད་པའི་འཕྲིན་ཡིག།", + "session.revertDock.collapse": "ལོག་བཤུད་འབད་ཡོད་པའི་འཕྲིན་དོན་ཚུ་ མར་བསྡམས།", + "session.revertDock.expand": "ཕྱིར་ལོག་འབད་ཡོད་པའི་འཕྲིན་དོན་ཚུ་རྒྱ་བསྐྱེད་འབད།", + "session.revertDock.restore": "འཕྲིན་དོན་སླར་གསོ་འབད།", + "session.new.title": "ག་ཅི་ཡང་བཟོ་བསྐྲུན།", + "session.new.project.new": "ལས་འགུལ་གསརཔ།", + "session.new.project.search": "ལས་གཞི་འཚོལ་ཞིབ།", + "session.new.project.add": "ལས་འགུལ་ཁ་སྐོང་རྐྱབས།", + "session.new.worktree.main": "ཡན་ལག་གཙོ་བོ།", + "session.new.worktree.mainWithBranch": "ཡན་ལག་གཙོ་བོ་ ({{branch}})།", + "session.new.worktree.create": "Git worktree གསརཔ་གསར་བསྐྲུན་འབད།", + "session.new.workspace.runIn": "ནང་ལཱ་ཡུན་གཡོག་བཀོལ།", + "session.new.workspace.triggerLocal": "ས་གནས་ཀྱི", + "session.new.workspace.local": "ས་གནས་ཀྱི་མཛོད་ཁང་།", + "session.new.workspace.existing": "ལཱ་གི་ས་སྒོ...", + "session.new.git.none": "མེད། Git།", + "session.new.lastModified": "མཐའ་མའི་བཟོ་བཅོས་འབད་ཡོདཔ།", + "session.header.search.placeholder": "འཚོལ་ཞིབ་ {{project}}།", + "session.header.searchFiles": "ཡིག་སྣོད་ཚུ་འཚོལ་ཞིབ་འབད།", + "session.header.openIn": "ནང་ཁ་ཕྱེ།", + "session.header.open.action": "{{app}}ཁ་ཕྱེ།", + "session.header.open.ariaLabel": "{{app}}ནང་ཁ་ཕྱེ།", + "session.header.open.menu": "གདམ་ཁ་ཁ་ཕྱེ།", + "session.header.open.copyPath": "འདྲ་བཤུས་འགྲུལ་ལམ།", + "session.header.open.finder": "Finder།", + "session.header.open.fileExplorer": "File Explorer།", + "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "Finderནང་གསལ་སྟོན་འབད།", + "session.header.reveal.fileExplorer": "File Explorerནང་གསལ་སྟོན་འབད།", + "session.header.reveal.containingFolder": "སྣོད་འཛིན་ཡོད་པའི་ཁ་ཕྱེ།", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor།", + "session.header.open.app.zed": "Zed།", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity།", + "session.header.open.app.terminal": "ཊར་མི་ནཱལ།", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty།", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode།", + "session.header.open.app.androidStudio": "Android Studio།", + "session.header.open.app.powershell": "PowerShell།", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "གནས་ཚད", + "status.popover.ariaLabel": "སར་བར་རིམ་སྒྲིག་ཚུ།", + "status.popover.tab.servers": "སར་བར་ཚུ།", + "status.popover.tab.mcp": "MCP།", + "status.popover.tab.lsp": "LSP།", + "status.popover.tab.plugins": "པ་ལག་ཨིན་ཚུ།", + "status.popover.action.manageServers": "སར་བར་ཚུ་འཛིན་སྐྱོང་འཐབ།", + "session.share.popover.title": "ཝེབ་ནང་དཔར་བསྐྲུན་འབད།", + "session.share.popover.description.shared": + "ལཱ་ཡུན་འདི་ ཝེབ་ནང་ མི་མང་ཨིན། འབྲེལ་ལམ་ཡོད་མི་ག་ར་གིས་ འཛུལ་སྤྱོད་འབད་ཚུགས།", + "session.share.popover.description.unshared": + "ཡོངས་འབྲེལ་ནང་ལུ་ མི་མང་ལུ་ ལཱ་ཡུན་བརྗེ་སོར་འབད། འབྲེལ་ལམ་ཡོད་མི་ག་ར་གིས་ འཛུལ་སྤྱོད་འབད་ཚུགས།", + "session.share.action.share": "བགོ་བཤའ", + "session.share.action.publish": "དཔར་སྐྲུན།", + "session.share.action.publishing": "དཔར་བསྐྲུན་འབད་དོ།...", + "session.share.action.unpublish": "དཔར་བསྐྲུན་བཤོལ།", + "session.share.action.unpublishing": "དཔར་བསྐྲུན་མ་འབད་བའི་...", + "session.share.action.view": "བསམ༌འཆར", + "session.share.copy.copied": "འདྲ་བཤུས་འབད་ཡོདཔ།", + "session.share.copy.copyLink": "འབྲེལ་མཐུད་འདྲ་བཤུས།", + "lsp.tooltip.none": "LSP སར་བར་ཚུ་མེད།", + "lsp.label.connected": "{{count}} LSP།", + "prompt.loading": "མངོན་གསལ་འབད་དོ།", + "terminal.loading": "ཊར་མི་ནཱལ་མངོན་གསལ་འབད་དོ་...", + "terminal.title": "ཊར་མི་ནཱལ།", + "terminal.title.numbered": "ཊར་མི་ནཱལ་ {{number}}།", + "terminal.close": "ཊར་མི་ནཱལ་ཁ་བསྡམས།", + "terminal.connectionLost.title": "མཐུད་ལམ་བརླག་སྟོར་ཞུགས་ཡོདཔ།", + "terminal.connectionLost.abnormalClose": "WebSocket སྤྱིར་བཏང་མེན་པར་ཁ་བསྡམས: {{code}}", + "terminal.connectionLost.description": + "ཊར་མི་ནཱལ་མཐུད་ལམ་འདི་ བར་ཆད་བྱུང་ནུག། སར་བར་འདི་ལོག་འགོ་བཙུགས་པའི་སྐབས་ལུ་འདི་འབྱུང་འོང་།", + "terminal.connectTicket.csrfError": + "PTY མཐུད་ལམ་ཤོག་བྱང་འདི་ འབྱུང་ཁུངས་དང་ ཡང་ན་ CSRF ཞིབ་དཔྱད་ཚུ་གིས་ ངོས་ལེན་མ་འབད་བས། སར་བར་ CORS རིམ་སྒྲིག་ཞིབ་དཔྱད་འབད།", + "terminal.connectTicket.statusError": "PTY མཐུད་ལམ་ཤོག་བྱང་འདི་ {{status}}དང་གཅིག་ཁར་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "titlebar.update": "དུས༌མཐུན༌བཟོ༌ནི", + "titlebar.updateVersion": "{{version}} དུས་མཐུན་བཟོ།", + "common.closeTab": "མཆོང་ལྡེ་ཁ་བསྡམས།", + "common.dismiss": "ཁུངས༌མེད༌བརྩི༌ནི", + "common.moreCountSuffix": "(+{{count}} མང་།", + "common.requestFailed": "ཞུ་བ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "common.moreOptions": "གདམ་ཁ་མང་བ།", + "common.learnMore": "ཁ་གསལ་ཤེས།", + "common.rename": "མིང་བསྒྱུར།", + "common.export": "ཕྱིར་བཙོང", + "common.reset": "སླར་སྒྲིག་འབད།", + "common.archive": "གཏན་མཛོད", + "common.delete": "བཏོན༌གཏང༌བ", + "common.close": "སྒོ་བསྡམ", + "common.edit": "ཞུན༌དག", + "common.loadMore": "མངམ་མངོན་གསལ་འབད།", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "མ་ཤེསཔ་", + "common.time.justNow": "ད་ལྟོ་རང་།", + "common.time.minutesAgo.short": "{{count}}m གོང་།", + "common.time.hoursAgo.short": "{{count}}ཆུ་ཚོད་གོང་།", + "common.time.daysAgo.short": "{{count}}d གོང་།", + "sidebar.menu.toggle": "དཀར་ཆག་སོར་བསྒྱུར་འབད།", + "sidebar.nav.projectsAndSessions": "ལས་གཞི་དང་ཚོགས་ཚན།", + "sidebar.settings": "སྒྲིག་སྟངས་ཚུ།", + "sidebar.help": "ཆ་རོགས་འབད་ནི", + "sidebar.workspaces.enable": "ལཱ་གི་ས་སྒོ་ཚུ་ལྕོགས་ཅན་བཟོ།", + "sidebar.workspaces.disable": "ལཱ་གི་ས་སྒོ་ཚུ་ལྕོགས་མིན་བཟོ།", + "sidebar.gettingStarted.title": "འགོ་བཙུགས་དོ།", + "sidebar.gettingStarted.line1": "OpenCode ནང་རིན་མེད་དཔེ་ཚད་ཚུ་ཚུད་དེ་ཡོདཔ་ལས་ཁྱོད་ཀྱིས་དེ་འཕྲལ་ལས་འགོ་བཙུགས་བཏུབ།", + "sidebar.gettingStarted.line2": + "དཔེ་ཚད་ཚུ་ལག་ལེན་འཐབ་ནི་ལུ་ བྱིན་མི་གང་རུང་ཅིག་མཐུད།, inc. Claude དང་ GPT Gemini སོགས།", + "sidebar.project.recentSessions": "འཕྲལ་གྱི་ལཱ་ཡུན་ཚུ།", + "sidebar.project.viewAllSessions": "ལཱ་ཡུན་ཆ་མཉམ་བལྟ།", + "sidebar.project.clearNotifications": "བརྡ་བསྐུལ་ཚུ་གསལ་བཟོ།", + "sidebar.empty.title": "ལས་འགུལ་ཁ་ཕྱེ་མེད།", + "sidebar.empty.description": "འགོ་བཙུགས་ནིའི་དོན་ལུ་ལས་འགུལ་ཅིག་ཁ་ཕྱེ།", + "debugBar.ariaLabel": "གོང་འཕེལ་གྱི་ལས་དོན་བརྟག་དཔྱད།", + "debugBar.na": "ན/ཨེ།", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "མཐའ་མཇུག་མཇུག་བསྡུ་ཡོད་པའི་འགྲུལ་ལམ་འགྱུར་བ་འདི་ ལཱ་ཡུན་ཤོག་ལེབ་ཅིག་ལུ་ལགཔ་བརྐྱབ་སྟེ་ རའུ་ཊར་འགོ་བཙུགས་ལས་ དེ་གཞི་ཆགས་པའི་ཤུལ་ལས་ ཚོས་གཞི་འགོ་དང་པ་ཚུན་ཚོད་ འཇལ་ཡོདཔ་ཨིན།", + "debugBar.fps.label": "FPS།", + "debugBar.fps.tip": "མཇུག་གི་སྐར་ཆ་༥ ལས་ སྐར་ཆ་རེ་ལུ་ གཞི་ཁྲམ་ཚུ་བསྒྱིར་དོ།", + "debugBar.frame.label": "FRAME།", + "debugBar.frame.tip": "འདས་པའི་སྐར་ཆ་༥ ལས་ གཞི་ཁྲམ་གྱི་དུས་ཚོད་སྡུག་ཤོས།", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "མཇུག་གི་སྐར་ཆ་༥ ནང་ ༣༢ཨེམ་ཨེསི་ལས་ལྷག་སྟེ་ གཞི་ཁྲམ་ཚུ།", + "debugBar.long.label": "LONG།", + "debugBar.long.tip": "མཇུག་གི་སྐར་ཆ་༥ ནང་ བཀག་ཆ་འབད་ཡོད་པའི་དུས་ཚོད་དང་ ལས་འགན་རིངམོ་གྱངས་ཁ། མཐོ་ཤོས་ལས་ཀ: {{max}}.", + "debugBar.delay.label": "DELAY།", + "debugBar.delay.tip": "མཇུག་གི་སྐར་ཆ་༥ ནང་ བལྟ་རྟོག་འབད་མི་ ཨིན་པུཊི་ཕྱིར་འགྱངས་སྡུག་ཤོས།", + "debugBar.inp.label": "INP།", + "debugBar.inp.tip": + "འདས་པའི་སྐར་ཆ་༥ གི་རིང་ལུ་ འབྲེལ་བ་འཐབ་པའི་དུས་ཡུན་ཚོད་དཔག། འདི་ INP-དང་འདྲ་བ་ཨིན་ གཞུང་འབྲེལ་ཝེབ་ཝི་ཊ་ལསི་ INP མེན།", + "debugBar.cls.label": "CLS།", + "debugBar.cls.tip": "ད་ལྟོའི་གློག་རིམ་གྱི་མི་ཚེ་གི་དོན་ལུ་ བསྡོམས་རྩིས་སྒྲིག་བཀོད་སོར་བསྒྱུར་འབད།", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "ལག་ལེན་འཐབ་ཡོད་པའི་ JS བང་རིམ་ vs བང་རིམ་ཚད་གཞི། Chromium རྐྱངམ་གཅིག།", + "debugBar.mem.tip": "ལག་ལེན་འཐབ་ཡོད་པའི་ JS བང་རིམ་ vs བང་རིམ་ཚད་གཞི། {{used}} གི་ {{limit}} གི་ཨིན།", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "ཕན་ཚུན་འབྲེལ་བའི་ཆ་ཤས་ཚུ་ཆ་མཉམ་ལུ་ ཆེད་དམིགས་བཟོ་རྣམ་ཚུ་ བཙན་ཤུགས་འབད།", + "debugBar.focus.on": "ཐོག༌ཁར", + "debugBar.focus.off": "ཡལ་བ", + "debugBar.direction.label": "DIR།", + "debugBar.direction.tip": + "སེལ་འཐུ་འབད་ཡོད་པའི་སྐད་ཡིག་བསྒྱུར་བཅོས་མ་འབད་བར་ གློག་རིམ་ཆ་ཚང་སྒྲིག་བཀོད་ཁ་ཕྱོགས་བང་བཙོང་འབད།", + "debugBar.direction.ltr": "LTR།", + "debugBar.direction.rtl": "RTL།", + "app.name.desktop": "OpenCode ཌེཀསི་ཊོཔ།", + "settings.section.desktop": "ཌེཀསི་ཊོཔ།", + "settings.section.server": "སར་བར།", + "settings.tab.general": "སྤྱིར༌བཏང", + "settings.tab.shortcuts": "མགྱོགས་ཐབས།", + "settings.desktop.section.wsl": "WSL།", + "settings.desktop.wsl.title": "WSL མཉམ་སྡེབ།", + "settings.desktop.wsl.description": "Windowsགུ་ WSLནང་ན་ OpenCodeསར་བར་གཡོག་བཀོལ།", + "settings.general.section.appearance": "བཟོ་དབྱིབས", + "settings.general.section.advanced": "ཡར་རྒྱས་འགྱོ་ཡོདཔ", + "settings.general.section.notifications": "རིམ་ལུགས་བརྡ་བསྐུལ་ཚུ།", + "settings.general.section.updates": "དུས་མཐུན།", + "settings.general.section.sounds": "སྒྲའི་ནུས་པ།", + "settings.general.section.feed": "བྱིན་ནི", + "settings.general.section.display": "གསལ༌སྟོན", + "settings.general.row.language.title": "སྐད༌ཡིག", + "settings.general.row.language.description": "OpenCodeགི་དོན་ལུ་ བཀྲམ་སྟོན་སྐད་ཡིག་བསྒྱུར་བཅོས་འབད།", + "settings.general.row.shell.title": "ཊར་མི་ནཱལ་ Shell", + "settings.general.row.shell.description": "ཊར་མི་ནཱལ་དང་ལས་ཚབ་ལག་ཆས་ཚུ་གིས་ལག་ལེན་འཐབ་མི་ Shell", + "settings.general.row.shell.autoDefault": "རང་བཞིན་ (སྔོན་སྒྲིག)", + "settings.general.row.shell.terminalOnly": "ཊར་མི་ནཱལ་རྐྱངམ་ཅིག།", + "settings.general.row.appearance.title": "བཟོ་དབྱིབས", + "settings.general.row.appearance.description": + "OpenCodeའདི་ཁྱོད་རའི་ཐབས་འཕྲུལ་གུ་ག་དེ་སྦེ་མཐོངམ་ཨིན་ན་སྲོལ་སྒྲིག་འབད།", + "settings.general.row.colorScheme.title": "ཚོས་གཞིའི་འཆར་གཞི།", + "settings.general.row.colorScheme.description": + "OpenCode གིས་ རིམ་ལུགས་དང་ འོད་ ཡང་ན་ གནགཔོ་གི་བརྗོད་དོན་ལུ་ རྗེས་སུ་འཇུག་ནི་ཨིན་ན་ གདམ་ཁ་རྐྱབས།", + "settings.general.row.theme.title": "བརྗོད༌དོན", + "settings.general.row.theme.description": "OpenCodeའདི་བརྗོད་དོན་ག་དེ་སྦེ་ཡོདཔ་ཨིན་ན་སྲོལ་སྒྲིག་འབད།", + "settings.general.row.font.title": "ཨང་རྟགས་ཡིག་གཟུགས།", + "settings.general.row.font.description": "གསང་གྲངས་སྡེབ་ཚན་ཚུ་ནང་ལག་ལེན་འཐབ་མི་ཡིག་གཟུགས་སྲོལ་སྒྲིག་འབད།", + "settings.general.row.terminalFont.title": "ཊར་མི་ནཱལ་ཡིག་གཟུགས།", + "settings.general.row.terminalFont.description": "ཊར་མི་ནཱལ་ནང་ལག་ལེན་འཐབ་ཡོད་པའི་ཡིག་གཟུགས་སྲོལ་སྒྲིག་འབད།", + "settings.general.row.uiFont.title": "ཡུ་ཨའི་ཡིག་གཟུགས།", + "settings.general.row.uiFont.description": "ངོས་འདྲ་བ་ཆ་མཉམ་ལུ་ལག་ལེན་འཐབ་ཡོད་པའི་ཡིག་གཟུགས་སྲོལ་སྒྲིག་འབད།", + "settings.general.row.followup.title": "རྗེས་སུ་འབྲངས་པའི་སྤྱོད་ལམ།", + "settings.general.row.followup.description": + "རྗེས་འཇུག་འབོད་བརྡ་ཚུ་ དེ་འཕྲོ་ལས་ སྒུལ་ནི་ཨིན་ན་ ཡང་ན་ གྱལ་ནང་བསྒུག་སྡོད་ནི་ཨིན་ན་ གདམ་ཁ་རྐྱབས།", + "settings.general.row.followup.option.queue": "གྱལ", + "settings.general.row.followup.option.steer": "སྒུལ་ཤུགས།", + "settings.general.row.showFileTree.title": "ཡིག་སྣོད་ཤིང་།", + "settings.general.row.showFileTree.description": "ལཱ་ཡུན་ཚུ་ནང་ཡིག་སྣོད་ཤིང་གི་པེ་ནཱལ་སྟོན།", + "settings.general.row.showNavigation.title": "འགྲུལ་བསྐྱོད་ཚད་འཛིན་ཚུ།", + "settings.general.row.showNavigation.description": + "ཌེཀསི་ཊོཔ་ཁ་ཡིག་ཕྲ་རིང་ནང་ རྒྱབ་བསྐྱོད་དང་གདོང་བསྐྱོད་ཨེབ་རྟ་ཚུ་སྟོན།", + "settings.general.row.showSearch.title": "བརྡ་བཀོད་པེ་ལེཊི།", + "settings.general.row.showSearch.description": "ཁ་ཡིག་ཕྲ་རིང་ནང་ འཚོལ་ཞིབ་དང་བརྡ་བཀོད་པེ་ལེཊི་ཨེབ་རྟ་སྟོན།", + "settings.general.row.showTerminal.title": "ཊར་མི་ནཱལ།", + "settings.general.row.showTerminal.description": "ཌེཀསི་ཊོཔ་མགོ་མིང་ཕྲ་རིང་ནང་ ཊར་མི་ནཱལ་ཨེབ་རྟ་སྟོན།", + "settings.general.row.showStatus.title": "སར་བར་གནས་ཚད།", + "settings.general.row.showStatus.description": "མགོ་མིང་ཕྲ་རིང་ནང་ སར་བར་གནས་ཚད་ཨེབ་རྟ་སྟོན།", + "settings.general.row.mobileTitlebarBottom.title": "འོག་གི་འགྲུལ་བསྐྱོད།", + "settings.general.row.mobileTitlebarBottom.description": + "མོ་བཱ་ཡེལ་གུ་ གསལ་གཞི་གི་མཇུག་ལུ་ མགོ་མིང་ཕྲ་རིང་དང་ ལཱ་ཡུན་མཆོང་ལྡེ་ཚུ་བཙུགས།", + "settings.general.row.showCustomAgents.title": "ལས་ཚབ་སྟོན།", + "settings.general.row.showCustomAgents.description": + "བརྩམ་མི་ནང་ལུ་ ལས་ཚབ་ཚུ་གི་བར་ན་ སོར་བསྒྱུར་འབད། སྦ་བཞག་པའི་སྐབས་ བཟོ་བསྐྲུན་ལས་ཚབ་ལུ་སྔོན་སྒྲིག་འབདཝ་ཨིན།", + "settings.general.row.reasoningSummaries.title": "རྒྱུ་མཚན་བཅུད་བསྡུས་ཚུ་སྟོན།", + "settings.general.row.reasoningSummaries.description": + "དུས་ཚོད་གྲལ་ཐིག་ནང་དཔེ་ཚད་དོན་དག་བཅུད་བསྡུས་ཚུ་བཀྲམ་སྟོན་འབད།", + "settings.general.row.shellToolPartsExpanded.title": "Shell ལག་ཆས་ཆ་ཤས་ཚུ་རྒྱ་བསྐྱེད་འབད།", + "settings.general.row.shellToolPartsExpanded.description": + "དུས་ཚོད་གྲལ་ཐིག་ནང་སྔོན་སྒྲིག་གིས་རྒྱ་བསྐྱེད་འབད་ཡོད་མི་ Shell ལག་ཆས་ཆ་ཤས་ཚུ་སྟོན།", + "settings.general.row.editToolPartsExpanded.title": "ཞུན་དག་ལག་ཆས་ཆ་ཤས་ཚུ་རྒྱ་བསྐྱེད་འབད།", + "settings.general.row.editToolPartsExpanded.description": + "དུས་ཚོད་གྲལ་ཐིག་ནང་སྔོན་སྒྲིག་གིས་རྒྱ་བསྐྱེད་འབད་ཡོད་པའི་ཞུན་དག་དང་འབྲི་ནི་ དེ་ལས་ཐབས་འཕྲུལ་གྱི་ལག་ཆས་ཆ་ཤས་ཚུ་སྟོན།", + "settings.general.row.newInterface.title": "བཀོད་སྒྲིག་གསརཔ།", + "settings.general.row.newInterface.badge": "གསརཔ", + "settings.general.row.newInterface.description": + "མཆོང་ལྡེ་གསརཔ་ཚུ་དང་ཁྱིམ་སྒྲིག་བཀོད་ཚུ་ལག་ལེན་འཐབ། དུས་ཚོད་ཚད་ཅིག་གི་དོན་ལུ་ སྒྲིག་བཀོད་ཚུ་གི་བར་ན་ སོར་བསྒྱུར་འབད།", + "settings.general.row.newInterfaceNotice.title": "ད་ཁྱོད་ཀྱིས་སྒྲིག་བཀོད་གསརཔ་ལག་ལེན་འཐབ་དོ།", + "settings.general.row.newInterfaceNotice.description": "ཧེ་མའི་སྒྲིག་བཀོད་འདི་ ད་ལས་ཕར་འཐོབ་མི་ཚུགས།", + "settings.general.row.newInterfaceNotice.dismiss": "ཁུངས༌མེད༌བརྩི༌ནི", + "settings.general.row.pinchZoom.title": "རྒྱས་ཟུམ་འབད་ནི་ལུ་ ཨེབ་གཏང་།", + "settings.general.row.pinchZoom.description": "ཊེག་པེཌི་པིནཆི་དང་ Ctrl-བཤུད་སྒྲིལ་བརྡ་མཚོན་ཚུ་རྒྱས་ཟུམ་འབད་བཅུག།", + "settings.general.row.wayland.title": "ས་གནས་ཀྱི་ Waylandལག་ལེན་འཐབ།", + "settings.general.row.wayland.description": "Waylandགུ་ཨེགསི་༡༡ ཕོལབེཀ་ལྕོགས་མིན་བཟོ། ལོག་འགོ་བཙུགས་དགོཔ་ཨིན།", + "settings.general.row.wayland.tooltip": + "Linuxགུ་ སླ་བསྲེ་གསར་བསྐྲུན་-ཚད་བལྟ་རྟོག་པ་ཚུ་དང་གཅིག་ཁར་ ནེ་ཊིབ་ Wayland འདི་ཧེང་སྐལ་བརྟན་ཏོག་ཏོ་འོང་།", + "settings.general.row.releaseNotes.title": "འགྲེམ་སྤེལ་དྲན་ཐོ།", + "settings.general.row.releaseNotes.description": "དུས་མཐུན་བཟོ་ཚར་བའི་ཤུལ་ལས་ པོཔ་ཨཔ་གསརཔ་ག་ཅི་ཡོདཔ་ཨིན་ན་སྟོན།", + "settings.updates.row.startup.title": "འགོ་བཙུགས་གུ་དུས་མཐུན་ཚུ་ཞིབ་དཔྱད་འབད།", + "settings.updates.row.startup.description": "OpenCode འགོ་བཙུགསཔ་ད་ རང་བཞིན་གྱིས་དུས་མཐུན་ཚུ་ཞིབ་དཔྱད་འབད།", + "settings.updates.row.check.title": "དུས་མཐུན་ཚུ་ཞིབ་དཔྱད་འབད།", + "settings.updates.row.check.description": + "ལག་ཐོག་ལས་དུས་མཐུན་ཚུ་ཞིབ་དཔྱད་འབད་ཞིནམ་ལས་ འཐོབ་ཚུགས་པ་ཅིན་གཞི་བཙུགས་འབད།", + "settings.updates.action.checkNow": "ད་ལྟོ་ཞིབ་དཔྱད་འབད།", + "settings.updates.action.checking": "ཞིབ་དཔྱད་འབད་དོ།", + "settings.updates.action.downloading": "ཕབ་ལེན་འབད་དོ།", + "settings.updates.action.installing": "གཞི་བཙུགས་འབད་དོ།", + "settings.updates.toast.latest.title": "ཁྱོད་དུས་དང་བསྟུན།", + "settings.updates.toast.latest.description": "ཁྱོད་ཀྱིས་ OpenCodeགི་ཐོན་རིམ་གསརཔ་འདི་གཡོག་བཀོལ་དོ།", + "sound.option.none": "ག་ནི་ཡང་མེདཔ།", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "ལས་ཚབ།", + "settings.general.notifications.agent.description": + "ལས་ཚབ་འདི་མཇུག་བསྡུ་ཡོདཔ་ ཡང་ན་ དོ་སྣང་དགོཔ་ད་ རིམ་ལུགས་བརྡ་བསྐུལ་སྟོན།", + "settings.general.notifications.permissions.title": "གནང་བ་ཚུ།", + "settings.general.notifications.permissions.description": "གནང་བ་ཅིག་དགོ་པའི་སྐབས་ རིམ་ལུགས་བརྡ་བསྐུལ་སྟོན།", + "settings.general.notifications.errors.title": "འཛོལ་བ།", + "settings.general.notifications.errors.description": "འཛོལ་བ་བྱུངམ་ད་ རིམ་ལུགས་བརྡ་བསྐུལ་སྟོན།", + "settings.general.sounds.agent.title": "ལས་ཚབ།", + "settings.general.sounds.agent.description": "ལས་ཚབ་འདི་མཇུག་བསྡུ་བའི་སྐབས་ཡང་ན་དོ་སྣང་དགོ་པའི་སྐབས་སྒྲ་གཏང་།", + "settings.general.sounds.permissions.title": "གནང་བ་ཚུ།", + "settings.general.sounds.permissions.description": "གནང་བ་དགོཔ་ད་ སྒྲ་སྐད་གཏང་ནི།", + "settings.general.sounds.errors.title": "འཛོལ་བ།", + "settings.general.sounds.errors.description": "འཛོལ་བ་བྱུངམ་ད་སྒྲ་སྐད་གཏང་།", + "settings.shortcuts.title": "ལྡེ་སྒྲོམ་གྱི་མགྱོགས་ཐབས།", + "settings.shortcuts.reset.button": "སྔོན་སྒྲིག་ཚུ་ལུ་སླར་སྒྲིག་འབད།", + "settings.shortcuts.reset.toast.title": "མགྱོགས་ཐབས་སླར་སྒྲིག་འབད།", + "settings.shortcuts.reset.toast.description": + "ལྡེ་སྒྲོམ་གྱི་མགྱོགས་ཐབས་ཚུ་ སྔོན་སྒྲིག་ཚུ་ལུ་ སླར་སྒྲིག་འབད་ཡོདཔ་ཨིན།", + "settings.shortcuts.conflict.title": "མགྱོགས་ཐབས་ཧེ་མ་ལས་ལག་ལེན་འཐབ་དོ།", + "settings.shortcuts.conflict.description": "{{keybind}} འདི་ ཧེ་མ་ལས་རང་ {{titles}} ལུ་འགན་སྤྲོད་འབད་ཡོདཔ་ཨིན།", + "settings.shortcuts.unassigned": "འགན་སྤྲོད་མ་འབད་བ།", + "settings.shortcuts.pressKeys": "ལྡེ་མིག་ཨེབ།", + "settings.shortcuts.search.placeholder": "འཚོལ་ཞིབ་མགྱོགས་ཐབས་ཚུ།", + "settings.shortcuts.search.empty": "མགྱོགས་ཐབས་འཚོལ་མ་ཐོབ།", + "settings.shortcuts.group.general": "སྤྱིར༌བཏང", + "settings.shortcuts.group.session": "ལཱ་ཡུན།", + "settings.shortcuts.group.navigation": "འགྲུལ་བསྐྱོད།", + "settings.shortcuts.group.modelAndAgent": "དཔེ་ཚད་དང་ལས་ཚབ།", + "settings.shortcuts.group.terminal": "ཊར་མི་ནཱལ།", + "settings.shortcuts.group.prompt": "འདི་འཕྲོ་ལས", + "settings.providers.title": "བྱིན་མི་ཚུ།", + "settings.providers.description": "བྱིན་མི་སྒྲིག་སྟངས་ཚུ་ ནཱ་ལུ་རིམ་སྒྲིག་འབད་བཏུབ།", + "settings.providers.section.connected": "མཐུད་ཡོད་པའི་བྱིན་མི་ཚུ།", + "settings.providers.connected.empty": "མཐུད་ཡོད་པའི་བྱིན་མི་མེད།", + "settings.providers.connected.environmentDescription": "ཁྱོད་ཀྱི་མཐའ་འཁོར་འགྱུར་ཅན་ཚུ་ལས་མཐུད་ཡོདཔ།", + "settings.providers.section.popular": "ཡོངས་གྲགས་ཅན་གྱི་བྱིན་མི་ཚུ།", + "settings.providers.custom.description": "གཞི་རྟེན་ URLགིས་ OpenAI-མཐུན་འགྱུར་བྱིན་མི་ཅིག་ཁ་སྐོང་བརྐྱབ།", + "settings.providers.tag.environment": "མཐའ་འཁོར", + "settings.providers.tag.config": "རིམ་སྒྲིག།", + "settings.providers.tag.custom": "ལུགས་སྲོལ", + "settings.providers.tag.other": "གཞན", + "settings.models.title": "དཔེ་ཚད།", + "settings.models.description": "དཔེ་ཚད་སྒྲིག་སྟངས་ཚུ་ནཱ་ལུ་རིམ་སྒྲིག་འབད་བཏུབ་འོང་།", + "settings.agents.title": "ལས་ཚབ་ཚུ།", + "settings.agents.description": "ལས་ཚབ་སྒྲིག་སྟངས་ཚུ་ནཱ་ལུ་རིམ་སྒྲིག་འབད་བཏུབ་འོང་།", + "settings.commands.title": "བརྡ་བཀོད་ཚུ།", + "settings.commands.description": "བརྡ་བཀོད་སྒྲིག་སྟངས་ཚུ་ ནཱ་ལུ་རིམ་སྒྲིག་འབད་བཏུབ་འོང་།", + "settings.mcp.title": "MCP།", + "settings.mcp.description": "MCP སྒྲིག་སྟངས་ཚུ་ ནཱ་ལུ་རིམ་སྒྲིག་འབད་བཏུབ་འོང་།", + "settings.permissions.title": "གནང་བ་ཚུ།", + "settings.permissions.description": "སར་བར་གྱིས་སྔོན་སྒྲིག་གིས་ལག་ཆས་ག་ཅི་ལག་ལེན་འཐབ་ཚུགསཔ་ཨིན་ན་ཚད་འཛིན་འབད།", + "settings.permissions.section.tools": "ཐབས༌ཤེས", + "settings.permissions.toast.updateFailed.title": "གནང་བ་ཚུ་དུས་མཐུན་བཟོ་ནི་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "settings.permissions.action.allow": "བྱིན་ནི", + "settings.permissions.action.ask": "དྲི་ནི", + "settings.permissions.action.deny": "ཁས༌མི༌ལེན", + "settings.permissions.tool.read.title": "ལྸག་ནི", + "settings.permissions.tool.read.description": "ཡིག་སྣོད་ཅིག་ལྷག་དོ(ཡིག་སྣོད་འགྲུལ་ལམ་མཐུན་སྒྲིག་འབདཝ་ཨིན།)", + "settings.permissions.tool.edit.title": "ཞུན༌དག", + "settings.permissions.tool.edit.description": + "ཞུན་དག་དང་འབྲི་ནི་ དེ་ལས་ཐབས་འཕྲུལ་ཚུ་གྲངས་སུ་བཙུགས་ཏེ་ཡིག་སྣོད་ཚུ་ལེགས་བཅོས་འབད།", + "settings.permissions.tool.glob.title": "Glob།", + "settings.permissions.tool.glob.description": "གོ་ལོབ་དཔེ་གཞི་ཚུ་ལག་ལེན་འཐབ་ཐོག་ལས་ཡིག་སྣོད་ཚུ་མཐུན་སྒྲིག་འབད།", + "settings.permissions.tool.grep.title": "Grep།", + "settings.permissions.tool.grep.description": + "དུས་རྒྱུན་གསལ་བརྗོད་ཚུ་ལག་ལེན་འཐབ་ཐོག་ཡིག་སྣོད་ནང་དོན་ཚུ་འཚོལ་ཞིབ་འབད།", + "settings.permissions.tool.list.title": "ཐོ", + "settings.permissions.tool.list.description": "སྣོད་ཐོ་ཅིག་གི་ནང་འཁོད་ཡིག་སྣོད་ཚུ་ཐོ་བཀོད་འབད།", + "settings.permissions.tool.bash.title": "Bash།", + "settings.permissions.tool.bash.description": "ཤལ་བརྡ་བཀོད་ཚུ་གཡོག་བཀོལ།", + "settings.permissions.tool.task.title": "ལཱ", + "settings.permissions.tool.task.description": "ཡན་ལག་ལས་ཚབ་ཚུ་འགོ་བཙུགས།", + "settings.permissions.tool.skill.title": "རིག་རྩལ", + "settings.permissions.tool.skill.description": "མིང་གིས་རིག་རྩལ་ཅིག་མངོན་གསལ་འབད།", + "settings.permissions.tool.lsp.title": "LSP།", + "settings.permissions.tool.lsp.description": "སྐད་ཡིག་སར་བར་གྱི་འདྲི་དཔྱད་ཚུ་གཡོག་བཀོལ།", + "settings.permissions.tool.todowrite.title": "Todo Write།", + "settings.permissions.tool.todowrite.description": "འབད་དགོ་པའི་ཐོ་ཡིག་དུས་མཐུན་བཟོ།", + "settings.permissions.tool.webfetch.title": "Web Fetch།", + "settings.permissions.tool.webfetch.description": "URLནང་ལས་ནང་དོན་ལེན།", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "ཝེབ་འཚོལ་ཞིབ།", + "settings.permissions.tool.external_directory.title": "ཕྱིའི་སྣོད་ཐོ།", + "settings.permissions.tool.external_directory.description": "ལས་འགུལ་སྣོད་ཐོའི་ཕྱི་ཁར་ཡིག་སྣོད་ཚུ་འཛུལ་སྤྱོད་འབད།", + "settings.permissions.tool.doom_loop.title": "Doom Loop།", + "settings.permissions.tool.doom_loop.description": + "ཅོག་འཐདཔ་ཨིན་པུཊི་གཅིག་ཁར་ ལག་ཆས་འབོད་བརྡ་ཚུ་ཡང་བསྐྱར་སྐྱོན་འཛིན་འབད།", + "session.delete.failed.title": "ལཱ་ཡུན་བཏོན་གཏང་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "session.delete.title": "ལཱ་ཡུན་བཏོན་གཏང་།", + "session.delete.confirm": 'ལཱ་ཡུན་ "{{name}}" བཏོན་གཏང་ནི་ཨིན་ན?', + "session.delete.button": "ལཱ་ཡུན་བཏོན་གཏང་།", + "workspace.new": "ལཱ་གི་ས་སྒོ་གསརཔ།", + "workspace.type.local": "ས་གནས་ཀྱི", + "workspace.type.sandbox": "བྱེ་སྒམ།", + "workspace.create.failed.title": "ལཱ་གི་ས་སྒོ་གསར་བསྐྲུན་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "workspace.delete.failed.title": "ལཱ་གི་ས་སྒོ་བཏོན་གཏང་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "workspace.resetting.title": "ལཱ་གི་ས་སྒོ་སླར་སྒྲིག་འབད་དོ།", + "workspace.resetting.description": "འདི་སྐར་མ་གཅིག་འགོར་འོང་།", + "workspace.reset.failed.title": "ལཱ་གི་ས་སྒོ་སླར་སྒྲིག་འབད་ནི་ལུ་འཐུས་ཤོར་འབྱུང་ཡོདཔ།", + "workspace.reset.success.title": "ལཱ་གི་ས་སྒོ་སླར་སྒྲིག་འབད།", + "workspace.reset.success.description": "ལཱ་གི་ས་སྒོ་འདི་ད་ལྟོ་སྔོན་སྒྲིག་ཡན་ལག་ལུ་མཐུན་སྒྲིག་འབདཝ་ཨིན།", + "workspace.error.stillPreparing": "ལཱ་གི་ས་སྒོ་འདི་ ད་ལྟོ་ཡང་གྲ་སྒྲིག་འབད་དོ།", + "workspace.status.checking": "མཉམ་བསྡོམས་མ་འབད་བའི་བསྒྱུར་བཅོས་ཚུ་ཞིབ་དཔྱད་འབད་དོ...", + "workspace.status.error": "ཇིཊི་གནས་ཚད་བདེན་སྦྱོར་འབད་མ་ཚུགས།", + "workspace.status.clean": "མཉམ་བསྡོམས་མ་འབད་བའི་བསྒྱུར་བཅོས་ཚུ་ སྐྱོན་འཛིན་མ་འབད་བས།", + "workspace.status.dirty": "ལཱ་གི་ས་སྒོ་འདི་ནང་མཉམ་བསྡོམས་མ་འབད་བའི་བསྒྱུར་བཅོས་ཚུ་སྐྱོན་འཛིན་འབད་ཡོདཔ།", + "workspace.delete.title": "ལཱ་གི་ས་སྒོ་བཏོན་གཏང་།", + "workspace.delete.confirm": 'ལཱ་གི་ས་སྒོ་ "{{name}}" བཏོན་གཏང་ནི་ཨིན་ན?', + "workspace.delete.button": "ལཱ་གི་ས་སྒོ་བཏོན་གཏང་།", + "workspace.reset.title": "ལཱ་གི་ས་སྒོ་སླར་སྒྲིག་འབད།", + "workspace.reset.confirm": 'ལཱ་གི་ས་སྒོ་ "{{name}}" སླར་སྒྲིག་འབད་ནི་ཨིན་ན?', + "workspace.reset.button": "ལཱ་གི་ས་སྒོ་སླར་སྒྲིག་འབད།", + "workspace.reset.archived.none": "ཤུགས་ལྡན་ལཱ་ཡུན་ཚུ་གཏན་མཛོད་མི་འབད།", + "workspace.reset.archived.one": "ལཱ་ཡུན་ ༡ གཏན་མཛོད་ནང་བཞག་འོང་།", + "workspace.reset.archived.many": "{{count}} ལཱ་ཡུན་ཚུ་ཡིག་མཛོད་ནང་བཞག་འོང་།", + "workspace.reset.note": "འདི་གིས་ སྔོན་སྒྲིག་ཡན་ལག་མཐུན་སྒྲིག་འབད་ནི་ལུ་ ལཱ་གི་ས་སྒོ་འདི་སླར་སྒྲིག་འབད་འོང་།", + "dialog.usageExceeded.dontShowAgain": "ལོག་སྟེ་མ་སྟོན།", +} diff --git a/packages/app/src/i18n/el.ts b/packages/app/src/i18n/el.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f91b46af010fd3103e1de9dbba79163bed3a149 --- /dev/null +++ b/packages/app/src/i18n/el.ts @@ -0,0 +1,1173 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Αρχείο", + "desktop.menu.edit": "Επεξεργασία", + "desktop.menu.view": "Προβολή", + "desktop.menu.go": "Μετάβαση", + "desktop.menu.window": "Παράθυρο", + "desktop.menu.help": "Βοήθεια", + "desktop.menu.checkForUpdates": "Έλεγχος για ενημερώσεις...", + "desktop.menu.settings": "Ρυθμίσεις", + "desktop.menu.reloadWebview": "Επανάληψη φόρτωσης Webview", + "desktop.menu.restart": "Επανεκκίνηση", + "desktop.menu.exportLogs": "Εξαγωγή αρχείων καταγραφής...", + "desktop.menu.newSession": "Νέα συνεδρία", + "desktop.menu.openProject": "Άνοιγμα έργου...", + "desktop.menu.newWindow": "Νέο παράθυρο", + "desktop.menu.closeWindow": "Κλείσιμο παραθύρου", + "desktop.menu.undo": "Αναίρεση", + "desktop.menu.redo": "Επανάληψη", + "desktop.menu.cut": "Αποκοπή", + "desktop.menu.copy": "Αντιγραφή", + "desktop.menu.paste": "Επικόλληση", + "desktop.menu.delete": "Διαγραφή", + "desktop.menu.selectAll": "Επιλογή όλων", + "desktop.menu.toggleSidebar": "Εναλλαγή πλευρικής γραμμής", + "desktop.menu.toggleTerminal": "Εναλλαγή τερματικού", + "desktop.menu.toggleFileTree": "Εναλλαγή δέντρου αρχείου", + "desktop.menu.reload": "Επανάληψη φόρτωσης", + "desktop.menu.toggleDeveloperTools": "Εναλλαγή εργαλείων προγραμματιστή", + "desktop.menu.actualSize": "Πραγματικό μέγεθος", + "desktop.menu.zoomIn": "Μεγέθυνση", + "desktop.menu.zoomOut": "Σμίκρυνση", + "desktop.menu.toggleFullScreen": "Εναλλαγή πλήρους οθόνης", + "desktop.menu.back": "Πίσω", + "desktop.menu.forward": "Προώθηση", + "desktop.menu.previousSession": "Προηγούμενη συνεδρία", + "desktop.menu.nextSession": "Επόμενη συνεδρία", + "desktop.menu.previousProject": "Προηγούμενο έργο", + "desktop.menu.nextProject": "Επόμενο έργο", + "desktop.menu.minimize": "Ελαχιστοποίηση", + "desktop.menu.maximize": "Μεγιστοποίηση", + "desktop.menu.documentation": "OpenCode Τεκμηρίωση", + "desktop.menu.supportForum": "Φόρουμ υποστήριξης", + "desktop.menu.shareFeedback": "Κοινή χρήση σχολίων", + "desktop.menu.reportBug": "Αναφορά σφάλματος", + "desktop.menu.ariaLabel": "OpenCode μενού", + "desktop.updater.dialog.checkFailed.message": "Ο έλεγχος ενημέρωσης απέτυχε.", + "desktop.updater.dialog.checkFailed.title": "Σφάλμα ενημέρωσης", + "desktop.updater.dialog.upToDate.message": "Είστε ενημερωμένοι.", + "desktop.updater.dialog.upToDate.title": "Χωρίς ενημερώσεις", + "desktop.updater.dialog.ready.message": "Η ενημέρωση {{version}} λήφθηκε. Επανεκκίνηση τώρα;", + "desktop.updater.dialog.ready.title": "Έτοιμη ενημέρωση", + "desktop.updater.dialog.restart": "Επανεκκίνηση", + "desktop.updater.dialog.later": "Αργότερα", + "desktop.recovery.action.relaunch": "Επανεκκίνηση", + "desktop.recovery.action.exportLogs": "Εξαγωγή αρχείων καταγραφής", + "desktop.recovery.action.keepWaiting": "Συνεχίστε την αναμονή", + "desktop.recovery.action.quit": "Τερματισμός", + "desktop.recovery.loadFailed": "OpenCode απέτυχε η φόρτωση", + "desktop.recovery.terminated": "OpenCode το παράθυρο τερματίστηκε απροσδόκητα", + "desktop.recovery.unresponsive": "OpenCode δεν αποκρίνεται", + "desktop.recovery.unresponsive.detail": + "Μπορείτε να επανεκκινήσετε την εφαρμογή, να ανοίξετε τα αρχεία καταγραφής ή να συνεχίσετε να περιμένετε.", + "desktop.recovery.loadFailed.detail": "Παράθυρο: {{window}}\nURL: {{url}}\nΣφάλμα: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Παράθυρο: {{window}}\nΑιτία: {{reason}}\nΚωδικός: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Επιλογή φακέλου", + "desktop.dialog.chooseFile": "Επιλογή αρχείου", + "desktop.dialog.saveFile": "Αποθήκευση αρχείου", + "desktop.dialog.files": "Αρχεία", + "desktop.server.local": "Τοπικός διακομιστής", + "desktop.wsl.error.windowsOnly": "WSL είναι διαθέσιμο μόνο στις Windows", + "desktop.wsl.error.unavailable": "WSL δεν είναι διαθέσιμο", + "desktop.wsl.error.listInstalled": "Απέτυχε η λίστα εγκατεστημένων WSL διανομών", + "desktop.wsl.error.listOnline": "Απέτυχε η λίστα με τις διαδικτυακές διανομές WSL", + "desktop.wsl.error.executeDistro": "Δεν είναι δυνατή η εκτέλεση εντολών στη διανομή", + "desktop.wsl.error.installWsl": "WSL η εγκατάσταση απέτυχε", + "desktop.wsl.error.installDistro": "Αποτυχία εγκατάστασης διανομής: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode η εγκατάσταση απέτυχε", + "desktop.wsl.error.alreadyAdded": "{{distro}} έχει ήδη προστεθεί", + "desktop.wsl.error.opencodeMissing": "ο ανοιχτός κώδικας δεν είναι εγκατεστημένος σε αυτήν τη διανομή", + "desktop.wsl.error.opencodeCannotRun": "ο ανοιχτός κώδικας είναι εγκατεστημένος αλλά δεν ήταν δυνατή η εκτέλεση", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode δεν είναι εγκατεστημένο στο {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode η ενημέρωση ολοκληρώθηκε αλλά το {{distro}} εξακολουθεί να αναφέρει {{installed}}; αναμενόμενο {{expected}}", + "desktop.wsl.error.noVersion": "χωρίς έκδοση", + "desktop.wsl.error.serverExited": "WSL έξοδος διακομιστή μετά την εκκίνηση (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL ο διακομιστής εξήλθε πριν γίνει υγιής (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Το πλευρικό καρότσι για έλεγχο υγείας {{distro}} έληξε μετά από {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} έληξε μετά από {{timeout}}ms", + "desktop.wsl.error.failedPort": "Αποτυχία λήψης θύρας", + "desktop.picker.error.notSelected": "Το αρχείο δεν επιλέχθηκε από τον επιλογέα", + "desktop.picker.error.sizeLimit": "Τα επιλεγμένα συνημμένα υπερβαίνουν το όριο των {{limit}} MB", + "command.category.suggested": "Προτεινόμενο", + "command.category.view": "Προβολή", + "command.category.project": "Έργο", + "command.category.provider": "Πάροχος", + "command.category.server": "Διακομιστής", + "command.category.session": "Συνεδρία", + "command.category.theme": "Θέμα", + "command.category.language": "Γλώσσα", + "command.category.file": "Αρχείο", + "command.category.context": "Πλαίσιο", + "command.category.terminal": "Τερματικό", + "command.category.model": "Μοντέλο", + "command.category.mcp": "MCP", + "command.category.agent": "Πράκτορας", + "command.category.permissions": "Δικαιώματα", + "command.category.workspace": "Χώρος εργασίας", + "command.category.settings": "Ρυθμίσεις", + "command.logs.export": "Εξαγωγή αρχείων καταγραφής", + "theme.scheme.system": "Σύστημα", + "theme.scheme.light": "Φωτεινό", + "theme.scheme.dark": "Σκούρο", + "command.sidebar.toggle": "Εναλλαγή πλαϊνής γραμμής", + "command.project.open": "Άνοιγμα έργου", + "command.project.previous": "Προηγούμενο έργο", + "command.project.next": "Επόμενο έργο", + "command.project.index": "Εναλλαγή στο έργο {{index}}", + "command.provider.connect": "Σύνδεση παρόχου", + "command.server.switch": "Εναλλαγή διακομιστή", + "command.settings.open": "Άνοιγμα ρυθμίσεων", + "command.session.previous": "Προηγούμενη συνεδρία", + "command.session.next": "Επόμενη συνεδρία", + "command.session.previous.unseen": "Προηγούμενη μη αναγνωσμένη συνεδρία", + "command.session.next.unseen": "Επόμενη μη αναγνωσμένη συνεδρία", + "command.session.archive": "Αρχειοθέτηση συνεδρίας", + "command.palette": "Παλέτα εντολών", + "command.theme.cycle": "Θέμα κύκλου", + "command.theme.set": "Χρήση θέματος: {{theme}}", + "command.theme.scheme.cycle": "Χρωματικός συνδυασμός κύκλου", + "command.theme.scheme.set": "Χρήση συνδυασμού χρωμάτων: {{scheme}}", + "command.language.cycle": "Γλώσσα κύκλου", + "command.language.set": "Γλώσσα χρήσης: {{language}}", + "command.session.new": "Νέα συνεδρία", + "command.file.open": "Άνοιγμα αρχείου", + "command.tab.close": "Κλείσιμο καρτέλας", + "command.tab.reopenClosed": "Άνοιγμα ξανά κλειστής καρτέλας", + "command.context.addSelection": "Προσθήκη επιλογής στο πλαίσιο", + "command.context.addSelection.description": "Προσθήκη επιλεγμένων γραμμών από το τρέχον αρχείο", + "command.input.focus": "Εισαγωγή εστίασης", + "command.terminal.toggle": "Εναλλαγή τερματικού", + "command.fileTree.toggle": "Εναλλαγή δέντρου αρχείου", + "command.review.toggle": "Εναλλαγή αναθεώρησης", + "command.terminal.new": "Νέο τερματικό", + "command.terminal.new.description": "Δημιουργία νέας καρτέλας τερματικού", + "command.steps.toggle": "Εναλλαγή βημάτων", + "command.steps.toggle.description": "Εμφάνιση ή απόκρυψη βημάτων για το τρέχον μήνυμα", + "command.message.previous": "Προηγούμενο μήνυμα", + "command.message.previous.description": "Μετάβαση στο προηγούμενο μήνυμα χρήστη", + "command.message.next": "Επόμενο μήνυμα", + "command.message.next.description": "Μετάβαση στο επόμενο μήνυμα χρήστη", + "command.model.choose": "Επιλογή μοντέλου", + "command.model.choose.description": "Επιλέξτε διαφορετικό μοντέλο", + "command.mcp.toggle": "Εναλλαγή MCPs", + "command.mcp.toggle.description": "Εναλλαγή MCPs", + "command.agent.cycle": "Εναλλαγή πράκτορα", + "command.agent.cycle.description": "Μετάβαση στον επόμενο πράκτορα", + "command.agent.cycle.reverse": "Μετάβαση στον προηγούμενο πράκτορα", + "command.agent.cycle.reverse.description": "Εναλλαγή στον προηγούμενο πράκτορα", + "command.model.variant.cycle": "Εναλλαγή επιπέδου συλλογιστικής", + "command.model.variant.cycle.description": "Μετάβαση στο επόμενο επίπεδο προσπάθειας", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Προτροπή", + "command.permissions.autoaccept.enable": "Αυτόματη αποδοχή δικαιωμάτων", + "command.permissions.autoaccept.disable": "Διακοπή αυτόματης αποδοχής δικαιωμάτων", + "command.workspace.toggle": "Εναλλαγή χώρων εργασίας", + "command.workspace.toggle.description": "Ενεργοποίηση ή απενεργοποίηση πολλών χώρων εργασίας στην πλαϊνή γραμμή", + "command.session.undo": "Αναίρεση", + "command.session.undo.description": "Αναίρεση του τελευταίου μηνύματος", + "command.session.redo": "Επανάληψη", + "command.session.redo.description": "Επανάληψη του τελευταίου μηνύματος που αναιρέθηκε", + "command.session.compact": "Συμπαγής συνεδρία", + "command.session.compact.description": "Συνοψήστε τη συνεδρία για να μειώσετε το μέγεθος του περιβάλλοντος", + "command.session.fork": "Διακλάδωση από μήνυμα", + "command.session.fork.description": "Δημιουργία νέας συνεδρίας από προηγούμενο μήνυμα", + "command.session.share": "Κοινή χρήση συνεδρίας", + "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": "Δεν βρέθηκαν αποτελέσματα", + "palette.group.commands": "Εντολές", + "palette.group.files": "Αρχεία", + "dialog.provider.search.placeholder": "Αναζήτηση παρόχων", + "dialog.provider.empty": "Δεν βρέθηκαν πάροχοι", + "dialog.provider.group.popular": "Δημοφιλή", + "dialog.provider.group.other": "Άλλο", + "dialog.provider.custom.label": "Προσαρμοσμένη OpenAI-συμβατός πάροχος", + "dialog.provider.tag.recommended": "Συνιστάται", + "dialog.provider.opencode.note": "Επιμελημένα μοντέλα συμπεριλαμβανομένων των Claude, GPT, Gemini και άλλων", + "dialog.provider.opencode.tagline": "Αξιόπιστα βελτιστοποιημένα μοντέλα", + "dialog.provider.opencodeGo.tagline": "Συνδρομή χαμηλού κόστους για όλους", + "dialog.provider.anthropic.note": "Άμεση πρόσβαση σε Claude μοντέλα, συμπεριλαμβανομένων των Pro και Max", + "dialog.provider.copilot.note": "Μοντέλα AI για βοήθεια κωδικοποίησης μέσω GitHub Copilot", + "dialog.provider.openai.note": "GPT μοντέλα για γρήγορες, ικανές εργασίες γενικής τεχνητής νοημοσύνης", + "dialog.provider.google.note": "Gemini μοντέλα για γρήγορες, δομημένες απαντήσεις", + "dialog.provider.openrouter.note": "Πρόσβαση σε όλα τα υποστηριζόμενα μοντέλα από έναν πάροχο", + "dialog.provider.vercel.note": "Ενοποιημένη πρόσβαση σε μοντέλα τεχνητής νοημοσύνης με έξυπνη δρομολόγηση", + "dialog.model.select.title": "Επιλογή μοντέλου", + "dialog.model.search.placeholder": "Αναζήτηση μοντέλων", + "dialog.model.empty": "Δεν υπάρχουν αποτελέσματα μοντέλου", + "dialog.model.manage": "Διαχείριση μοντέλων", + "dialog.model.manage.description": "Προσαρμογή των μοντέλων που εμφανίζονται στον επιλογέα μοντέλων.", + "dialog.model.manage.provider.toggle": "Εναλλαγή όλων των {{provider}} μοντέλων", + "dialog.model.unpaid.freeModels.title": "Δωρεάν μοντέλα παρέχονται από OpenCode", + "dialog.model.unpaid.addMore.title": "Προσθήκη περισσότερων μοντέλων από δημοφιλείς παρόχους", + "dialog.model.unpaid.viewMoreProviders": "Δείτε 70+ ακόμη παρόχους", + "dialog.provider.viewAll": "Εμφάνιση περισσότερων παρόχων", + "provider.connect.title": "Σύνδεση {{provider}}", + "provider.connect.title.anthropicProMax": "Σύνδεση με Claude Pro/Max", + "provider.connect.selectMethod": "Επιλογή μεθόδου σύνδεσης για {{provider}}.", + "provider.connect.method.apiKey": "API κλειδί", + "provider.connect.method.browser": "Πρόγραμμα περιήγησης", + "provider.connect.method.headless": "Χωρίς γραφικό περιβάλλον", + "provider.connect.status.inProgress": "Εξουσιοδότηση σε εξέλιξη...", + "provider.connect.status.waiting": "Αναμονή για εξουσιοδότηση...", + "provider.connect.status.failed": "Η εξουσιοδότηση απέτυχε: {{error}}", + "provider.connect.apiKey.description": + "Εισαγάγετε το κλειδί {{provider}} API για να συνδέσετε τον λογαριασμό σας και χρησιμοποιήστε μοντέλα {{provider}} στο OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API κλειδί", + "provider.connect.apiKey.placeholder": "API κλειδί", + "provider.connect.apiKey.required": "API απαιτείται κλειδί", + "provider.connect.opencodeZen.line1": + "OpenCode Το Zen σάς δίνει πρόσβαση σε ένα επιμελημένο σύνολο αξιόπιστων βελτιστοποιημένων μοντέλων για πράκτορες κωδικοποίησης.", + "provider.connect.opencodeZen.line2": + "Με ένα μόνο κλειδί API θα έχετε πρόσβαση σε μοντέλα όπως Claude, GPT, Gemini, GLM και άλλα.", + "provider.connect.opencodeZen.visit.prefix": "Επίσκεψη ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " για τη συλλογή του κλειδιού σας API.", + "provider.connect.oauth.code.visit.prefix": "Επίσκεψη ", + "provider.connect.oauth.code.visit.link": "αυτός ο σύνδεσμος", + "provider.connect.oauth.code.visit.suffix": + " για να συλλέξετε τον κωδικό εξουσιοδότησης για να συνδέσετε τον λογαριασμό σας και να χρησιμοποιήσετε {{provider}} μοντέλα στο OpenCode.", + "provider.connect.oauth.code.label": "{{method}} κωδικός εξουσιοδότησης", + "provider.connect.oauth.code.placeholder": "Κωδικός εξουσιοδότησης", + "provider.connect.oauth.code.required": "Απαιτείται κωδικός εξουσιοδότησης", + "provider.connect.oauth.code.invalid": "Μη έγκυρος κωδικός εξουσιοδότησης", + "provider.connect.oauth.auto.visit.prefix": "Επίσκεψη ", + "provider.connect.oauth.auto.visit.link": "αυτός ο σύνδεσμος", + "provider.connect.oauth.auto.visit.suffix": + " και εισαγάγετε τον παρακάτω κωδικό για να συνδέσετε τον λογαριασμό σας και να χρησιμοποιήσετε {{provider}} μοντέλα στο OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Κωδικός επιβεβαίωσης", + "provider.connect.toast.connected.title": "{{provider}} συνδεδεμένο", + "provider.connect.toast.connected.description": "{{provider}} μοντέλα είναι πλέον διαθέσιμα για χρήση.", + "provider.custom.title": "Προσαρμοσμένος πάροχος", + "provider.custom.unavailable": "Οι προσαρμοσμένοι πάροχοι δεν είναι διαθέσιμοι σε αυτόν τον διακομιστή", + "provider.custom.description.prefix": "Διαμορφώστε έναν πάροχο συμβατό με OpenAI. Δείτε το ", + "provider.custom.description.link": "έγγραφα διαμόρφωσης παρόχου", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Αναγνωριστικό παρόχου", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Πεζά γράμματα, αριθμοί, παύλες ή κάτω παύλες", + "provider.custom.field.name.label": "Εμφανιζόμενο όνομα", + "provider.custom.field.name.placeholder": "Ο πάροχος τεχνητής νοημοσύνης μου", + "provider.custom.field.baseURL.label": "Βάση URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API κλειδί", + "provider.custom.field.apiKey.placeholder": "API κλειδί", + "provider.custom.field.apiKey.description": + "Προαιρετικό. Αφήστε κενό εάν διαχειρίζεστε την ταυτότητα μέσω κεφαλίδων.", + "provider.custom.models.label": "Μοντέλα", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Όνομα", + "provider.custom.models.name.placeholder": "Εμφανιζόμενο όνομα", + "provider.custom.models.remove": "Κατάργηση μοντέλου", + "provider.custom.models.add": "Προσθήκη μοντέλου", + "provider.custom.headers.label": "Κεφαλίδες (προαιρετικά)", + "provider.custom.headers.key.label": "Κεφαλίδα", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Τιμή", + "provider.custom.headers.value.placeholder": "τιμή", + "provider.custom.headers.remove": "Κατάργηση κεφαλίδας", + "provider.custom.headers.add": "Προσθήκη κεφαλίδας", + "provider.custom.error.providerID.required": "Απαιτείται αναγνωριστικό παρόχου", + "provider.custom.error.providerID.format": "Χρησιμοποιήστε πεζά γράμματα, αριθμούς, παύλες ή κάτω παύλες", + "provider.custom.error.providerID.exists": "Αυτό το αναγνωριστικό παρόχου υπάρχει ήδη", + "provider.custom.error.name.required": "Απαιτείται το εμφανιζόμενο όνομα", + "provider.custom.error.baseURL.required": "Η βάση URL απαιτείται", + "provider.custom.error.baseURL.format": "Πρέπει να ξεκινά με http:// ή https://", + "provider.custom.error.required": "Απαιτείται", + "provider.custom.error.duplicate": "Διπλότυπο", + "provider.disconnect.toast.disconnected.title": "{{provider}} αποσυνδέθηκε", + "provider.disconnect.toast.disconnected.description": "{{provider}} μοντέλα δεν είναι πλέον διαθέσιμα.", + "model.tag.free": "Δωρεάν", + "model.tag.latest": "Τελευταία", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "κείμενο", + "model.input.image": "εικόνα", + "model.input.audio": "ήχος", + "model.input.video": "βίντεο", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Επιτρέπει: {{inputs}}", + "model.tooltip.context.label": "Πλαίσιο", + "model.tooltip.inputs": "Είσοδοι", + "model.tooltip.model": "Μοντέλο", + "model.tooltip.provider": "Πάροχος", + "model.tooltip.reasoning": "Συλλογισμός", + "model.tooltip.reasoning.allowed": "Επιτρέπει τη συλλογιστική", + "model.tooltip.reasoning.none": "Χωρίς αιτιολογία", + "model.tooltip.context": "Όριο περιβάλλοντος {{limit}}", + "common.search.placeholder": "Αναζήτηση", + "common.goBack": "Πλοήγηση πίσω", + "common.goForward": "Πλοήγηση προς τα εμπρός", + "common.loading": "Φόρτωση", + "common.loading.ellipsis": "...", + "common.cancel": "Ακύρωση", + "common.clear": "Διαγραφή", + "common.open": "Άνοιγμα", + "common.connect": "Σύνδεση", + "common.disconnect": "Αποσύνδεση", + "common.continue": "Συνέχεια", + "common.submit": "Υποβολή", + "common.save": "Αποθήκευση", + "common.saving": "Αποθήκευση...", + "common.default": "Προεπιλογή", + "common.attachment": "συνημμένο", + "prompt.placeholder.shell": "Εισαγωγή εντολής κελύφους... {{example}}", + "prompt.placeholder.normal": 'Ρωτήστε οτιδήποτε... "{{example}}"', + "prompt.placeholder.simple": "Ρωτήστε οτιδήποτε...", + "prompt.placeholder.summarizeComments": "Σύνοψη σχολίων…", + "prompt.placeholder.summarizeComment": "Σύνοψη σχολίου…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Προτροπή", + "prompt.mode.shell.exit": "esc για έξοδο", + "session.child.promptDisabled": "Δεν είναι δυνατή η αποστολή προτροπών σε συνεδρίες υποπρακτόρων.", + "session.child.backToParent": "Επιστροφή στην κύρια συνεδρία.", + "prompt.example.1": "Διόρθωση TODO στη βάση κώδικα", + "prompt.example.2": "Ποια είναι η τεχνική στοίβα αυτού του έργου;", + "prompt.example.3": "Διόρθωση σπασμένων δοκιμών", + "prompt.example.4": "Εξηγήστε πώς λειτουργεί ο έλεγχος ταυτότητας", + "prompt.example.5": "Εύρεση και διόρθωση ευπαθειών ασφαλείας", + "prompt.example.6": "Προσθήκη δοκιμών μονάδας για την υπηρεσία χρήστη", + "prompt.example.7": "Επανασκευάστε αυτήν τη λειτουργία για να είναι πιο ευανάγνωστη", + "prompt.example.8": "Τι σημαίνει αυτό το σφάλμα;", + "prompt.example.9": "Βοηθήστε με να διορθώσω αυτό το ζήτημα", + "prompt.example.10": "Δημιουργία API τεκμηρίωσης", + "prompt.example.11": "Βελτιστοποίηση ερωτημάτων βάσης δεδομένων", + "prompt.example.12": "Προσθήκη επικύρωσης εισόδου", + "prompt.example.13": "Δημιουργία νέου στοιχείου για...", + "prompt.example.14": "Πώς μπορώ να αναπτύξω αυτό το έργο;", + "prompt.example.15": "Ελέγξτε τον κώδικά μου για βέλτιστες πρακτικές", + "prompt.example.16": "Προσθήκη χειρισμού σφαλμάτων σε αυτήν τη λειτουργία", + "prompt.example.17": "Εξηγήστε αυτό το regex μοτίβο", + "prompt.example.18": "Μετατροπή αυτού σε TypeScript", + "prompt.example.19": "Προσθήκη καταγραφής σε όλη τη βάση κώδικα", + "prompt.example.20": "Ποιες εξαρτήσεις είναι ξεπερασμένες;", + "prompt.example.21": "Βοηθήστε με να γράψω ένα σενάριο μετεγκατάστασης", + "prompt.example.22": "Εφαρμογή προσωρινής αποθήκευσης για αυτό το τελικό σημείο", + "prompt.example.23": "Προσθήκη σελιδοποίησης σε αυτήν τη λίστα", + "prompt.example.24": "Δημιουργία εντολής CLI για...", + "prompt.example.25": "Πώς λειτουργούν οι μεταβλητές περιβάλλοντος εδώ;", + "prompt.popover.emptyResults": "Δεν υπάρχουν αποτελέσματα", + "prompt.popover.emptyCommands": "Δεν ταιριάζουν εντολές", + "prompt.dropzone.label": "Αποθέστε εικόνες, αρχεία PDF ή αρχεία κειμένου εδώ", + "prompt.dropzone.file.label": "Αποβίβαση στο αρχείο @mention", + "prompt.slash.badge.custom": "προσαρμοσμένο", + "prompt.slash.badge.skill": "ικανότητα", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "ενεργό", + "prompt.context.includeActiveFile": "Συμπεριλάβετε ενεργό αρχείο", + "prompt.context.removeActiveFile": "Κατάργηση ενεργού αρχείου από το περιβάλλον", + "prompt.context.removeFile": "Κατάργηση αρχείου από το περιβάλλον", + "prompt.action.attachFile": "Προσθήκη αρχείων", + "prompt.menu.addImagesAndFiles": "Προσθήκη αρχείων και άλλα", + "prompt.menu.imagesAndFiles": "Εικόνες και αρχεία", + "prompt.menu.commands": "Εντολές", + "prompt.menu.context": "Πλαίσιο", + "prompt.menu.shellCommand": "εντολή Shell", + "prompt.attachment.remove": "Κατάργηση συνημμένου", + "prompt.action.send": "Αποστολή", + "prompt.action.stop": "Διακοπή", + "prompt.toast.pasteUnsupported.title": "Μη υποστηριζόμενο συνημμένο", + "prompt.toast.pasteUnsupported.description": "Εδώ επισυνάπτονται μόνο εικόνες, αρχεία PDF ή αρχεία κειμένου.", + "prompt.toast.attachmentDuplicate.title": "Αυτό το αρχείο έχει ήδη μεταφορτωθεί", + "prompt.toast.modelAgentRequired.title": "Επιλέξτε έναν πράκτορα και μοντέλο", + "prompt.toast.modelAgentRequired.description": + "Επιλέξτε έναν πράκτορα και ένα μοντέλο πριν στείλετε ένα μήνυμα προτροπής.", + "prompt.toast.worktreeCreateFailed.title": "Αποτυχία δημιουργίας δέντρου εργασίας", + "prompt.toast.sessionCreateFailed.title": "Αποτυχία δημιουργίας συνεδρίας", + "prompt.toast.shellSendFailed.title": "Αποτυχία αποστολής εντολής κελύφους", + "prompt.toast.commandSendFailed.title": "Αποτυχία αποστολής εντολής", + "prompt.toast.promptSendFailed.title": "Αποτυχία αποστολής προτροπής", + "prompt.toast.promptSendFailed.description": "Δεν είναι δυνατή η ανάκτηση της συνεδρίας", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} από {{total}} ενεργοποιημένα", + "dialog.mcp.empty": "Δεν έχει διαμορφωθεί το MCPs", + "dialog.lsp.empty": "LSPs ανιχνεύεται αυτόματα από τύπους αρχείων", + "dialog.plugins.empty": "Προσθήκες ρυθμισμένες στο opencode.json", + "mcp.status.connected": "συνδεδεμένος", + "mcp.status.failed": "απέτυχε", + "mcp.status.needs_auth": "χρειάζεται εξουσιοδότηση", + "mcp.status.disabled": "απενεργοποιημένο", + "mcp.auth.clickToAuthenticate": "Κάντε κλικ για έλεγχο ταυτότητας", + "dialog.fork.empty": "Δεν υπάρχουν μηνύματα για διακλάδωση", + "dialog.directory.search.placeholder": "Αναζήτηση φακέλων", + "dialog.directory.empty": "Δεν βρέθηκαν φάκελοι", + "dialog.directory.action.selectFile": "Επιλογή αρχείου", + "dialog.directory.action.selectFolder": "Επιλογή φακέλου", + "dialog.directory.root": "Ρίζα", + "dialog.directory.parent": "Γονικός", + "dialog.directory.readError": "Δεν είναι δυνατή η ανάγνωση αυτού του φακέλου", + "app.server.unreachable": "Δεν ήταν δυνατή η πρόσβαση στο {{server}}", + "app.server.retrying": "Αυτόματη προσπάθεια επανάληψης...", + "app.server.otherServers": "Άλλοι διακομιστές", + "dialog.server.title": "Διακομιστές", + "dialog.server.description": "Αλλαγή σε ποιον διακομιστή OpenCode συνδέεται αυτή η εφαρμογή.", + "dialog.server.search.placeholder": "Αναζήτηση διακομιστών", + "dialog.server.empty": "Δεν υπάρχουν ακόμη διακομιστές", + "dialog.server.add.title": "Προσθήκη διακομιστή", + "dialog.server.add.url": "Διεύθυνση διακομιστή", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Δεν ήταν δυνατή η σύνδεση με τον διακομιστή", + "dialog.server.add.checking": "Έλεγχος...", + "dialog.server.add.button": "Προσθήκη διακομιστή", + "dialog.server.add.name": "Όνομα διακομιστή (προαιρετικό)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Όνομα χρήστη (προαιρετικό)", + "dialog.server.add.usernamePlaceholder": "όνομα χρήστη", + "dialog.server.add.password": "Κωδικός πρόσβασης (προαιρετικό)", + "dialog.server.add.passwordPlaceholder": "κωδικός πρόσβασης", + "dialog.server.edit.title": "Επεξεργασία διακομιστή", + "dialog.server.default.title": "Προεπιλεγμένος διακομιστής", + "dialog.server.default.description": + "Συνδεθείτε σε αυτόν τον διακομιστή κατά την εκκίνηση της εφαρμογής αντί να ξεκινήσετε έναν τοπικό διακομιστή. Απαιτείται επανεκκίνηση.", + "dialog.server.default.none": "Δεν έχει επιλεγεί διακομιστής", + "dialog.server.default.set": "Ορισμός τρέχοντος διακομιστή ως προεπιλογής", + "dialog.server.default.clear": "Διαγραφή", + "dialog.server.action.remove": "Κατάργηση διακομιστή", + "dialog.server.menu.edit": "Επεξεργασία", + "dialog.server.menu.default": "Ορισμός ως προεπιλογή", + "dialog.server.menu.defaultRemove": "Κατάργηση προεπιλογής", + "dialog.server.menu.delete": "Διαγραφή", + "dialog.server.current": "Τρέχον διακομιστής", + "dialog.server.status.default": "Προεπιλογή", + "wsl.server.add": "Προσθήκη διακομιστή WSL", + "wsl.server.addShort": "Προσθήκη WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL διακομιστής", + "wsl.server.retryStart": "Επανάληψη έναρξης", + "wsl.server.updating": "Ενημέρωση...", + "wsl.onboarding.step.distro": "Επιλογή διανομής", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Έλεγχος WSL...", + "wsl.onboarding.restartRequired": "Windows χρειάζεται επανεκκίνηση για να ολοκληρωθεί η εγκατάσταση του WSL.", + "wsl.onboarding.ready": "WSL είναι έτοιμο.", + "wsl.onboarding.required": "WSL απαιτείται για να συνεχίσετε.", + "wsl.onboarding.checkingDistros": "Έλεγχος διανομών...", + "wsl.onboarding.installingDistro": "Εγκατάσταση του {{distro}}...", + "wsl.onboarding.checkingDistro": "Έλεγχος {{distro}}...", + "wsl.onboarding.listingDistros": "Διανομές λίστας...", + "wsl.onboarding.distroReady": "{{distro}} είναι έτοιμο.", + "wsl.onboarding.distroNotInstalled": "{{distro}} δεν έχει εγκατασταθεί ακόμα.", + "wsl.onboarding.openDistroOnce": "Ανοίξτε το {{distro}} μία φορά για να ολοκληρώσετε τη ρύθμιση.", + "wsl.onboarding.finishingDistro": "Ολοκλήρωση της ρύθμισης για το {{distro}}.", + "wsl.onboarding.pickDistro": "Επιλέξτε μια διανομή ή εγκαταστήστε μία παρακάτω.", + "wsl.onboarding.checkingOpencode": "Έλεγχος OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Έλεγχος OpenCode σε {{distro}}...", + "wsl.onboarding.updatingOpencode": "Ενημέρωση OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Ενημέρωση OpenCode σε {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Ενημέρωση OpenCode σε {{distro}}.", + "wsl.onboarding.updateOpencode": "Ενημέρωση OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode είναι έτοιμο στο {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode είναι έτοιμο.", + "wsl.onboarding.installOpencodeIn": "Εγκατάσταση του OpenCode στο {{distro}}.", + "wsl.onboarding.installOpencode": "Εγκατάσταση OpenCode", + "wsl.onboarding.chooseDistroFirst": "Επιλέξτε πρώτα μια διανομή.", + "wsl.onboarding.loadFailed": "Απέτυχε η φόρτωση της κατάστασης WSL.", + "wsl.onboarding.loading": "Φόρτωση...", + "wsl.onboarding.installedDistros": "Εγκατεστημένες διανομές", + "wsl.onboarding.checkAgain": "Έλεγχος ξανά", + "wsl.onboarding.distroStatus.ready": "Έτοιμο", + "wsl.onboarding.distroStatus.checking": "Έλεγχος...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode δεν έχει εγκατασταθεί", + "wsl.onboarding.distroStatus.missingTools": "Λείπει το bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Δεν υποστηρίζεται · Χρήση WSL 2", + "wsl.onboarding.needAnotherDistro": "Χρειάζεστε άλλη διανομή;", + "wsl.onboarding.needAnotherDistroHint": "Εγκαταστήστε μια διανομή Linux από τον κατάλογο WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL δεν έχει εγκατασταθεί", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Υποσύστημα για Linux) απαιτείται για να μπορέσει ο OpenCode να προσθέσει έναν διακομιστή WSL", + "wsl.onboarding.wslUnavailable.title": "WSL μη διαθέσιμο", + "wsl.onboarding.wslUnavailable.description": "OpenCode δεν μπόρεσε να επαληθεύσει το WSL σε αυτό το μηχάνημα.", + "wsl.onboarding.installWsl": "Εγκατάσταση WSL", + "wsl.onboarding.windowsRestartRequired": + "Επανεκκινήστε το Windows για να ολοκληρώσετε την εγκατάσταση του WSL και μετά ανοίξτε ξανά το OpenCode.", + "wsl.onboarding.next": "Επόμενο", + "wsl.onboarding.refresh": "Ανανέωση", + "wsl.onboarding.allDistrosAdded": "Όλες οι εγκατεστημένες διανομές έχουν ήδη προστεθεί.", + "wsl.onboarding.noDistros": "Δεν έχουν εντοπιστεί ακόμη διανομές.", + "wsl.onboarding.install": "Εγκατάσταση", + "wsl.onboarding.installing": "Εγκατάσταση...", + "wsl.onboarding.installDistro": "Εγκατάσταση διανομής", + "wsl.onboarding.searchDistros": "Αναζήτηση διανομών", + "wsl.onboarding.wsl2Required": "WSL 2 απαιτείται.", + "wsl.onboarding.toolsRequired": "Αυτή η διανομή χρειάζεται bash και curl.", + "wsl.onboarding.openTerminal": "Άνοιγμα τερματικού", + "wsl.onboarding.path": "Διαδρομή: {{path}}", + "wsl.onboarding.notFound": "δεν βρέθηκε", + "wsl.onboarding.version": "Έκδοση: {{version}}", + "wsl.onboarding.unknown": "άγνωστο", + "wsl.onboarding.desktopVersion": "επιτραπέζιος υπολογιστής {{version}}", + "wsl.onboarding.versionMismatch": "Η εγκατεστημένη έκδοση δεν ταιριάζει με την έκδοση της εφαρμογής για υπολογιστές.", + "wsl.onboarding.adding": "Προσθήκη...", + "help.tabs.toast.ariaLabel": "Εισαγωγή καρτελών. Οργανώστε την εργασία και τις ενεργές συνεδρίες σας με καρτέλες", + "help.tabs.toast.dismiss": "Παράβλεψη πληροφοριών καρτελών", + "help.tabs.title": "Εισαγωγή καρτελών", + "help.tabs.description": "Οργανώστε την εργασία και τις ενεργές συνεδρίες σας με καρτέλες", + "help.tabs.date": "14 Ιουλίου", + "help.tabs.introduction": "OpenCode Η επιφάνεια εργασίας είναι πλέον χτισμένη γύρω από καρτέλες.", + "help.tabs.sessions": + "Ξεκινήστε μια νέα συνεδρία σε μια καρτέλα ή ανοίξτε μια υπάρχουσα περίοδο λειτουργίας από οποιοδήποτε από τα έργα σας. Ανοίξτε μια νέα καρτέλα όταν ξεκινάτε κάτι νέο και κλείστε την όταν τελειώσετε.", + "help.tabs.organize": + "Διατηρώντας μερικές καρτέλες ανοιχτές διευκολύνετε την οργάνωση των ενεργών συνεδριών σας. Μετονομάστε τις καρτέλες σε κάτι αξιομνημόνευτο, αν σκοπεύετε να τις διατηρήσετε.", + "help.tabs.home": + "Θα βρείτε όλες τις συνεδρίες και τα έργα σας στη νέα Αρχική οθόνη. Η επιλογή μιας συνεδρίας ανοίγει σε μια καρτέλα.", + "help.tabs.persistence": "Όταν ανοίγετε ξανά την εφαρμογή, οι καρτέλες σας είναι ακόμα ανοιχτές.", + "help.tabs.worktrees": + "Η νέα σχεδίαση δεν υποστηρίζει ακόμα Git Worktrees, θα είναι σύντομα διαθέσιμη. Επομένως, εάν προτιμάτε να συνεχίσετε να χρησιμοποιείτε την προηγούμενη διάταξη, μπορείτε να κάνετε εναλλαγή μεταξύ των διατάξεων στις Ρυθμίσεις. Απλώς έχετε κατά νου ότι η νέα διάταξη θα γίνει μόνιμη σε λίγες εβδομάδες.", + "server.row.noUsername": "χωρίς όνομα χρήστη", + "dialog.project.edit.title": "Επεξεργασία έργου", + "dialog.project.edit.name": "Όνομα", + "dialog.project.edit.icon": "Εικονίδιο", + "dialog.project.edit.icon.alt": "Εικονίδιο έργου", + "dialog.project.edit.icon.hint": "Κάντε κλικ ή σύρετε μια εικόνα", + "dialog.project.edit.icon.recommended": "Συνιστάται: 128x128px", + "dialog.project.edit.color": "Χρώμα", + "dialog.project.edit.color.select": "Επιλογή {{color}} χρώματος", + "dialog.project.edit.worktree.startup": "Σενάριο εκκίνησης χώρου εργασίας", + "dialog.project.edit.worktree.startup.description": + "Εκτελείται μετά τη δημιουργία νέου χώρου εργασίας (δέντρου εργασίας).", + "dialog.project.edit.worktree.startup.placeholder": "π.χ. εγκατάσταση bun", + "dialog.releaseNotes.action.getStarted": "Ξεκινήστε", + "dialog.releaseNotes.action.next": "Επόμενο", + "dialog.releaseNotes.action.hideFuture": "Να μην εμφανίζονται αυτά στο μέλλον", + "dialog.releaseNotes.media.alt": "Προεπισκόπηση έκδοσης", + "context.breakdown.title": "Ανάλυση περιβάλλοντος", + "context.breakdown.note": + 'Κατά προσέγγιση ανάλυση των διακριτικών εισόδου. Το "Άλλο" περιλαμβάνει ορισμούς εργαλείων και γενικά έξοδα.', + "context.breakdown.system": "Σύστημα", + "context.breakdown.user": "Χρήστης", + "context.breakdown.assistant": "Βοηθός", + "context.breakdown.tool": "Κλήσεις εργαλείου", + "context.breakdown.other": "Άλλο", + "context.systemPrompt.title": "Προτροπή συστήματος", + "context.rawMessages.title": "Ακατέργαστα μηνύματα", + "context.export.session": "Εξαγωγή συνεδρίας", + "context.stats.session": "Συνεδρία", + "context.stats.messages": "Μηνύματα", + "context.stats.provider": "Πάροχος", + "context.stats.model": "Μοντέλο", + "context.stats.limit": "Όριο περιβάλλοντος", + "context.stats.totalTokens": "Σύνολο διακριτικών", + "context.stats.usage": "Χρήση", + "context.stats.inputTokens": "Εισαγωγή διακριτικών", + "context.stats.outputTokens": "Διακριτικά εξόδου", + "context.stats.reasoningTokens": "Διακριτικά συλλογιστικής", + "context.stats.cacheTokens": "Διακριτικά κρυφής μνήμης (ανάγνωση/εγγραφή)", + "context.stats.userMessages": "Μηνύματα χρήστη", + "context.stats.assistantMessages": "Μηνύματα Βοηθού", + "context.stats.totalCost": "Συνολικό κόστος", + "context.stats.sessionCreated": "Η συνεδρία δημιουργήθηκε", + "context.stats.lastActivity": "Τελευταία δραστηριότητα", + "context.usage.tokens": "Διακριτικά", + "context.usage.usage": "Χρήση", + "context.usage.cost": "Κόστος", + "context.usage.clickToView": "Κάντε κλικ για προβολή του περιβάλλοντος", + "context.usage.view": "Προβολή χρήσης περιβάλλοντος", + "language.en": "Αγγλικά", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Νορβηγικά", + "language.br": "Πορτογαλία (Βραζιλία)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Γλώσσα", + "toast.language.description": "Μετάβαση σε {{language}}", + "toast.theme.title": "Εναλλαγή θέματος", + "toast.scheme.title": "Χρωματικός συνδυασμός", + "toast.workspace.enabled.title": "Οι χώροι εργασίας ενεργοποιημένοι", + "toast.workspace.enabled.description": "Πολλά δέντρα εργασίας εμφανίζονται τώρα στην πλαϊνή γραμμή", + "toast.workspace.disabled.title": "Οι χώροι εργασίας είναι απενεργοποιημένοι", + "toast.workspace.disabled.description": "Μόνο το κύριο δέντρο εργασίας εμφανίζεται στην πλαϊνή γραμμή", + "toast.permissions.autoaccept.on.title": "Αυτόματη αποδοχή δικαιωμάτων", + "toast.permissions.autoaccept.on.description": "Τα αιτήματα άδειας θα εγκρίνονται αυτόματα", + "toast.permissions.autoaccept.off.title": "Διακοπή αυτόματης αποδοχής δικαιωμάτων", + "toast.permissions.autoaccept.off.description": "Τα αιτήματα άδειας απαιτούν έγκριση", + "toast.model.none.title": "Δεν έχει επιλεγεί μοντέλο", + "toast.model.none.description": "Συνδέστε έναν πάροχο για να συνοψίσετε αυτήν τη συνεδρία", + "toast.file.loadFailed.title": "Αποτυχία φόρτωσης αρχείου", + "toast.file.listFailed.title": "Απέτυχε η λίστα αρχείων", + "toast.context.noLineSelection.title": "Χωρίς επιλογή γραμμής", + "toast.context.noLineSelection.description": "Επιλέξτε ένα εύρος γραμμής σε μια καρτέλα αρχείου πρώτα.", + "toast.session.share.copyFailed.title": "Αποτυχία αντιγραφής του URL στο πρόχειρο", + "toast.session.share.success.title": "Η συνεδρία κοινοποιήθηκε", + "toast.session.share.success.description": "Κοινή χρήση του URL αντιγράφηκε στο πρόχειρο!", + "toast.session.share.failed.title": "Αποτυχία κοινής χρήσης συνεδρίας", + "toast.session.share.failed.description": "Παρουσιάστηκε σφάλμα κατά την κοινή χρήση της συνεδρίας", + "toast.session.unshare.success.title": "Καταργήθηκε η κοινή χρήση της συνεδρίας", + "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": "Διαθέσιμη ενημέρωση", + "toast.update.description": "Μια νέα έκδοση του OpenCode ({{version}}) είναι τώρα διαθέσιμη για εγκατάσταση.", + "toast.update.action.installRestart": "Εγκατάσταση και επανεκκίνηση", + "toast.update.action.notYet": "Όχι ακόμη", + "error.page.title": "Κάτι πήγε στραβά", + "error.page.description": "Παρουσιάστηκε σφάλμα κατά τη φόρτωση της εφαρμογής.", + "error.page.description.localServerStartup": "Παρουσιάστηκε σφάλμα κατά την εκκίνηση του τοπικού διακομιστή.", + "error.page.details.label": "Λεπτομέρειες σφάλματος", + "error.page.action.restart": "Επανεκκίνηση", + "error.page.action.report": "Αναφορά σφάλματος", + "error.page.action.reported": "Αναφέρθηκε σφάλμα", + "error.page.action.exportLogs": "Εξαγωγή αρχείων καταγραφής", + "error.page.action.checking": "Έλεγχος...", + "error.page.action.checkUpdates": "Έλεγχος για ενημερώσεις", + "error.page.action.updateTo": "Ενημέρωση σε {{version}}", + "error.page.circular": "[Circular]", + "error.page.report.prefix": "Αναφέρετε αυτό το σφάλμα στην ομάδα OpenCode", + "error.page.report.discord": "στο Discord", + "error.page.version": "Έκδοση: {{version}}", + "error.dev.rootNotFound": + "Το στοιχείο ρίζας δεν βρέθηκε. Ξεχάσατε να το προσθέσετε στο index.html; Ή μήπως το χαρακτηριστικό id γράφτηκε λάθος;", + "error.serverSync.connectFailed": + "Δεν ήταν δυνατή η σύνδεση με τον διακομιστή. Υπάρχει διακομιστής που εκτελείται στο `{{url}}`;", + "error.serverSDK.noServerAvailable": "Δεν υπάρχει διαθέσιμος διακομιστής", + "error.serverSDK.serverNotAvailable": "Ο διακομιστής δεν είναι διαθέσιμος", + "error.childStore.persistedCacheCreateFailed": "Αποτυχία δημιουργίας διαρκούς κρυφής μνήμης", + "error.childStore.persistedProjectMetadataCreateFailed": "Αποτυχία δημιουργίας συνεχιζόμενων μεταδεδομένων έργου", + "error.childStore.persistedProjectIconCreateFailed": "Αποτυχία δημιουργίας εικονιδίου συνεχούς έργου", + "error.childStore.storeCreateFailed": "Αποτυχία δημιουργίας καταστήματος", + "directory.error.invalidUrl": "Μη έγκυρος κατάλογος στο URL.", + "error.chain.unknown": "Άγνωστο σφάλμα", + "error.server.invalidConfiguration": "Μη έγκυρη διαμόρφωση", + "error.chain.causedBy": "Προκλήθηκε από:", + "error.chain.apiError": "API σφάλμα", + "error.chain.status": "Κατάσταση: {{status}}", + "error.chain.retryable": "Επανάληψη δοκιμής: {{retryable}}", + "error.chain.responseBody": "Σώμα απόκρισης:\n{{body}}", + "error.chain.didYouMean": "Μήπως εννοείτε: {{suggestions}}", + "error.chain.modelNotFound": "Το μοντέλο δεν βρέθηκε: {{provider}}/{{model}}", + "error.chain.checkConfig": "Ελέγξτε τη διαμόρφωση (opencode.json) ονόματα παρόχου/μοντέλων", + "error.chain.mcpFailed": + 'MCP διακομιστής "{{name}}" απέτυχε. Σημείωση, το OpenCode δεν υποστηρίζει ακόμη έλεγχο ταυτότητας MCP.', + "error.chain.providerAuthFailed": "Ο έλεγχος ταυτότητας παρόχου απέτυχε ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Απέτυχε η προετοιμασία του παρόχου "{{provider}}". Ελέγξτε τα διαπιστευτήρια και τη διαμόρφωση.', + "error.chain.configJsonInvalid": "Το αρχείο διαμόρφωσης στο {{path}} δεν είναι έγκυρο JSON(C)", + "error.chain.configJsonInvalidWithMessage": + "Το αρχείο διαμόρφωσης στο {{path}} δεν είναι έγκυρο JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Ο κατάλογος "{{dir}}" στο {{path}} δεν είναι έγκυρος. Μετονομάστε τον κατάλογο σε "{{suggestion}}" ή καταργήστε τον. Αυτό είναι ένα συνηθισμένο τυπογραφικό λάθος.', + "error.chain.configFrontmatterError": "Απέτυχε η ανάλυση του frontmatter στο {{path}}:\n{{message}}", + "error.chain.configInvalid": "Το αρχείο διαμόρφωσης στο {{path}} δεν είναι έγκυρο", + "error.chain.configInvalidWithMessage": "Το αρχείο διαμόρφωσης στο {{path}} δεν είναι έγκυρο: {{message}}", + "notification.permission.title": "Απαιτείται άδεια", + "notification.permission.description": "{{sessionTitle}} στο {{projectName}} χρειάζεται άδεια", + "notification.question.title": "Ερώτηση", + "notification.question.description": "{{sessionTitle}} στο {{projectName}} έχει μια ερώτηση", + "notification.action.goToSession": "Μετάβαση στην περίοδο λειτουργίας", + "notification.session.responseReady.title": "Έτοιμη απάντηση", + "notification.session.error.title": "Σφάλμα συνεδρίας", + "notification.session.error.fallbackDescription": "Παρουσιάστηκε σφάλμα", + "home.recentProjects": "Πρόσφατα έργα", + "home.empty.title": "Δεν υπάρχουν πρόσφατα έργα", + "home.empty.description": "Ξεκινήστε ανοίγοντας ένα τοπικό έργο", + "home.title": "Αρχική", + "home.projects": "Έργα", + "home.project.add": "Προσθήκη έργου", + "home.recentlyClosed": "Πρόσφατα έκλεισε", + "home.server.collapse": "Σύμπτυξη έργων διακομιστή", + "home.server.expand": "Ανάπτυξη έργων διακομιστή", + "home.sessions.search.placeholder": "Συνεδρίες αναζήτησης", + "home.sessions.search.placeholder.scoped": "Περιόδους αναζήτησης σε {{scope}}", + "home.sessions.search.sessions": "Συνεδρίες", + "home.sessions.search.noResults": "Δεν βρέθηκαν περίοδοι σύνδεσης για {{query}}", + "home.sessions.empty": "Τίποτα εδώ ακόμα", + "home.sessions.empty.description": "Δημιουργήστε μια περίοδο λειτουργίας για να ξεκινήσετε", + "home.sessions.group.today": "Σήμερα", + "home.sessions.group.yesterday": "Χθες", + "home.sessions.group.older": "Παλαιότερο", + "home.providerTip": + "Συνδεθείτε σε 75+ παρόχους για να χρησιμοποιήσετε άλλα μοντέλα, συμπεριλαμβανομένων των Claude, GPT, Gemini, κ.λπ.", + "session.tab.session": "Συνεδρία", + "session.tab.review": "Επισκόπηση", + "session.tab.context": "Πλαίσιο", + "session.tab.unknown": "Άγνωστη συνεδρία", + "session.panel.reviewAndFiles": "Επισκόπηση και αρχεία", + "session.error.notFound": "Δεν μπορεί να βρεθεί αυτή η συνεδρία", + "session.error.notFound.description": + "Αυτή η καρτέλα δείχνει μια περίοδο λειτουργίας που δεν υπάρχει πλέον σε αυτόν τον διακομιστή.", + "session.error.notFound.closeTab": "Κλείσιμο καρτέλας", + "session.error.serverConnection": "Δεν είναι δυνατή η σύνδεση σε αυτόν τον διακομιστή", + "session.review.filesChanged": "Τα αρχεία άλλαξαν {{count}}", + "session.review.change.one": "Αλλαγή", + "session.review.change.other": "Αλλαγές", + "session.review.loadingChanges": "Φόρτωση αλλαγών...", + "session.review.empty": "Δεν υπάρχουν ακόμη αλλαγές σε αυτήν τη συνεδρία", + "session.review.noVcs": "Δεν εντοπίστηκε σύστημα ελέγχου έκδοσης Git, οι αλλαγές δεν εμφανίζονται", + "session.review.noVcs.createGit.title": "Δημιουργία αποθετηρίου Git", + "session.review.noVcs.createGit.description": "Παρακολούθηση, έλεγχος και αναίρεση αλλαγών σε αυτό το έργο", + "session.review.noVcs.createGit.actionLoading": "Δημιουργία αποθετηρίου Git...", + "session.review.noVcs.createGit.action": "Δημιουργία αποθετηρίου Git", + "session.review.noSnapshot": + "Η παρακολούθηση στιγμιότυπου είναι απενεργοποιημένη στις ρυθμίσεις παραμέτρων, επομένως οι αλλαγές περιόδου λειτουργίας δεν είναι διαθέσιμες", + "session.review.noChanges": "Χωρίς αλλαγές", + "session.review.noUncommittedChanges": "Δεν υπάρχουν ακόμη μη δεσμευμένες αλλαγές", + "session.review.noBranchChanges": "Δεν υπάρχουν ακόμη αλλαγές κλάδου", + "session.files.selectToOpen": "Επιλέξτε ένα αρχείο για άνοιγμα", + "session.files.all": "Όλα τα αρχεία", + "session.files.empty": "Δεν υπάρχουν αρχεία", + "session.files.binaryContent": "Δυαδικό αρχείο (το περιεχόμενο δεν μπορεί να εμφανιστεί)", + "session.messages.renderEarlier": "Απόδοση προηγούμενων μηνυμάτων", + "session.messages.loadingEarlier": "Φόρτωση προηγούμενων μηνυμάτων...", + "session.messages.loadEarlier": "Φόρτωση προηγούμενων μηνυμάτων", + "session.messages.loading": "Φόρτωση μηνυμάτων...", + "session.messages.jumpToLatest": "Μετάβαση στο πιο πρόσφατο", + "session.context.addToContext": "Προσθήκη {{selection}} στο περιβάλλον", + "session.todo.title": "Εκκρεμότητες", + "session.todo.collapse": "Σύμπτυξη", + "session.todo.expand": "Ανάπτυξη", + "session.todo.progress": "{{done}} από {{total}} εργασίες ολοκληρώθηκαν", + "session.question.progress": "{{current}} από {{total}} ερωτήσεις", + "session.question.minimize": "Ελαχιστοποίηση ερώτησης", + "session.question.restore": "Ερώτηση επαναφοράς", + "session.question.pending.one": "{{count}} ερώτηση σε εκκρεμότητα", + "session.question.pending.other": "{{count}} ερωτήσεις σε εκκρεμότητα", + "session.followupDock.summary.one": "{{count}} μήνυμα στην ουρά", + "session.followupDock.summary.other": "{{count}} μηνύματα στην ουρά", + "session.followupDock.sendNow": "Αποστολή τώρα", + "session.followupDock.edit": "Επεξεργασία", + "session.followupDock.collapse": "Σύμπτυξη μηνυμάτων στην ουρά", + "session.followupDock.expand": "Ανάπτυξη μηνυμάτων στην ουρά", + "session.revertDock.summary.one": "{{count}} ανανεωμένο μήνυμα", + "session.revertDock.summary.other": "{{count}} επαναλαμβανόμενα μηνύματα", + "session.revertDock.collapse": "Σύμπτυξη επαναλαμβανόμενων μηνυμάτων", + "session.revertDock.expand": "Ανάπτυξη επαναλαμβανόμενων μηνυμάτων", + "session.revertDock.restore": "Επαναφορά μηνύματος", + "session.new.title": "Δημιουργία οτιδήποτε", + "session.new.project.new": "Νέο έργο", + "session.new.project.search": "Αναζήτηση έργων", + "session.new.project.add": "Προσθήκη έργου", + "session.new.worktree.main": "Κύριος κλάδος", + "session.new.worktree.mainWithBranch": "Κύριος κλάδος ({{branch}})", + "session.new.worktree.create": "Δημιουργία νέου δέντρου εργασίας", + "session.new.workspace.runIn": "Εκτέλεση συνεδρίας σε", + "session.new.workspace.triggerLocal": "Τοπική", + "session.new.workspace.local": "Τοπικό αποθετήριο", + "session.new.workspace.existing": "Χώρος εργασίας…", + "session.new.git.none": "Χωρίς Git", + "session.new.lastModified": "Τελευταία τροποποίηση", + "session.header.search.placeholder": "Αναζήτηση {{project}}", + "session.header.searchFiles": "Αναζήτηση αρχείων", + "session.header.openIn": "Άνοιγμα σε", + "session.header.open.action": "Άνοιγμα {{app}}", + "session.header.open.ariaLabel": "Άνοιγμα σε {{app}}", + "session.header.open.menu": "Άνοιγμα επιλογών", + "session.header.open.copyPath": "Διαδρομή αντιγραφής", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Εξερεύνηση αρχείων", + "session.header.open.fileManager": "Διαχείριση αρχείων", + "session.header.reveal.finder": "Αποκάλυψη σε Finder", + "session.header.reveal.fileExplorer": "Αποκάλυψη στην Εξερεύνηση αρχείων", + "session.header.reveal.containingFolder": "Άνοιγμα που περιέχει φάκελο", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Τερματικό", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Κατάσταση", + "status.popover.ariaLabel": "Διαμορφώσεις διακομιστή", + "status.popover.tab.servers": "Διακομιστές", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Προσθήκες", + "status.popover.action.manageServers": "Διαχείριση διακομιστών", + "session.share.popover.title": "Δημοσίευση στον Ιστό", + "session.share.popover.description.shared": + "Αυτή η συνεδρία είναι δημόσια στον Ιστό. Είναι προσβάσιμο σε οποιονδήποτε έχει τον σύνδεσμο.", + "session.share.popover.description.unshared": + "Κοινή χρήση συνεδρίας δημόσια στον ιστό. Θα είναι προσβάσιμο σε οποιονδήποτε έχει τον σύνδεσμο.", + "session.share.action.share": "Κοινή χρήση", + "session.share.action.publish": "Δημοσίευση", + "session.share.action.publishing": "Δημοσίευση...", + "session.share.action.unpublish": "Κατάργηση δημοσίευσης", + "session.share.action.unpublishing": "Κατάργηση δημοσίευσης...", + "session.share.action.view": "Προβολή", + "session.share.copy.copied": "Αντιγράφηκε", + "session.share.copy.copyLink": "Αντιγραφή συνδέσμου", + "lsp.tooltip.none": "Δεν υπάρχουν LSP διακομιστές", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Φόρτωση προτροπής...", + "terminal.loading": "Φόρτωση τερματικού...", + "terminal.title": "Τερματικό", + "terminal.title.numbered": "Τερματικό {{number}}", + "terminal.close": "Κλείσιμο τερματικού", + "terminal.connectionLost.title": "Η σύνδεση χάθηκε", + "terminal.connectionLost.abnormalClose": "WebSocket έκλεισε ασυνήθιστα: {{code}}", + "terminal.connectionLost.description": + "Η σύνδεση τερματικού διακόπηκε. Αυτό μπορεί να συμβεί κατά την επανεκκίνηση του διακομιστή.", + "terminal.connectTicket.csrfError": + "PTY εισιτήριο σύνδεσης απορρίφθηκε από την προέλευση ή CSRF επιταγές. Ελέγξτε τη διαμόρφωση του διακομιστή CORS.", + "terminal.connectTicket.statusError": "PTY η σύνδεση εισιτηρίου απέτυχε με {{status}}", + "titlebar.update": "Ενημέρωση", + "titlebar.updateVersion": "Ενημέρωση {{version}}", + "common.closeTab": "Κλείσιμο καρτέλας", + "common.dismiss": "Παράβλεψη", + "common.moreCountSuffix": " (+{{count}} περισσότερα)", + "common.requestFailed": "Το αίτημα απέτυχε", + "common.moreOptions": "Περισσότερες επιλογές", + "common.learnMore": "Μάθετε περισσότερα", + "common.rename": "Μετονομασία", + "common.export": "Εξαγωγή", + "common.reset": "Επαναφορά", + "common.archive": "Αρχείο", + "common.delete": "Διαγραφή", + "common.close": "Κλείσιμο", + "common.edit": "Επεξεργασία", + "common.loadMore": "Φόρτωση περισσότερων", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "άγνωστο", + "common.time.justNow": "Μόλις τώρα", + "common.time.minutesAgo.short": "{{count}}λ. πριν", + "common.time.hoursAgo.short": "{{count}}h πριν", + "common.time.daysAgo.short": "{{count}}ημέρα πριν", + "sidebar.menu.toggle": "Εναλλαγή μενού", + "sidebar.nav.projectsAndSessions": "Έργα και συνεδρίες", + "sidebar.settings": "Ρυθμίσεις", + "sidebar.help": "Βοήθεια", + "sidebar.workspaces.enable": "Ενεργοποίηση χώρων εργασίας", + "sidebar.workspaces.disable": "Απενεργοποίηση χώρων εργασίας", + "sidebar.gettingStarted.title": "Ξεκινώντας", + "sidebar.gettingStarted.line1": "OpenCode περιλαμβάνει δωρεάν μοντέλα, ώστε να μπορείτε να ξεκινήσετε αμέσως.", + "sidebar.gettingStarted.line2": "Συνδέστε οποιονδήποτε πάροχο για χρήση μοντέλων, inc. Claude, GPT, Gemini κ.λπ.", + "sidebar.project.recentSessions": "Πρόσφατες συνεδρίες", + "sidebar.project.viewAllSessions": "Προβολή όλων των συνεδριών", + "sidebar.project.clearNotifications": "Διαγραφή ειδοποιήσεων", + "sidebar.empty.title": "Δεν υπάρχουν ανοιχτά έργα", + "sidebar.empty.description": "Ανοίξτε ένα έργο για να ξεκινήσετε", + "debugBar.ariaLabel": "Διαγνωστικά απόδοσης ανάπτυξης", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Τελευταία ολοκληρωμένη μετάβαση διαδρομής που αγγίζει μια σελίδα συνεδρίας, μετρημένη από την έναρξη του δρομολογητή μέχρι την πρώτη βαφή μετά την καθίζηση.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Κυλιόμενα καρέ ανά δευτερόλεπτο τα τελευταία 5 δευτερόλεπτα.", + "debugBar.frame.label": "ΚΑΡΕ", + "debugBar.frame.tip": "Χειρότερος χρόνος καρέ τα τελευταία 5 δευτερόλεπτα.", + "debugBar.jank.label": "ΕΜΠΛΟΚΗ", + "debugBar.jank.tip": "Καρέ πάνω από 32 ms τα τελευταία 5 δευτερόλεπτα.", + "debugBar.long.label": "ΜΑΚΡΟ", + "debugBar.long.tip": + "Αποκλεισμένος χρόνος και πλήθος εργασιών μεγάλης διάρκειας τα τελευταία 5 δευτερόλεπτα. Μέγιστη εργασία: {{max}}.", + "debugBar.delay.label": "ΚΑΘΥΣΤΕΡΗΣΗ", + "debugBar.delay.tip": "Η χειρότερη καθυστέρηση εισόδου που παρατηρήθηκε τα τελευταία 5 δευτερόλεπτα.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Κατά προσέγγιση διάρκεια αλληλεπίδρασης τα τελευταία 5 δευτερόλεπτα. Αυτό είναι INPόπως, όχι το επίσημο Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Σωρευτική αλλαγή διάταξης για την τρέχουσα διάρκεια ζωής της εφαρμογής.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Χρησιμοποιημένο σωρό JS έναντι ορίου σωρού. Chromium μόνο.", + "debugBar.mem.tip": "Χρησιμοποιημένο σωρό JS έναντι ορίου σωρού. {{used}} από {{limit}}.", + "debugBar.focus.label": "ΕΣΤΙΑΣΗ", + "debugBar.focus.tip": "Επιβολή στυλ εστίασης σε όλα τα διαδραστικά στοιχεία", + "debugBar.focus.on": "ΕΝΕΡΓΟ", + "debugBar.focus.off": "ΑΝΕΝΕΡΓΟ", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": + "Επιβολή της κατεύθυνσης της πλήρους διάταξης εφαρμογής χωρίς αλλαγή της επιλεγμένης γλώσσας", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Επιτραπέζιος υπολογιστής", + "settings.section.desktop": "Επιτραπέζιος", + "settings.section.server": "Διακομιστής", + "settings.tab.general": "Γενικά", + "settings.tab.shortcuts": "Συντομεύσεις", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL ενσωμάτωση", + "settings.desktop.wsl.description": "Εκτέλεση του διακομιστή OpenCode μέσα στο WSL στο Windows.", + "settings.general.section.appearance": "Εμφάνιση", + "settings.general.section.advanced": "Για προχωρημένους", + "settings.general.section.notifications": "Ειδοποιήσεις συστήματος", + "settings.general.section.updates": "Ενημέρωση", + "settings.general.section.sounds": "Ηχητικά εφέ", + "settings.general.section.feed": "Ροή", + "settings.general.section.display": "Εμφάνιση", + "settings.general.row.language.title": "Γλώσσα", + "settings.general.row.language.description": "Αλλαγή της γλώσσας εμφάνισης για το OpenCode", + "settings.general.row.shell.title": "Τερματικό κέλυφος", + "settings.general.row.shell.description": "Κέλυφος που χρησιμοποιείται από το τερματικό και τα εργαλεία του πράκτορα", + "settings.general.row.shell.autoDefault": "Αυτόματο (Προεπιλογή)", + "settings.general.row.shell.terminalOnly": "μόνο τερματικό", + "settings.general.row.appearance.title": "Εμφάνιση", + "settings.general.row.appearance.description": "Προσαρμογή της εμφάνισης του OpenCode στη συσκευή σας", + "settings.general.row.colorScheme.title": "Χρωματικός συνδυασμός", + "settings.general.row.colorScheme.description": + "Επιλέξτε εάν το OpenCode ακολουθεί το σύστημα, το ανοιχτό ή το σκοτεινό θέμα", + "settings.general.row.theme.title": "Θέμα", + "settings.general.row.theme.description": "Προσαρμογή του τρόπου με τον οποίο έχει θέμα το OpenCode.", + "settings.general.row.font.title": "Γραμματοσειρά κώδικα", + "settings.general.row.font.description": "Προσαρμογή της γραμματοσειράς που χρησιμοποιείται στα μπλοκ κώδικα", + "settings.general.row.terminalFont.title": "Τερματική γραμματοσειρά", + "settings.general.row.terminalFont.description": "Προσαρμογή της γραμματοσειράς που χρησιμοποιείται στο τερματικό", + "settings.general.row.uiFont.title": "Γραμματοσειρά UI", + "settings.general.row.uiFont.description": "Προσαρμογή της γραμματοσειράς που χρησιμοποιείται σε όλη τη διεπαφή", + "settings.general.row.followup.title": "Συμπεριφορά παρακολούθησης", + "settings.general.row.followup.description": + "Επιλέξτε εάν οι επακόλουθες προτροπές κατευθύνονται αμέσως ή περιμένετε σε μια ουρά", + "settings.general.row.followup.option.queue": "Ουρά", + "settings.general.row.followup.option.steer": "Καθοδήγηση", + "settings.general.row.showFileTree.title": "Δέντρο αρχείων", + "settings.general.row.showFileTree.description": "Εμφάνιση του πίνακα δέντρου αρχείων σε περιόδους λειτουργίας", + "settings.general.row.showNavigation.title": "Στοιχεία ελέγχου πλοήγησης", + "settings.general.row.showNavigation.description": + "Εμφάνιση των κουμπιών πίσω και εμπρός στη γραμμή τίτλου της επιφάνειας εργασίας", + "settings.general.row.showSearch.title": "Παλέτα εντολών", + "settings.general.row.showSearch.description": + "Εμφάνιση του κουμπιού παλέτας αναζήτησης και εντολών στη γραμμή τίτλου", + "settings.general.row.showTerminal.title": "Τερματικό", + "settings.general.row.showTerminal.description": + "Εμφάνιση του κουμπιού τερματικού στη γραμμή τίτλου της επιφάνειας εργασίας", + "settings.general.row.showStatus.title": "Κατάσταση διακομιστή", + "settings.general.row.showStatus.description": "Εμφάνιση του κουμπιού κατάστασης διακομιστή στη γραμμή τίτλου", + "settings.general.row.mobileTitlebarBottom.title": "Κάτω πλοήγηση", + "settings.general.row.mobileTitlebarBottom.description": + "Τοποθετήστε τη γραμμή τίτλου και τις καρτέλες περιόδου λειτουργίας στο κάτω μέρος της οθόνης στο κινητό", + "settings.general.row.showCustomAgents.title": "Εμφάνιση πράκτορα", + "settings.general.row.showCustomAgents.description": + "Εναλλαγή μεταξύ πρακτόρων στον συνθέτη. Όταν είναι κρυφό, ορίζεται από προεπιλογή Build agent.", + "settings.general.row.reasoningSummaries.title": "Εμφάνιση περιλήψεων συλλογισμών", + "settings.general.row.reasoningSummaries.description": "Εμφάνιση περιλήψεων συλλογισμών μοντέλων στη γραμμή χρόνου", + "settings.general.row.shellToolPartsExpanded.title": "Ανάπτυξη εξαρτημάτων εργαλείου κελύφους", + "settings.general.row.shellToolPartsExpanded.description": + "Εμφάνιση εξαρτημάτων εργαλείου κελύφους που έχουν αναπτυχθεί από προεπιλογή στη γραμμή χρόνου", + "settings.general.row.editToolPartsExpanded.title": "Ανάπτυξη τμημάτων εργαλείου επεξεργασίας", + "settings.general.row.editToolPartsExpanded.description": + "Εμφάνιση τμημάτων του εργαλείου επεξεργασίας, εγγραφής και ενημέρωσης κώδικα που έχουν αναπτυχθεί από προεπιλογή στη γραμμή χρόνου", + "settings.general.row.newInterface.title": "Νέα διάταξη", + "settings.general.row.newInterface.badge": "Νέο", + "settings.general.row.newInterface.description": + "Χρησιμοποιήστε τις νέες καρτέλες και τη διάταξη του σπιτιού. Εναλλαγή μεταξύ των διατάξεων για περιορισμένο χρονικό διάστημα.", + "settings.general.row.newInterfaceNotice.title": "Χρησιμοποιείτε τώρα νέα διάταξη", + "settings.general.row.newInterfaceNotice.description": "Η προηγούμενη διάταξη δεν είναι πλέον διαθέσιμη", + "settings.general.row.newInterfaceNotice.dismiss": "Παράβλεψη", + "settings.general.row.pinchZoom.title": "Τσιμπήστε για μεγέθυνση", + "settings.general.row.pinchZoom.description": + "Να επιτρέπεται η μεγέθυνση των χειρονομιών τσιμπήματος και κύλισης Ctrl", + "settings.general.row.wayland.title": "Χρήση εγγενούς Wayland", + "settings.general.row.wayland.description": "Απενεργοποίηση εναλλακτικού X11 στο Wayland. Απαιτείται επανεκκίνηση.", + "settings.general.row.wayland.tooltip": + "Σε Linux με μικτές οθόνες ρυθμού ανανέωσης, το εγγενές Wayland μπορεί να είναι πιο σταθερό.", + "settings.general.row.releaseNotes.title": "Σημειώσεις έκδοσης", + "settings.general.row.releaseNotes.description": "Εμφάνιση αναδυόμενων παραθύρων για τα νέα μετά από ενημερώσεις", + "settings.updates.row.startup.title": "Έλεγχος για ενημερώσεις κατά την εκκίνηση", + "settings.updates.row.startup.description": "Αυτόματος έλεγχος για ενημερώσεις κατά την εκκίνηση του OpenCode", + "settings.updates.row.check.title": "Έλεγχος για ενημερώσεις", + "settings.updates.row.check.description": "Ελέγξτε μη αυτόματα για ενημερώσεις και εγκαταστήστε εάν είναι διαθέσιμες", + "settings.updates.action.checkNow": "Έλεγχος τώρα", + "settings.updates.action.checking": "Έλεγχος...", + "settings.updates.action.downloading": "Λήψη...", + "settings.updates.action.installing": "Εγκατάσταση...", + "settings.updates.toast.latest.title": "Είστε ενημερωμένοι", + "settings.updates.toast.latest.description": "Εκτελείτε την πιο πρόσφατη έκδοση του OpenCode.", + "sound.option.none": "Κανένα", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "Πράκτορας", + "settings.general.notifications.agent.description": + "Εμφάνιση ειδοποίησης συστήματος όταν ο πράκτορας είναι πλήρης ή χρειάζεται προσοχή", + "settings.general.notifications.permissions.title": "Δικαιώματα", + "settings.general.notifications.permissions.description": "Εμφάνιση ειδοποίησης συστήματος όταν απαιτείται άδεια", + "settings.general.notifications.errors.title": "Σφάλματα", + "settings.general.notifications.errors.description": "Εμφάνιση ειδοποίησης συστήματος όταν παρουσιαστεί σφάλμα", + "settings.general.sounds.agent.title": "Πράκτορας", + "settings.general.sounds.agent.description": "Αναπαραγωγή ήχου όταν ο πράκτορας είναι πλήρης ή χρειάζεται προσοχή", + "settings.general.sounds.permissions.title": "Δικαιώματα", + "settings.general.sounds.permissions.description": "Αναπαραγωγή ήχου όταν απαιτείται άδεια", + "settings.general.sounds.errors.title": "Σφάλματα", + "settings.general.sounds.errors.description": "Αναπαραγωγή ήχου όταν παρουσιαστεί σφάλμα", + "settings.shortcuts.title": "Συντομεύσεις πληκτρολογίου", + "settings.shortcuts.reset.button": "Επαναφορά στις προεπιλογές", + "settings.shortcuts.reset.toast.title": "Επαναφορά συντομεύσεων", + "settings.shortcuts.reset.toast.description": "Οι συντομεύσεις πληκτρολογίου έχουν επαναφερθεί στις προεπιλογές.", + "settings.shortcuts.conflict.title": "Η συντόμευση χρησιμοποιείται ήδη", + "settings.shortcuts.conflict.description": "{{keybind}} έχει ήδη εκχωρηθεί στο {{titles}}.", + "settings.shortcuts.unassigned": "Μη εκχωρημένο", + "settings.shortcuts.pressKeys": "Πατήστε τα πλήκτρα", + "settings.shortcuts.search.placeholder": "Συντομεύσεις αναζήτησης", + "settings.shortcuts.search.empty": "Δεν βρέθηκαν συντομεύσεις", + "settings.shortcuts.group.general": "Γενικά", + "settings.shortcuts.group.session": "Συνεδρία", + "settings.shortcuts.group.navigation": "Πλοήγηση", + "settings.shortcuts.group.modelAndAgent": "Μοντέλο και πράκτορας", + "settings.shortcuts.group.terminal": "Τερματικό", + "settings.shortcuts.group.prompt": "Προτροπή", + "settings.providers.title": "Παρόχοι", + "settings.providers.description": "Οι ρυθμίσεις παρόχου θα μπορούν να διαμορφωθούν εδώ.", + "settings.providers.section.connected": "Συνδεδεμένοι πάροχοι", + "settings.providers.connected.empty": "Δεν υπάρχουν συνδεδεμένοι πάροχοι", + "settings.providers.connected.environmentDescription": "Συνδέθηκε από τις μεταβλητές περιβάλλοντος", + "settings.providers.section.popular": "Δημοφιλείς πάροχοι", + "settings.providers.custom.description": "Προσθήκη παρόχου συμβατού με OpenAI κατά βάση URL.", + "settings.providers.tag.environment": "Περιβάλλον", + "settings.providers.tag.config": "Ρύθμιση", + "settings.providers.tag.custom": "Προσαρμοσμένο", + "settings.providers.tag.other": "Άλλο", + "settings.models.title": "Μοντέλα", + "settings.models.description": "Οι ρυθμίσεις μοντέλου μπορούν να διαμορφωθούν εδώ.", + "settings.agents.title": "Πράκτορες", + "settings.agents.description": "Οι ρυθμίσεις πρακτόρων μπορούν να διαμορφωθούν εδώ.", + "settings.commands.title": "Εντολές", + "settings.commands.description": "Οι ρυθμίσεις εντολών θα μπορούν να διαμορφωθούν εδώ.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP οι ρυθμίσεις θα μπορούν να διαμορφωθούν εδώ.", + "settings.permissions.title": "Δικαιώματα", + "settings.permissions.description": "Ελέγξτε ποια εργαλεία μπορεί να χρησιμοποιήσει ο διακομιστής από προεπιλογή.", + "settings.permissions.section.tools": "Εργαλεία", + "settings.permissions.toast.updateFailed.title": "Αποτυχία ενημέρωσης δικαιωμάτων", + "settings.permissions.action.allow": "Να επιτρέπεται", + "settings.permissions.action.ask": "Ρωτήστε", + "settings.permissions.action.deny": "Απόρριψη", + "settings.permissions.tool.read.title": "Διαβάστε", + "settings.permissions.tool.read.description": "Ανάγνωση αρχείου (ταιριάζει με τη διαδρομή αρχείου)", + "settings.permissions.tool.edit.title": "Επεξεργασία", + "settings.permissions.tool.edit.description": + "Τροποποίηση αρχείων, συμπεριλαμβανομένων επεξεργασιών, εγγραφών και ενημερώσεων κώδικα", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Αντιστοιχία αρχείων με χρήση μοτίβων σφαιρών", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Αναζήτηση περιεχομένων αρχείου με χρήση κανονικών εκφράσεων", + "settings.permissions.tool.list.title": "Λίστα", + "settings.permissions.tool.list.description": "Λίστα αρχείων σε έναν κατάλογο", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Εκτέλεση εντολών κελύφους", + "settings.permissions.tool.task.title": "Εργασία", + "settings.permissions.tool.task.description": "Εκκίνηση υπο-πρακτόρων", + "settings.permissions.tool.skill.title": "Ικανότητα", + "settings.permissions.tool.skill.description": "Φόρτωση δεξιότητας με όνομα", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Ερωτήματα διακομιστή γλώσσας εκτέλεσης", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Ενημέρωση της λίστας εργασιών", + "settings.permissions.tool.webfetch.title": "Λήψη Ιστού", + "settings.permissions.tool.webfetch.description": "Λήψη περιεχομένου από ένα URL", + "settings.permissions.tool.websearch.title": "Αναζήτηση Ιστού", + "settings.permissions.tool.websearch.description": "Αναζήτηση στον Ιστό", + "settings.permissions.tool.external_directory.title": "Εξωτερικός κατάλογος", + "settings.permissions.tool.external_directory.description": "Πρόσβαση σε αρχεία εκτός του καταλόγου του έργου", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Εντοπισμός επαναλαμβανόμενων κλήσεων εργαλείου με ίδια είσοδο", + "session.delete.failed.title": "Αποτυχία διαγραφής συνεδρίας", + "session.delete.title": "Διαγραφή συνεδρίας", + "session.delete.confirm": 'Διαγραφή συνεδρίας "{{name}}";', + "session.delete.button": "Διαγραφή συνεδρίας", + "workspace.new": "Νέος χώρος εργασίας", + "workspace.type.local": "τοπικό", + "workspace.type.sandbox": "απομονωμένο περιβάλλον", + "workspace.create.failed.title": "Αποτυχία δημιουργίας χώρου εργασίας", + "workspace.delete.failed.title": "Αποτυχία διαγραφής χώρου εργασίας", + "workspace.resetting.title": "Επαναφορά χώρου εργασίας", + "workspace.resetting.description": "Αυτό μπορεί να διαρκέσει ένα λεπτό.", + "workspace.reset.failed.title": "Αποτυχία επαναφοράς του χώρου εργασίας", + "workspace.reset.success.title": "Επαναφορά χώρου εργασίας", + "workspace.reset.success.description": "Ο χώρος εργασίας αντιστοιχεί πλέον στον προεπιλεγμένο κλάδο.", + "workspace.error.stillPreparing": "Ο χώρος εργασίας προετοιμάζεται ακόμη", + "workspace.status.checking": "Έλεγχος για μη συγχωνευμένες αλλαγές...", + "workspace.status.error": "Δεν είναι δυνατή η επαλήθευση της κατάστασης git.", + "workspace.status.clean": "Δεν εντοπίστηκαν μη συγχωνευμένες αλλαγές.", + "workspace.status.dirty": "Εντοπίστηκαν μη συγχωνευμένες αλλαγές σε αυτόν τον χώρο εργασίας.", + "workspace.delete.title": "Διαγραφή χώρου εργασίας", + "workspace.delete.confirm": 'Διαγραφή χώρου εργασίας "{{name}}";', + "workspace.delete.button": "Διαγραφή χώρου εργασίας", + "workspace.reset.title": "Επαναφορά χώρου εργασίας", + "workspace.reset.confirm": 'Επαναφορά χώρου εργασίας "{{name}}";', + "workspace.reset.button": "Επαναφορά χώρου εργασίας", + "workspace.reset.archived.none": "Δεν θα αρχειοθετηθούν ενεργές συνεδρίες.", + "workspace.reset.archived.one": "Θα αρχειοθετηθεί 1 συνεδρία.", + "workspace.reset.archived.many": "{{count}} θα αρχειοθετηθούν οι περίοδοι σύνδεσης.", + "workspace.reset.note": "Αυτό θα επαναφέρει τον χώρο εργασίας ώστε να ταιριάζει με τον προεπιλεγμένο κλάδο.", + "dialog.usageExceeded.dontShowAgain": "Να μην εμφανιστεί ξανά", +} diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts new file mode 100644 index 0000000000000000000000000000000000000000..62d4dc1ae6e905aa68a88c2d88c74ae1a47a28a4 --- /dev/null +++ b/packages/app/src/i18n/en.ts @@ -0,0 +1,1150 @@ +import { DESKTOP_NATIVE_ENGLISH } from "./desktop-native" + +export const dict = { + ...DESKTOP_NATIVE_ENGLISH, + "command.category.suggested": "Suggested", + "command.category.view": "View", + "command.category.project": "Project", + "command.category.provider": "Provider", + "command.category.server": "Server", + "command.category.session": "Session", + "command.category.theme": "Theme", + "command.category.language": "Language", + "command.category.file": "File", + "command.category.context": "Context", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Permissions", + "command.category.workspace": "Workspace", + "command.category.settings": "Settings", + "command.logs.export": "Export logs", + + "theme.scheme.system": "System", + "theme.scheme.light": "Light", + "theme.scheme.dark": "Dark", + + "command.sidebar.toggle": "Toggle sidebar", + "command.project.open": "Open project", + "command.project.previous": "Previous project", + "command.project.next": "Next project", + "command.project.index": "Switch to project {{index}}", + "command.provider.connect": "Connect provider", + "command.server.switch": "Switch server", + "command.settings.open": "Open settings", + "command.session.previous": "Previous session", + "command.session.next": "Next session", + "command.session.previous.unseen": "Previous unread session", + "command.session.next.unseen": "Next unread session", + "command.session.archive": "Archive session", + + "command.palette": "Command palette", + + "command.theme.cycle": "Cycle theme", + "command.theme.set": "Use theme: {{theme}}", + "command.theme.scheme.cycle": "Cycle color scheme", + "command.theme.scheme.set": "Use color scheme: {{scheme}}", + + "command.language.cycle": "Cycle language", + "command.language.set": "Use language: {{language}}", + + "command.session.new": "New session", + "command.file.open": "Open file", + "command.tab.close": "Close tab", + "command.tab.reopenClosed": "Reopen closed tab", + "command.context.addSelection": "Add selection to context", + "command.context.addSelection.description": "Add selected lines from the current file", + "command.input.focus": "Focus input", + "command.terminal.toggle": "Toggle terminal", + "command.fileTree.toggle": "Toggle file tree", + "command.review.toggle": "Toggle review", + "command.terminal.new": "New terminal", + "command.terminal.new.description": "Create a new terminal tab", + "command.steps.toggle": "Toggle steps", + "command.steps.toggle.description": "Show or hide steps for the current message", + "command.message.previous": "Previous message", + "command.message.previous.description": "Go to the previous user message", + "command.message.next": "Next message", + "command.message.next.description": "Go to the next user message", + "command.model.choose": "Choose model", + "command.model.choose.description": "Select a different model", + "command.mcp.toggle": "Toggle MCPs", + "command.mcp.toggle.description": "Toggle MCPs", + "command.agent.cycle": "Cycle agent", + "command.agent.cycle.description": "Switch to the next agent", + "command.agent.cycle.reverse": "Cycle agent backwards", + "command.agent.cycle.reverse.description": "Switch to the previous agent", + "command.model.variant.cycle": "Cycle thinking effort", + "command.model.variant.cycle.description": "Switch to the next effort level", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Auto-accept permissions", + "command.permissions.autoaccept.disable": "Stop auto-accepting permissions", + "command.workspace.toggle": "Toggle workspaces", + "command.workspace.toggle.description": "Enable or disable multiple workspaces in the sidebar", + "command.session.undo": "Undo", + "command.session.undo.description": "Undo the last message", + "command.session.redo": "Redo", + "command.session.redo.description": "Redo the last undone message", + "command.session.compact": "Compact session", + "command.session.compact.description": "Summarize the session to reduce context size", + "command.session.fork": "Fork from message", + "command.session.fork.description": "Create a new session from a previous message", + "command.session.share": "Share session", + "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", + "palette.empty": "No results found", + "palette.group.commands": "Commands", + "palette.group.files": "Files", + + "dialog.provider.search.placeholder": "Search providers", + "dialog.provider.empty": "No providers found", + "dialog.provider.group.popular": "Popular", + "dialog.provider.group.other": "Other", + "dialog.provider.custom.label": "Custom OpenAI-compatible provider", + "dialog.provider.tag.recommended": "Recommended", + "dialog.provider.opencode.note": "Curated models including Claude, GPT, Gemini and more", + "dialog.provider.opencode.tagline": "Reliable optimized models", + "dialog.provider.opencodeGo.tagline": "Low cost subscription for everyone", + "dialog.provider.anthropic.note": "Direct access to Claude models, including Pro and Max", + "dialog.provider.copilot.note": "AI models for coding assistance via GitHub Copilot", + "dialog.provider.openai.note": "GPT models for fast, capable general AI tasks", + "dialog.provider.google.note": "Gemini models for fast, structured responses", + "dialog.provider.openrouter.note": "Access all supported models from one provider", + "dialog.provider.vercel.note": "Unified access to AI models with smart routing", + + "dialog.model.select.title": "Select model", + "dialog.model.search.placeholder": "Search models", + "dialog.model.empty": "No model results", + "dialog.model.manage": "Manage models", + "dialog.model.manage.description": "Customize which models appear in the model selector.", + "dialog.model.manage.provider.toggle": "Toggle all {{provider}} models", + + "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode", + "dialog.model.unpaid.addMore.title": "Add more models from popular providers", + "dialog.model.unpaid.viewMoreProviders": "See 70+ more providers", + + "dialog.provider.viewAll": "Show more providers", + + "provider.connect.title": "Connect {{provider}}", + "provider.connect.title.anthropicProMax": "Login with Claude Pro/Max", + "provider.connect.selectMethod": "Select login method for {{provider}}.", + "provider.connect.method.apiKey": "API key", + "provider.connect.method.browser": "Browser", + "provider.connect.method.headless": "Headless", + "provider.connect.status.inProgress": "Authorization in progress...", + "provider.connect.status.waiting": "Waiting for authorization...", + "provider.connect.status.failed": "Authorization failed: {{error}}", + "provider.connect.apiKey.description": + "Enter your {{provider}} API key to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API key", + "provider.connect.apiKey.placeholder": "API key", + "provider.connect.apiKey.required": "API key is required", + "provider.connect.opencodeZen.line1": + "OpenCode Zen gives you access to a curated set of reliable optimized models for coding agents.", + "provider.connect.opencodeZen.line2": + "With a single API key you'll get access to models such as Claude, GPT, Gemini, GLM and more.", + "provider.connect.opencodeZen.visit.prefix": "Visit ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " to collect your API key.", + "provider.connect.oauth.code.visit.prefix": "Visit ", + "provider.connect.oauth.code.visit.link": "this link", + "provider.connect.oauth.code.visit.suffix": + " to collect your authorization code to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.oauth.code.label": "{{method}} authorization code", + "provider.connect.oauth.code.placeholder": "Authorization code", + "provider.connect.oauth.code.required": "Authorization code is required", + "provider.connect.oauth.code.invalid": "Invalid authorization code", + "provider.connect.oauth.auto.visit.prefix": "Visit ", + "provider.connect.oauth.auto.visit.link": "this link", + "provider.connect.oauth.auto.visit.suffix": + " and enter the code below to connect your account and use {{provider}} models in OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Confirmation code", + "provider.connect.toast.connected.title": "{{provider}} connected", + "provider.connect.toast.connected.description": "{{provider}} models are now available to use.", + + "provider.custom.title": "Custom provider", + "provider.custom.unavailable": "Custom providers are unavailable on this server", + "provider.custom.description.prefix": "Configure an OpenAI-compatible provider. See the ", + "provider.custom.description.link": "provider config docs", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Provider ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Lowercase letters, numbers, hyphens, or underscores", + "provider.custom.field.name.label": "Display name", + "provider.custom.field.name.placeholder": "My AI Provider", + "provider.custom.field.baseURL.label": "Base URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API key", + "provider.custom.field.apiKey.placeholder": "API key", + "provider.custom.field.apiKey.description": "Optional. Leave empty if you manage auth via headers.", + "provider.custom.models.label": "Models", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Name", + "provider.custom.models.name.placeholder": "Display Name", + "provider.custom.models.remove": "Remove model", + "provider.custom.models.add": "Add model", + "provider.custom.headers.label": "Headers (optional)", + "provider.custom.headers.key.label": "Header", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Value", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "Remove header", + "provider.custom.headers.add": "Add header", + "provider.custom.error.providerID.required": "Provider ID is required", + "provider.custom.error.providerID.format": "Use lowercase letters, numbers, hyphens, or underscores", + "provider.custom.error.providerID.exists": "That provider ID already exists", + "provider.custom.error.name.required": "Display name is required", + "provider.custom.error.baseURL.required": "Base URL is required", + "provider.custom.error.baseURL.format": "Must start with http:// or https://", + "provider.custom.error.required": "Required", + "provider.custom.error.duplicate": "Duplicate", + + "provider.disconnect.toast.disconnected.title": "{{provider}} disconnected", + "provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.", + + "model.tag.free": "Free", + "model.tag.latest": "Latest", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "text", + "model.input.image": "image", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Allows: {{inputs}}", + "model.tooltip.context.label": "Context", + "model.tooltip.inputs": "Inputs", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Provider", + "model.tooltip.reasoning": "Reasoning", + "model.tooltip.reasoning.allowed": "Allows reasoning", + "model.tooltip.reasoning.none": "No reasoning", + "model.tooltip.context": "Context limit {{limit}}", + + "common.search.placeholder": "Search", + "common.goBack": "Navigate back", + "common.goForward": "Navigate forward", + "common.loading": "Loading", + "common.loading.ellipsis": "...", + "common.cancel": "Cancel", + "common.clear": "Clear", + "common.open": "Open", + "common.connect": "Connect", + "common.disconnect": "Disconnect", + "common.continue": "Continue", + "common.submit": "Submit", + "common.save": "Save", + "common.saving": "Saving...", + "common.default": "Default", + "common.attachment": "attachment", + + "prompt.placeholder.shell": "Enter shell command... {{example}}", + "prompt.placeholder.normal": 'Ask anything... "{{example}}"', + "prompt.placeholder.simple": "Ask anything...", + "prompt.placeholder.summarizeComments": "Summarize comments…", + "prompt.placeholder.summarizeComment": "Summarize comment…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "esc to exit", + "session.child.promptDisabled": "Subagent sessions cannot be prompted.", + "session.child.backToParent": "Back to main session.", + + "prompt.example.1": "Fix a TODO in the codebase", + "prompt.example.2": "What is the tech stack of this project?", + "prompt.example.3": "Fix broken tests", + "prompt.example.4": "Explain how authentication works", + "prompt.example.5": "Find and fix security vulnerabilities", + "prompt.example.6": "Add unit tests for the user service", + "prompt.example.7": "Refactor this function to be more readable", + "prompt.example.8": "What does this error mean?", + "prompt.example.9": "Help me debug this issue", + "prompt.example.10": "Generate API documentation", + "prompt.example.11": "Optimize database queries", + "prompt.example.12": "Add input validation", + "prompt.example.13": "Create a new component for...", + "prompt.example.14": "How do I deploy this project?", + "prompt.example.15": "Review my code for best practices", + "prompt.example.16": "Add error handling to this function", + "prompt.example.17": "Explain this regex pattern", + "prompt.example.18": "Convert this to TypeScript", + "prompt.example.19": "Add logging throughout the codebase", + "prompt.example.20": "What dependencies are outdated?", + "prompt.example.21": "Help me write a migration script", + "prompt.example.22": "Implement caching for this endpoint", + "prompt.example.23": "Add pagination to this list", + "prompt.example.24": "Create a CLI command for...", + "prompt.example.25": "How do environment variables work here?", + + "prompt.popover.emptyResults": "No matching results", + "prompt.popover.emptyCommands": "No matching commands", + "prompt.dropzone.label": "Drop images, PDFs, or text files here", + "prompt.dropzone.file.label": "Drop to @mention file", + "prompt.slash.badge.custom": "custom", + "prompt.slash.badge.skill": "skill", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "active", + "prompt.context.includeActiveFile": "Include active file", + "prompt.context.removeActiveFile": "Remove active file from context", + "prompt.context.removeFile": "Remove file from context", + "prompt.action.attachFile": "Add files", + "prompt.menu.addImagesAndFiles": "Add files and more", + "prompt.menu.imagesAndFiles": "Images and files", + "prompt.menu.commands": "Commands", + "prompt.menu.context": "Context", + "prompt.menu.shellCommand": "Shell command", + "prompt.attachment.remove": "Remove attachment", + "prompt.action.send": "Send", + "prompt.action.stop": "Stop", + + "prompt.toast.pasteUnsupported.title": "Unsupported attachment", + "prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.", + "prompt.toast.attachmentDuplicate.title": "This file has already been uploaded", + "prompt.toast.modelAgentRequired.title": "Select an agent and model", + "prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.", + "prompt.toast.worktreeCreateFailed.title": "Failed to create worktree", + "prompt.toast.sessionCreateFailed.title": "Failed to create session", + "prompt.toast.shellSendFailed.title": "Failed to send shell command", + "prompt.toast.commandSendFailed.title": "Failed to send command", + "prompt.toast.promptSendFailed.title": "Failed to send prompt", + "prompt.toast.promptSendFailed.description": "Unable to retrieve session", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} of {{total}} enabled", + "dialog.mcp.empty": "No MCPs configured", + + "dialog.lsp.empty": "LSPs auto-detected from file types", + "dialog.plugins.empty": "Plugins configured in opencode.json", + + "mcp.status.connected": "connected", + "mcp.status.failed": "failed", + "mcp.status.needs_auth": "needs auth", + "mcp.status.disabled": "disabled", + "mcp.auth.clickToAuthenticate": "Click to authenticate", + + "dialog.fork.empty": "No messages to fork from", + + "dialog.directory.search.placeholder": "Search folders", + "dialog.directory.empty": "No folders found", + "dialog.directory.action.selectFile": "Select file", + "dialog.directory.action.selectFolder": "Select folder", + "dialog.directory.root": "Root", + "dialog.directory.parent": "Parent", + "dialog.directory.readError": "Unable to read this folder", + + "app.server.unreachable": "Could not reach {{server}}", + "app.server.retrying": "Retrying automatically...", + "app.server.otherServers": "Other servers", + + "dialog.server.title": "Servers", + "dialog.server.description": "Switch which OpenCode server this app connects to.", + "dialog.server.search.placeholder": "Search servers", + "dialog.server.empty": "No servers yet", + "dialog.server.add.title": "Add server", + "dialog.server.add.url": "Server address", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Could not connect to server", + "dialog.server.add.checking": "Checking...", + "dialog.server.add.button": "Add server", + "dialog.server.add.name": "Server name (optional)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Username (optional)", + "dialog.server.add.usernamePlaceholder": "username", + "dialog.server.add.password": "Password (optional)", + "dialog.server.add.passwordPlaceholder": "password", + "dialog.server.edit.title": "Edit server", + "dialog.server.default.title": "Default server", + "dialog.server.default.description": + "Connect to this server on app launch instead of starting a local server. Requires restart.", + "dialog.server.default.none": "No server selected", + "dialog.server.default.set": "Set current server as default", + "dialog.server.default.clear": "Clear", + "dialog.server.action.remove": "Remove server", + + "dialog.server.menu.edit": "Edit", + "dialog.server.menu.default": "Set as default", + "dialog.server.menu.defaultRemove": "Remove default", + "dialog.server.menu.delete": "Delete", + "dialog.server.current": "Current Server", + "dialog.server.status.default": "Default", + "wsl.server.add": "Add WSL server", + "wsl.server.addShort": "Add WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL server", + "wsl.server.retryStart": "Retry start", + "wsl.server.updating": "Updating...", + "wsl.onboarding.step.distro": "Choose distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Checking WSL...", + "wsl.onboarding.restartRequired": "Windows needs a restart to finish installing WSL.", + "wsl.onboarding.ready": "WSL is ready.", + "wsl.onboarding.required": "WSL is required to continue.", + "wsl.onboarding.checkingDistros": "Checking distros...", + "wsl.onboarding.installingDistro": "Installing {{distro}}...", + "wsl.onboarding.checkingDistro": "Checking {{distro}}...", + "wsl.onboarding.listingDistros": "Listing distros...", + "wsl.onboarding.distroReady": "{{distro}} is ready.", + "wsl.onboarding.distroNotInstalled": "{{distro}} is not installed yet.", + "wsl.onboarding.openDistroOnce": "Open {{distro}} once to finish setup.", + "wsl.onboarding.finishingDistro": "Finishing setup for {{distro}}.", + "wsl.onboarding.pickDistro": "Pick a distro or install one below.", + "wsl.onboarding.checkingOpencode": "Checking OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Checking OpenCode in {{distro}}...", + "wsl.onboarding.updatingOpencode": "Updating OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Updating OpenCode in {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Update OpenCode in {{distro}}.", + "wsl.onboarding.updateOpencode": "Update OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode is ready in {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode is ready.", + "wsl.onboarding.installOpencodeIn": "Install OpenCode in {{distro}}.", + "wsl.onboarding.installOpencode": "Install OpenCode", + "wsl.onboarding.chooseDistroFirst": "Choose a distro first.", + "wsl.onboarding.loadFailed": "Failed to load WSL state.", + "wsl.onboarding.loading": "Loading...", + "wsl.onboarding.installedDistros": "Installed distros", + "wsl.onboarding.checkAgain": "Check again", + "wsl.onboarding.distroStatus.ready": "Ready", + "wsl.onboarding.distroStatus.checking": "Checking...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode not installed", + "wsl.onboarding.distroStatus.missingTools": "Missing bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Unsupported · Use WSL 2", + "wsl.onboarding.needAnotherDistro": "Need another distro?", + "wsl.onboarding.needAnotherDistroHint": "Install a Linux distribution from the WSL catalog", + "wsl.onboarding.wslNotInstalled.title": "WSL not installed", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) is required before OpenCode can add a WSL server", + "wsl.onboarding.wslUnavailable.title": "WSL unavailable", + "wsl.onboarding.wslUnavailable.description": "OpenCode could not verify WSL on this machine.", + "wsl.onboarding.installWsl": "Install WSL", + "wsl.onboarding.windowsRestartRequired": "Restart Windows to finish installing WSL, then reopen OpenCode.", + "wsl.onboarding.next": "Next", + "wsl.onboarding.refresh": "Refresh", + "wsl.onboarding.allDistrosAdded": "All installed distros are already added.", + "wsl.onboarding.noDistros": "No distros detected yet.", + "wsl.onboarding.install": "Install", + "wsl.onboarding.installing": "Installing...", + "wsl.onboarding.installDistro": "Install distro", + "wsl.onboarding.searchDistros": "Search distros", + "wsl.onboarding.wsl2Required": "WSL 2 is required.", + "wsl.onboarding.toolsRequired": "This distro needs bash and curl.", + "wsl.onboarding.openTerminal": "Open terminal", + "wsl.onboarding.path": "Path: {{path}}", + "wsl.onboarding.notFound": "not found", + "wsl.onboarding.version": "Version: {{version}}", + "wsl.onboarding.unknown": "unknown", + "wsl.onboarding.desktopVersion": "desktop {{version}}", + "wsl.onboarding.versionMismatch": "Installed version does not match the desktop app version.", + "wsl.onboarding.adding": "Adding...", + + "help.tabs.toast.ariaLabel": "Introducing Tabs. Organize your work and active sessions with tabs", + "help.tabs.toast.dismiss": "Dismiss Tabs information", + "help.tabs.title": "Introducing Tabs", + "help.tabs.description": "Organize your work and active sessions with tabs", + "help.tabs.date": "July 14", + "help.tabs.introduction": "OpenCode Desktop is now built around tabs.", + "help.tabs.sessions": + "Start a new session in a tab, or open an existing session from any of your projects. Open a new tab when you're starting something new, and close it when you're done.", + "help.tabs.organize": + "Keeping a few tabs open makes it easier to organize your active sessions. Rename tabs to something memorable if you plan to keep them around.", + "help.tabs.home": + "You'll find all your sessions and projects on the new Home screen. Selecting a session opens it in a tab.", + "help.tabs.persistence": "When you reopen the app, your tabs are still open.", + "help.tabs.worktrees": + "The new design does not support Git Worktrees yet, it's coming soon. So if you'd prefer to continue using the previous layout, you can switch between layouts in Settings. Just keep in mind that the new layout will become permanent in a few weeks.", + "server.row.noUsername": "no username", + + "dialog.project.edit.title": "Edit project", + "dialog.project.edit.name": "Name", + "dialog.project.edit.icon": "Icon", + "dialog.project.edit.icon.alt": "Project icon", + "dialog.project.edit.icon.hint": "Click or drag an image", + "dialog.project.edit.icon.recommended": "Recommended: 128x128px", + "dialog.project.edit.color": "Color", + "dialog.project.edit.color.select": "Select {{color}} color", + "dialog.project.edit.worktree.startup": "Workspace startup script", + "dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).", + "dialog.project.edit.worktree.startup.placeholder": "e.g. bun install", + + "dialog.releaseNotes.action.getStarted": "Get started", + "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.', + "context.breakdown.system": "System", + "context.breakdown.user": "User", + "context.breakdown.assistant": "Assistant", + "context.breakdown.tool": "Tool Calls", + "context.breakdown.other": "Other", + + "context.systemPrompt.title": "System Prompt", + "context.rawMessages.title": "Raw messages", + "context.export.session": "Export session", + + "context.stats.session": "Session", + "context.stats.messages": "Messages", + "context.stats.provider": "Provider", + "context.stats.model": "Model", + "context.stats.limit": "Context Limit", + "context.stats.totalTokens": "Total Tokens", + "context.stats.usage": "Usage", + "context.stats.inputTokens": "Input Tokens", + "context.stats.outputTokens": "Output Tokens", + "context.stats.reasoningTokens": "Reasoning Tokens", + "context.stats.cacheTokens": "Cache Tokens (read/write)", + "context.stats.userMessages": "User Messages", + "context.stats.assistantMessages": "Assistant Messages", + "context.stats.totalCost": "Total Cost", + "context.stats.sessionCreated": "Session Created", + "context.stats.lastActivity": "Last Activity", + + "context.usage.tokens": "Tokens", + "context.usage.usage": "Usage", + "context.usage.cost": "Cost", + "context.usage.clickToView": "Click to view context", + "context.usage.view": "View context usage", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "Language", + "toast.language.description": "Switched to {{language}}", + + "toast.theme.title": "Theme switched", + "toast.scheme.title": "Color scheme", + + "toast.workspace.enabled.title": "Workspaces enabled", + "toast.workspace.enabled.description": "Multiple worktrees are now shown in the sidebar", + "toast.workspace.disabled.title": "Workspaces disabled", + "toast.workspace.disabled.description": "Only the main worktree is shown in the sidebar", + + "toast.permissions.autoaccept.on.title": "Auto-accepting permissions", + "toast.permissions.autoaccept.on.description": "Permission requests will be automatically approved", + "toast.permissions.autoaccept.off.title": "Stopped auto-accepting permissions", + "toast.permissions.autoaccept.off.description": "Permission requests will require approval", + + "toast.model.none.title": "No model selected", + "toast.model.none.description": "Connect a provider to summarize this session", + + "toast.file.loadFailed.title": "Failed to load file", + "toast.file.listFailed.title": "Failed to list files", + + "toast.context.noLineSelection.title": "No line selection", + "toast.context.noLineSelection.description": "Select a line range in a file tab first.", + + "toast.session.share.copyFailed.title": "Failed to copy URL to clipboard", + "toast.session.share.success.title": "Session shared", + "toast.session.share.success.description": "Share URL copied to clipboard!", + "toast.session.share.failed.title": "Failed to share session", + "toast.session.share.failed.description": "An error occurred while sharing the session", + + "toast.session.unshare.success.title": "Session unshared", + "toast.session.unshare.success.description": "Session unshared successfully!", + "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}}", + + "toast.update.title": "Update available", + "toast.update.description": "A new version of OpenCode ({{version}}) is now available to install.", + "toast.update.action.installRestart": "Install and restart", + "toast.update.action.notYet": "Not yet", + + "error.page.title": "Something went wrong", + "error.page.description": "An error occurred while loading the application.", + "error.page.description.localServerStartup": "An error occurred while starting the local server.", + "error.page.details.label": "Error Details", + "error.page.action.restart": "Restart", + "error.page.action.report": "Report Error", + "error.page.action.reported": "Error Reported", + "error.page.action.exportLogs": "Export Logs", + "error.page.action.checking": "Checking...", + "error.page.action.checkUpdates": "Check for updates", + "error.page.action.updateTo": "Update to {{version}}", + "error.page.circular": "[Circular]", + "error.page.report.prefix": "Please report this error to the OpenCode team", + "error.page.report.discord": "on Discord", + "error.page.version": "Version: {{version}}", + + "error.dev.rootNotFound": + "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", + + "error.serverSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?", + "error.serverSDK.noServerAvailable": "No server available", + "error.serverSDK.serverNotAvailable": "Server not available", + "error.childStore.persistedCacheCreateFailed": "Failed to create persisted cache", + "error.childStore.persistedProjectMetadataCreateFailed": "Failed to create persisted project metadata", + "error.childStore.persistedProjectIconCreateFailed": "Failed to create persisted project icon", + "error.childStore.storeCreateFailed": "Failed to create store", + "directory.error.invalidUrl": "Invalid directory in URL.", + + "error.chain.unknown": "Unknown error", + "error.server.invalidConfiguration": "Invalid configuration", + "error.chain.causedBy": "Caused by:", + "error.chain.apiError": "API error", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Retryable: {{retryable}}", + "error.chain.responseBody": "Response body:\n{{body}}", + "error.chain.didYouMean": "Did you mean: {{suggestions}}", + "error.chain.modelNotFound": "Model not found: {{provider}}/{{model}}", + "error.chain.checkConfig": "Check your config (opencode.json) provider/model names", + "error.chain.mcpFailed": 'MCP server "{{name}}" failed. Note, OpenCode does not support MCP authentication yet.', + "error.chain.providerAuthFailed": "Provider authentication failed ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Failed to initialize provider "{{provider}}". Check credentials and configuration.', + "error.chain.configJsonInvalid": "Config file at {{path}} is not valid JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Config file at {{path}} is not valid JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Directory "{{dir}}" in {{path}} is not valid. Rename the directory to "{{suggestion}}" or remove it. This is a common typo.', + "error.chain.configFrontmatterError": "Failed to parse frontmatter in {{path}}:\n{{message}}", + "error.chain.configInvalid": "Config file at {{path}} is invalid", + "error.chain.configInvalidWithMessage": "Config file at {{path}} is invalid: {{message}}", + + "notification.permission.title": "Permission required", + "notification.permission.description": "{{sessionTitle}} in {{projectName}} needs permission", + "notification.question.title": "Question", + "notification.question.description": "{{sessionTitle}} in {{projectName}} has a question", + "notification.action.goToSession": "Go to session", + + "notification.session.responseReady.title": "Response ready", + "notification.session.error.title": "Session error", + "notification.session.error.fallbackDescription": "An error occurred", + + "home.recentProjects": "Recent projects", + "home.empty.title": "No recent projects", + "home.empty.description": "Get started by opening a local project", + "home.title": "Home", + "home.projects": "Projects", + "home.project.add": "Add project", + "home.recentlyClosed": "Recently closed", + "home.server.collapse": "Collapse server projects", + "home.server.expand": "Expand server projects", + "home.sessions.search.placeholder": "Search sessions", + "home.sessions.search.placeholder.scoped": "Search sessions in {{scope}}", + "home.sessions.search.sessions": "Sessions", + "home.sessions.search.noResults": "No sessions found for {{query}}", + "home.sessions.empty": "Nothing here yet", + "home.sessions.empty.description": "Create a session to get started", + "home.sessions.group.today": "Today", + "home.sessions.group.yesterday": "Yesterday", + "home.sessions.group.older": "Older", + "home.providerTip": "Connect to 75+ providers to use other models, including Claude, GPT, Gemini, etc", + + "session.tab.session": "Session", + "session.tab.review": "Review", + "session.tab.context": "Context", + "session.tab.unknown": "Unknown Session", + "session.panel.reviewAndFiles": "Review and files", + "session.error.notFound": "This session cannot be found", + "session.error.notFound.description": "This tab points to a session that no longer exists on this server.", + "session.error.notFound.closeTab": "Close Tab", + "session.error.serverConnection": "Can't connect to this server", + "session.review.filesChanged": "Files Changed {{count}}", + "session.review.change.one": "Change", + "session.review.change.other": "Changes", + "session.review.loadingChanges": "Loading changes...", + "session.review.empty": "No changes in this session yet", + "session.review.noVcs": "No Git Version Control System detected, changes not displayed", + "session.review.noVcs.createGit.title": "Create a Git repository", + "session.review.noVcs.createGit.description": "Track, review, and undo changes in this project", + "session.review.noVcs.createGit.actionLoading": "Creating Git repository...", + "session.review.noVcs.createGit.action": "Create Git repository", + "session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable", + "session.review.noChanges": "No changes", + "session.review.noUncommittedChanges": "No uncommitted changes yet", + "session.review.noBranchChanges": "No branch changes yet", + + "session.files.selectToOpen": "Select a file to open", + "session.files.all": "All files", + "session.files.empty": "No files", + "session.files.binaryContent": "Binary file (content cannot be displayed)", + + "session.messages.renderEarlier": "Render earlier messages", + "session.messages.loadingEarlier": "Loading earlier messages...", + "session.messages.loadEarlier": "Load earlier messages", + "session.messages.loading": "Loading messages...", + "session.messages.jumpToLatest": "Jump to latest", + + "session.context.addToContext": "Add {{selection}} to context", + "session.todo.title": "Todos", + "session.todo.collapse": "Collapse", + "session.todo.expand": "Expand", + "session.todo.progress": "{{done}} of {{total}} todos completed", + "session.question.progress": "{{current}} of {{total}} questions", + "session.question.minimize": "Minimize question", + "session.question.restore": "Restore question", + "session.question.pending.one": "{{count}} pending question", + "session.question.pending.other": "{{count}} pending questions", + "session.followupDock.summary.one": "{{count}} queued message", + "session.followupDock.summary.other": "{{count}} queued messages", + "session.followupDock.sendNow": "Send now", + "session.followupDock.edit": "Edit", + "session.followupDock.collapse": "Collapse queued messages", + "session.followupDock.expand": "Expand queued messages", + "session.revertDock.summary.one": "{{count}} rolled back message", + "session.revertDock.summary.other": "{{count}} rolled back messages", + "session.revertDock.collapse": "Collapse rolled back messages", + "session.revertDock.expand": "Expand rolled back messages", + "session.revertDock.restore": "Restore message", + + "session.new.title": "Build anything", + "session.new.project.new": "New project", + "session.new.project.search": "Search projects", + "session.new.project.add": "Add project", + "session.new.worktree.main": "Main branch", + "session.new.worktree.mainWithBranch": "Main branch ({{branch}})", + "session.new.worktree.create": "Create new worktree", + "session.new.workspace.runIn": "Run session in", + "session.new.workspace.triggerLocal": "Local", + "session.new.workspace.local": "Local repository", + "session.new.workspace.existing": "Workspace…", + "session.new.git.none": "No Git", + "session.new.lastModified": "Last modified", + + "session.header.search.placeholder": "Search {{project}}", + "session.header.searchFiles": "Search files", + "session.header.openIn": "Open in", + "session.header.open.action": "Open {{app}}", + "session.header.open.ariaLabel": "Open in {{app}}", + "session.header.open.menu": "Open options", + "session.header.open.copyPath": "Copy path", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "Reveal in Finder", + "session.header.reveal.fileExplorer": "Reveal in File Explorer", + "session.header.reveal.containingFolder": "Open containing folder", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Server configurations", + "status.popover.tab.servers": "Servers", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Manage servers", + + "session.share.popover.title": "Publish on web", + "session.share.popover.description.shared": + "This session is public on the web. It is accessible to anyone with the link.", + "session.share.popover.description.unshared": + "Share session publicly on the web. It will be accessible to anyone with the link.", + "session.share.action.share": "Share", + "session.share.action.publish": "Publish", + "session.share.action.publishing": "Publishing...", + "session.share.action.unpublish": "Unpublish", + "session.share.action.unpublishing": "Unpublishing...", + "session.share.action.view": "View", + "session.share.copy.copied": "Copied", + "session.share.copy.copyLink": "Copy link", + + "lsp.tooltip.none": "No LSP servers", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Loading prompt...", + "terminal.loading": "Loading terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Close terminal", + "terminal.connectionLost.title": "Connection Lost", + "terminal.connectionLost.abnormalClose": "WebSocket closed abnormally: {{code}}", + "terminal.connectionLost.description": + "The terminal connection was interrupted. This can happen when the server restarts.", + "terminal.connectTicket.csrfError": + "PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.", + "terminal.connectTicket.statusError": "PTY connect ticket failed with {{status}}", + + "titlebar.update": "Update", + "titlebar.updateVersion": "Update {{version}}", + + "common.closeTab": "Close tab", + "common.dismiss": "Dismiss", + "common.moreCountSuffix": " (+{{count}} more)", + "common.requestFailed": "Request failed", + "common.moreOptions": "More options", + "common.learnMore": "Learn more", + "common.rename": "Rename", + "common.export": "Export", + "common.reset": "Reset", + "common.archive": "Archive", + "common.delete": "Delete", + "common.close": "Close", + "common.edit": "Edit", + "common.loadMore": "Load more", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "unknown", + + "common.time.justNow": "Just now", + "common.time.minutesAgo.short": "{{count}}m ago", + "common.time.hoursAgo.short": "{{count}}h ago", + "common.time.daysAgo.short": "{{count}}d ago", + + "sidebar.menu.toggle": "Toggle menu", + "sidebar.nav.projectsAndSessions": "Projects and sessions", + "sidebar.settings": "Settings", + "sidebar.help": "Help", + "sidebar.workspaces.enable": "Enable workspaces", + "sidebar.workspaces.disable": "Disable workspaces", + "sidebar.gettingStarted.title": "Getting started", + "sidebar.gettingStarted.line1": "OpenCode includes free models so you can start immediately.", + "sidebar.gettingStarted.line2": "Connect any provider to use models, inc. Claude, GPT, Gemini etc.", + "sidebar.project.recentSessions": "Recent sessions", + "sidebar.project.viewAllSessions": "View all sessions", + "sidebar.project.clearNotifications": "Clear notifications", + "sidebar.empty.title": "No projects open", + "sidebar.empty.description": "Open a project to get started", + + "debugBar.ariaLabel": "Development performance diagnostics", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Last completed route transition touching a session page, measured from router start until the first paint after it settles.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Rolling frames per second over the last 5 seconds.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Worst frame time over the last 5 seconds.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Frames over 32ms in the last 5 seconds.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blocked time and long-task count in the last 5 seconds. Max task: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Worst observed input delay in the last 5 seconds.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Approximate interaction duration over the last 5 seconds. This is INP-like, not the official Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Cumulative layout shift for the current app lifetime.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Used JS heap vs heap limit. Chromium only.", + "debugBar.mem.tip": "Used JS heap vs heap limit. {{used}} of {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Force focus styles on all interactive elements", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Force the full app layout direction without changing the selected language", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + + "app.name.desktop": "OpenCode Desktop", + + "settings.section.desktop": "Desktop", + "settings.section.server": "Server", + "settings.tab.general": "General", + "settings.tab.shortcuts": "Shortcuts", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integration", + "settings.desktop.wsl.description": "Run the OpenCode server inside WSL on Windows.", + + "settings.general.section.appearance": "Appearance", + "settings.general.section.advanced": "Advanced", + "settings.general.section.notifications": "System notifications", + "settings.general.section.updates": "Updates", + "settings.general.section.sounds": "Sound effects", + "settings.general.section.feed": "Feed", + "settings.general.section.display": "Display", + + "settings.general.row.language.title": "Language", + "settings.general.row.language.description": "Change the display language for OpenCode", + "settings.general.row.shell.title": "Terminal shell", + "settings.general.row.shell.description": "Shell used by the terminal and agent tools", + "settings.general.row.shell.autoDefault": "Auto (Default)", + "settings.general.row.shell.terminalOnly": "terminal only", + "settings.general.row.appearance.title": "Appearance", + "settings.general.row.appearance.description": "Customise how OpenCode looks on your device", + "settings.general.row.colorScheme.title": "Color scheme", + "settings.general.row.colorScheme.description": "Choose whether OpenCode follows the system, light, or dark theme", + "settings.general.row.theme.title": "Theme", + "settings.general.row.theme.description": "Customise how OpenCode is themed.", + "settings.general.row.font.title": "Code Font", + "settings.general.row.font.description": "Customise the font used in code blocks", + "settings.general.row.terminalFont.title": "Terminal Font", + "settings.general.row.terminalFont.description": "Customise the font used in the terminal", + "settings.general.row.uiFont.title": "UI Font", + "settings.general.row.uiFont.description": "Customise the font used throughout the interface", + "settings.general.row.followup.title": "Follow-up behavior", + "settings.general.row.followup.description": "Choose whether follow-up prompts steer immediately or wait in a queue", + "settings.general.row.followup.option.queue": "Queue", + "settings.general.row.followup.option.steer": "Steer", + "settings.general.row.showFileTree.title": "File tree", + "settings.general.row.showFileTree.description": "Show the file tree panel in sessions", + "settings.general.row.showNavigation.title": "Navigation controls", + "settings.general.row.showNavigation.description": "Show the back and forward buttons in the desktop title bar", + "settings.general.row.showSearch.title": "Command palette", + "settings.general.row.showSearch.description": "Show the search and command palette button in the title bar", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar", + "settings.general.row.showStatus.title": "Server status", + "settings.general.row.showStatus.description": "Show the server status button in the title bar", + "settings.general.row.mobileTitlebarBottom.title": "Bottom navigation", + "settings.general.row.mobileTitlebarBottom.description": + "Place the title bar and session tabs at the bottom of the screen on mobile", + "settings.general.row.showCustomAgents.title": "Show agent", + "settings.general.row.showCustomAgents.description": + "Switch between agents in the composer. When hidden, defaults to Build agent.", + "settings.general.row.reasoningSummaries.title": "Show reasoning summaries", + "settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline", + "settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts", + "settings.general.row.shellToolPartsExpanded.description": + "Show shell tool parts expanded by default in the timeline", + "settings.general.row.editToolPartsExpanded.title": "Expand edit tool parts", + "settings.general.row.editToolPartsExpanded.description": + "Show edit, write, and patch tool parts expanded by default in the timeline", + "settings.general.row.newInterface.title": "New layout", + "settings.general.row.newInterface.badge": "New", + "settings.general.row.newInterface.description": + "Use the new tabs and home layout. Switch between layouts for a limited time.", + "settings.general.row.newInterfaceNotice.title": "You're now using new layout", + "settings.general.row.newInterfaceNotice.description": "The previous layout is no longer available", + "settings.general.row.newInterfaceNotice.dismiss": "Dismiss", + "settings.general.row.pinchZoom.title": "Pinch to zoom", + "settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom", + + "settings.general.row.wayland.title": "Use native Wayland", + "settings.general.row.wayland.description": "Disable X11 fallback on Wayland. Requires restart.", + "settings.general.row.wayland.tooltip": + "On Linux with mixed refresh-rate monitors, native Wayland can be more stable.", + + "settings.general.row.releaseNotes.title": "Release notes", + "settings.general.row.releaseNotes.description": "Show What's New popups after updates", + + "settings.updates.row.startup.title": "Check for updates on startup", + "settings.updates.row.startup.description": "Automatically check for updates when OpenCode launches", + "settings.updates.row.check.title": "Check for updates", + "settings.updates.row.check.description": "Manually check for updates and install if available", + "settings.updates.action.checkNow": "Check now", + "settings.updates.action.checking": "Checking...", + "settings.updates.action.downloading": "Downloading...", + "settings.updates.action.installing": "Installing...", + "settings.updates.toast.latest.title": "You're up to date", + "settings.updates.toast.latest.description": "You're running the latest version of OpenCode.", + "sound.option.none": "None", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Show system notification when the agent is complete or needs attention", + "settings.general.notifications.permissions.title": "Permissions", + "settings.general.notifications.permissions.description": "Show system notification when a permission is required", + "settings.general.notifications.errors.title": "Errors", + "settings.general.notifications.errors.description": "Show system notification when an error occurs", + + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Play sound when the agent is complete or needs attention", + "settings.general.sounds.permissions.title": "Permissions", + "settings.general.sounds.permissions.description": "Play sound when a permission is required", + "settings.general.sounds.errors.title": "Errors", + "settings.general.sounds.errors.description": "Play sound when an error occurs", + + "settings.shortcuts.title": "Keyboard shortcuts", + "settings.shortcuts.reset.button": "Reset to defaults", + "settings.shortcuts.reset.toast.title": "Shortcuts reset", + "settings.shortcuts.reset.toast.description": "Keyboard shortcuts have been reset to defaults.", + "settings.shortcuts.conflict.title": "Shortcut already in use", + "settings.shortcuts.conflict.description": "{{keybind}} is already assigned to {{titles}}.", + "settings.shortcuts.unassigned": "Unassigned", + "settings.shortcuts.pressKeys": "Press keys", + "settings.shortcuts.search.placeholder": "Search shortcuts", + "settings.shortcuts.search.empty": "No shortcuts found", + + "settings.shortcuts.group.general": "General", + "settings.shortcuts.group.session": "Session", + "settings.shortcuts.group.navigation": "Navigation", + "settings.shortcuts.group.modelAndAgent": "Model and agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + + "settings.providers.title": "Providers", + "settings.providers.description": "Provider settings will be configurable here.", + "settings.providers.section.connected": "Connected providers", + "settings.providers.connected.empty": "No connected providers", + "settings.providers.connected.environmentDescription": "Connected from your environment variables", + "settings.providers.section.popular": "Popular providers", + "settings.providers.custom.description": "Add an OpenAI-compatible provider by base URL.", + "settings.providers.tag.environment": "Environment", + "settings.providers.tag.config": "Config", + "settings.providers.tag.custom": "Custom", + "settings.providers.tag.other": "Other", + "settings.models.title": "Models", + "settings.models.description": "Model settings will be configurable here.", + "settings.agents.title": "Agents", + "settings.agents.description": "Agent settings will be configurable here.", + "settings.commands.title": "Commands", + "settings.commands.description": "Command settings will be configurable here.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP settings will be configurable here.", + + "settings.permissions.title": "Permissions", + "settings.permissions.description": "Control what tools the server can use by default.", + "settings.permissions.section.tools": "Tools", + "settings.permissions.toast.updateFailed.title": "Failed to update permissions", + + "settings.permissions.action.allow": "Allow", + "settings.permissions.action.ask": "Ask", + "settings.permissions.action.deny": "Deny", + + "settings.permissions.tool.read.title": "Read", + "settings.permissions.tool.read.description": "Reading a file (matches the file path)", + "settings.permissions.tool.edit.title": "Edit", + "settings.permissions.tool.edit.description": "Modify files, including edits, writes, and patches", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Match files using glob patterns", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Search file contents using regular expressions", + "settings.permissions.tool.list.title": "List", + "settings.permissions.tool.list.description": "List files within a directory", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Run shell commands", + "settings.permissions.tool.task.title": "Task", + "settings.permissions.tool.task.description": "Launch sub-agents", + "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.description": "Load a skill by name", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Run language server queries", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Update the todo list", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Fetch content from a URL", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "Search the web", + "settings.permissions.tool.external_directory.title": "External Directory", + "settings.permissions.tool.external_directory.description": "Access files outside the project directory", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input", + + "session.delete.failed.title": "Failed to delete session", + "session.delete.title": "Delete session", + "session.delete.confirm": 'Delete session "{{name}}"?', + "session.delete.button": "Delete session", + + "workspace.new": "New workspace", + "workspace.type.local": "local", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "Failed to create workspace", + "workspace.delete.failed.title": "Failed to delete workspace", + "workspace.resetting.title": "Resetting workspace", + "workspace.resetting.description": "This may take a minute.", + "workspace.reset.failed.title": "Failed to reset workspace", + "workspace.reset.success.title": "Workspace reset", + "workspace.reset.success.description": "Workspace now matches the default branch.", + "workspace.error.stillPreparing": "Workspace is still preparing", + "workspace.status.checking": "Checking for unmerged changes...", + "workspace.status.error": "Unable to verify git status.", + "workspace.status.clean": "No unmerged changes detected.", + "workspace.status.dirty": "Unmerged changes detected in this workspace.", + "workspace.delete.title": "Delete workspace", + "workspace.delete.confirm": 'Delete workspace "{{name}}"?', + "workspace.delete.button": "Delete workspace", + "workspace.reset.title": "Reset workspace", + "workspace.reset.confirm": 'Reset workspace "{{name}}"?', + "workspace.reset.button": "Reset workspace", + "workspace.reset.archived.none": "No active sessions will be archived.", + "workspace.reset.archived.one": "1 session will be archived.", + "workspace.reset.archived.many": "{{count}} sessions will be archived.", + "workspace.reset.note": "This will reset the workspace to match the default branch.", +} diff --git a/packages/app/src/i18n/et.ts b/packages/app/src/i18n/et.ts new file mode 100644 index 0000000000000000000000000000000000000000..d78296675e3d0efe654b25444cd89ece0b118dbe --- /dev/null +++ b/packages/app/src/i18n/et.ts @@ -0,0 +1,1151 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Fail", + "desktop.menu.edit": "Muuda", + "desktop.menu.view": "Vaade", + "desktop.menu.go": "Mine", + "desktop.menu.window": "Aken", + "desktop.menu.help": "Abi", + "desktop.menu.checkForUpdates": "Otsige värskendusi...", + "desktop.menu.settings": "Seaded", + "desktop.menu.reloadWebview": "Laadige Webview uuesti", + "desktop.menu.restart": "Taaskäivitage", + "desktop.menu.exportLogs": "Ekspordi logid...", + "desktop.menu.newSession": "Uus sessioon", + "desktop.menu.openProject": "Ava projekt...", + "desktop.menu.newWindow": "Uus aken", + "desktop.menu.closeWindow": "Sule aken", + "desktop.menu.undo": "Võta tagasi", + "desktop.menu.redo": "Tee uuesti", + "desktop.menu.cut": "Lõika", + "desktop.menu.copy": "Kopeeri", + "desktop.menu.paste": "Kleebi", + "desktop.menu.delete": "Kustuta", + "desktop.menu.selectAll": "Valige Kõik", + "desktop.menu.toggleSidebar": "Külgriba sisse- ja väljalülitamine", + "desktop.menu.toggleTerminal": "Lülitage terminal", + "desktop.menu.toggleFileTree": "Failipuu sisse- ja väljalülitamine", + "desktop.menu.reload": "Laadi uuesti", + "desktop.menu.toggleDeveloperTools": "Lülitage välja arendaja tööriistad", + "desktop.menu.actualSize": "Tegelik suurus", + "desktop.menu.zoomIn": "Suumi sisse", + "desktop.menu.zoomOut": "Suumi välja", + "desktop.menu.toggleFullScreen": "Lülitage täisekraan sisse", + "desktop.menu.back": "Tagasi", + "desktop.menu.forward": "Edasi", + "desktop.menu.previousSession": "Eelmine seanss", + "desktop.menu.nextSession": "Järgmine seanss", + "desktop.menu.previousProject": "Eelmine projekt", + "desktop.menu.nextProject": "Järgmine projekt", + "desktop.menu.minimize": "Minimeeri", + "desktop.menu.maximize": "Maksimeerida", + "desktop.menu.documentation": "OpenCode Dokumentatsioon", + "desktop.menu.supportForum": "Tugifoorum", + "desktop.menu.shareFeedback": "Jaga tagasisidet", + "desktop.menu.reportBug": "Teatage veast", + "desktop.menu.ariaLabel": "OpenCode menüü", + "desktop.updater.dialog.checkFailed.message": "Värskenduskontroll ebaõnnestus.", + "desktop.updater.dialog.checkFailed.title": "Värskendusviga", + "desktop.updater.dialog.upToDate.message": "Oled kursis.", + "desktop.updater.dialog.upToDate.title": "Värskendusi pole", + "desktop.updater.dialog.ready.message": "Värskendus {{version}} on alla laaditud. Kas alustada kohe uuesti?", + "desktop.updater.dialog.ready.title": "Värskendus valmis", + "desktop.updater.dialog.restart": "Taaskäivitage", + "desktop.updater.dialog.later": "Hiljem", + "desktop.recovery.action.relaunch": "Taaskäivitage", + "desktop.recovery.action.exportLogs": "Ekspordi logid", + "desktop.recovery.action.keepWaiting": "Jätkake ootamist", + "desktop.recovery.action.quit": "Lõpeta", + "desktop.recovery.loadFailed": "OpenCode laadimine ebaõnnestus", + "desktop.recovery.terminated": "OpenCode aken suleti ootamatult", + "desktop.recovery.unresponsive": "OpenCode ei vasta", + "desktop.recovery.unresponsive.detail": "Saate rakenduse uuesti käivitada, logid avada või oodata.", + "desktop.recovery.loadFailed.detail": "Aken: {{window}}\nURL: {{url}}\nViga: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Aken: {{window}}\nPõhjus: {{reason}}\nKood: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Valige kaust", + "desktop.dialog.chooseFile": "Valige fail", + "desktop.dialog.saveFile": "Salvesta fail", + "desktop.dialog.files": "Failid", + "desktop.server.local": "Kohalik server", + "desktop.wsl.error.windowsOnly": "WSL on saadaval ainult Windows", + "desktop.wsl.error.unavailable": "WSL pole saadaval", + "desktop.wsl.error.listInstalled": "Installitud WSL distributsioonide loendisse lisamine nurjus", + "desktop.wsl.error.listOnline": "Internetis WSL distributsioonide loendisse lisamine ebaõnnestus", + "desktop.wsl.error.executeDistro": "Distros ei saa käske täita", + "desktop.wsl.error.installWsl": "WSL installimine ebaõnnestus", + "desktop.wsl.error.installDistro": "Distro installimine ebaõnnestus: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode installimine ebaõnnestus", + "desktop.wsl.error.alreadyAdded": "{{distro}} on juba lisatud", + "desktop.wsl.error.opencodeMissing": "opencode pole sellesse distrosse installitud", + "desktop.wsl.error.opencodeCannotRun": "opencode on installitud, kuid seda ei saa käivitada", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode pole installitud asukohta {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode värskendus on lõpetatud, kuid {{distro}} teatab endiselt {{installed}}; oodata {{expected}}", + "desktop.wsl.error.noVersion": "versiooni pole", + "desktop.wsl.error.serverExited": "WSL-server sulgus pärast käivitamist (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL server väljus enne tervenemist (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} tervisekontrolli külgkorv aegus {{timeout}} ms pärast", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} aegus {{timeout}} ms pärast", + "desktop.wsl.error.failedPort": "Pordi hankimine ebaõnnestus", + "desktop.picker.error.notSelected": "Valija ei valinud faili", + "desktop.picker.error.sizeLimit": "Valitud manused ületavad {{limit}} MB piirangut", + "command.category.suggested": "Soovitatud", + "command.category.view": "Vaade", + "command.category.project": "Projekt", + "command.category.provider": "Pakkuja", + "command.category.server": "Server", + "command.category.session": "Seanss", + "command.category.theme": "Teema", + "command.category.language": "Keel", + "command.category.file": "Fail", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Mudel", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "load", + "command.category.workspace": "Tööruum", + "command.category.settings": "Seaded", + "command.logs.export": "Ekspordi logid", + "theme.scheme.system": "Süsteem", + "theme.scheme.light": "Valgus", + "theme.scheme.dark": "Tume", + "command.sidebar.toggle": "Lülita külgriba sisse", + "command.project.open": "Avatud projekt", + "command.project.previous": "Eelmine projekt", + "command.project.next": "Järgmine projekt", + "command.project.index": "Lülitu projektile {{index}}", + "command.provider.connect": "Ühendage pakkuja", + "command.server.switch": "Vaheta serverit", + "command.settings.open": "Ava seaded", + "command.session.previous": "Eelmine seanss", + "command.session.next": "Järgmine seanss", + "command.session.previous.unseen": "Eelmine lugemata seanss", + "command.session.next.unseen": "Järgmine lugemata seanss", + "command.session.archive": "Arhiivi seanss", + "command.palette": "Käsupalett", + "command.theme.cycle": "Tsükli teema", + "command.theme.set": "Kasuta teemat: {{theme}}", + "command.theme.scheme.cycle": "Tsükli värviskeem", + "command.theme.scheme.set": "Kasuta värviskeemi: {{scheme}}", + "command.language.cycle": "Tsükli keel", + "command.language.set": "Kasuta keelt: {{language}}", + "command.session.new": "Uus seanss", + "command.file.open": "Ava fail", + "command.tab.close": "Sule vahekaart", + "command.tab.reopenClosed": "Ava suletud vaheleht uuesti", + "command.context.addSelection": "Lisage valik konteksti", + "command.context.addSelection.description": "Lisa valitud read aktiivsest failist", + "command.input.focus": "Fookuse sisend", + "command.terminal.toggle": "Lülita terminali sisse", + "command.fileTree.toggle": "Failipuu sisse- ja väljalülitamine", + "command.review.toggle": "Lülita ülevaatus sisse", + "command.terminal.new": "Uus terminal", + "command.terminal.new.description": "Looge uus terminali vahekaart", + "command.steps.toggle": "Lülitage samme sisse", + "command.steps.toggle.description": "Näita või peida praeguse sõnumi samme", + "command.message.previous": "Eelmine sõnum", + "command.message.previous.description": "Minge eelmise kasutaja sõnumi juurde", + "command.message.next": "Järgmine sõnum", + "command.message.next.description": "Minge järgmise kasutajateate juurde", + "command.model.choose": "Valige mudel", + "command.model.choose.description": "Valige mõni muu mudel", + "command.mcp.toggle": "MCP-de sisse- ja väljalülitamine", + "command.mcp.toggle.description": "MCP-de sisse- ja väljalülitamine", + "command.agent.cycle": "Tsükli agent", + "command.agent.cycle.description": "Lülituge järgmisele agendile", + "command.agent.cycle.reverse": "Tsükli agent tagurpidi", + "command.agent.cycle.reverse.description": "Lülituge eelmisele agendile", + "command.model.variant.cycle": "Tsükli mõtlemise pingutus", + "command.model.variant.cycle.description": "Lülituge järgmisele pingutustasemele", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Viip", + "command.permissions.autoaccept.enable": "Lubade automaatne aktsepteerimine", + "command.permissions.autoaccept.disable": "Peatage lubade automaatne vastuvõtmine", + "command.workspace.toggle": "Tööruumide sisse- ja väljalülitamine", + "command.workspace.toggle.description": "Lubage või keelake külgribal mitu tööruumi", + "command.session.undo": "Võta tagasi", + "command.session.undo.description": "Võta viimane sõnum tagasi", + "command.session.redo": "Tee uuesti", + "command.session.redo.description": "Korrake viimane tühistatud sõnum", + "command.session.compact": "Kompaktne seanss", + "command.session.compact.description": "Konteksti suuruse vähendamiseks tehke seansist kokkuvõte", + "command.session.fork": "Sõnumi kahvel", + "command.session.fork.description": "Looge eelmisest sõnumist uus seanss", + "command.session.share": "Jaga seanssi", + "command.session.share.description": "Jagage seda seanssi ja kopeerige URL lõikelauale", + "command.session.unshare": "Tühista seansi jagamine", + "command.session.unshare.description": "Lõpetage selle seansi jagamine", + "command.session.export": "Ekspordiseanss", + "command.session.export.description": "Ekspordi kogu seansi transkriptsioon kui JSON", + "palette.search.placeholder": "Otsige faile, käske ja seansse", + "palette.search.placeholder.home": "Otsige käske ja seansse", + "palette.empty": "Tulemusi ei leitud", + "palette.group.commands": "Käsud", + "palette.group.files": "Failid", + "dialog.provider.search.placeholder": "Otsi pakkujaid", + "dialog.provider.empty": "Pakkujaid ei leitud", + "dialog.provider.group.popular": "Populaarne", + "dialog.provider.group.other": "muud", + "dialog.provider.custom.label": "Kohandatud OpenAI-ga ühilduv pakkuja", + "dialog.provider.tag.recommended": "Soovitatav", + "dialog.provider.opencode.note": "Kureeritud mudelid, sealhulgas Claude, GPT, Gemini ja palju muud", + "dialog.provider.opencode.tagline": "Usaldusväärsed optimeeritud mudelid", + "dialog.provider.opencodeGo.tagline": "Madala hinnaga tellimus kõigile", + "dialog.provider.anthropic.note": "Otsene juurdepääs Claude mudelile, sealhulgas Pro ja Max", + "dialog.provider.copilot.note": "AI mudelid kodeerimisabiks GitHub Copilot kaudu", + "dialog.provider.openai.note": "GPT mudelid kiirete ja võimekate üldiste AI-ülesannete jaoks", + "dialog.provider.google.note": "Gemini mudelid kiirete ja struktureeritud vastuste jaoks", + "dialog.provider.openrouter.note": "Juurdepääs kõigile ühe pakkuja toetatud mudelitele", + "dialog.provider.vercel.note": "Ühtne juurdepääs AI mudelitele nutika marsruutimisega", + "dialog.model.select.title": "Valige mudel", + "dialog.model.search.placeholder": "Otsige mudeleid", + "dialog.model.empty": "Mudelitulemusi pole", + "dialog.model.manage": "Hallake mudeleid", + "dialog.model.manage.description": "Kohandage, millised mudelid mudelivalijas kuvatakse.", + "dialog.model.manage.provider.toggle": "Lülitab kõik {{provider}} mudelid sisse", + "dialog.model.unpaid.freeModels.title": "Tasuta mudeleid pakub OpenCode", + "dialog.model.unpaid.addMore.title": "Lisage populaarsete pakkujate mudeleid", + "dialog.model.unpaid.viewMoreProviders": "Vaadake veel 70+ pakkujat", + "dialog.provider.viewAll": "Kuva rohkem teenusepakkujaid", + "provider.connect.title": "Ühenda {{provider}}", + "provider.connect.title.anthropicProMax": "Logi sisse Claude Pro/Max", + "provider.connect.selectMethod": "Valige {{provider}} jaoks sisselogimisviis.", + "provider.connect.method.apiKey": "API klahv", + "provider.connect.method.browser": "Brauser", + "provider.connect.method.headless": "Peata", + "provider.connect.status.inProgress": "Autoriseerimine on pooleli...", + "provider.connect.status.waiting": "Autoriseerimise ootel...", + "provider.connect.status.failed": "Autoriseerimine ebaõnnestus: {{error}}", + "provider.connect.apiKey.description": + "Sisestage {{provider}} API võti, et ühendada konto ja kasutada {{provider}} mudeleid OpenCode'is.", + "provider.connect.apiKey.label": "{{provider}} API klahv", + "provider.connect.apiKey.placeholder": "API klahv", + "provider.connect.apiKey.required": "Klahv API on nõutav", + "provider.connect.opencodeZen.line1": + "OpenCode Zen annab teile juurdepääsu kodeerimisagentide usaldusväärsete optimeeritud mudelite kureeritud komplektile.", + "provider.connect.opencodeZen.line2": + "Ühe klahviga API pääsete juurde sellistele mudelitele nagu Claude, GPT, Gemini, GLM ja palju muud.", + "provider.connect.opencodeZen.visit.prefix": "Külastage ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " API võtme kättesaamiseks.", + "provider.connect.oauth.code.visit.prefix": "Külastage ", + "provider.connect.oauth.code.visit.link": "see link", + "provider.connect.oauth.code.visit.suffix": + " konto ühendamiseks autoriseerimiskoodi kogumiseks ja {{provider}} mudeli kasutamiseks jaotises OpenCode.", + "provider.connect.oauth.code.label": "{{method}} autoriseerimiskood", + "provider.connect.oauth.code.placeholder": "Autoriseerimiskood", + "provider.connect.oauth.code.required": "Vaja on autoriseerimiskoodi", + "provider.connect.oauth.code.invalid": "Vale autoriseerimiskood", + "provider.connect.oauth.auto.visit.prefix": "Külastage ", + "provider.connect.oauth.auto.visit.link": "see link", + "provider.connect.oauth.auto.visit.suffix": + " ja sisestage allolev kood, et ühendada oma konto ja kasutada {{provider}} mudelit jaotises OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Kinnituskood", + "provider.connect.toast.connected.title": "{{provider}} ühendatud", + "provider.connect.toast.connected.description": "{{provider}} mudelit on nüüd kasutamiseks saadaval.", + "provider.custom.title": "Kohandatud pakkuja", + "provider.custom.unavailable": "Kohandatud pakkujad pole selles serveris saadaval", + "provider.custom.description.prefix": "Konfigureerige OpenAI-ga ühilduv pakkuja. Vaadake ", + "provider.custom.description.link": "pakkuja konfiguratsioonidokumendid", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Pakkuja ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Väikesed tähed, numbrid, sidekriipsud või allkriipsud", + "provider.custom.field.name.label": "Kuvatav nimi", + "provider.custom.field.name.placeholder": "Minu AI pakkuja", + "provider.custom.field.baseURL.label": "Baas URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API klahv", + "provider.custom.field.apiKey.placeholder": "API klahv", + "provider.custom.field.apiKey.description": "Valikuline. Jätke tühjaks, kui haldate autentimist päiste kaudu.", + "provider.custom.models.label": "Mudelid", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Nimi", + "provider.custom.models.name.placeholder": "Kuvatav nimi", + "provider.custom.models.remove": "Eemalda mudel", + "provider.custom.models.add": "Lisa mudel", + "provider.custom.headers.label": "Päised (valikuline)", + "provider.custom.headers.key.label": "Päis", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Väärtus", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "Eemalda päis", + "provider.custom.headers.add": "Lisa päis", + "provider.custom.error.providerID.required": "Pakkuja ID on nõutav", + "provider.custom.error.providerID.format": "Kasutage väiketähti, numbreid, sidekriipse või allkriipse", + "provider.custom.error.providerID.exists": "See pakkuja ID on juba olemas", + "provider.custom.error.name.required": "Kuvatav nimi on kohustuslik", + "provider.custom.error.baseURL.required": "Alus URL on nõutav", + "provider.custom.error.baseURL.format": "Peab algama http:// või https://", + "provider.custom.error.required": "Nõutav", + "provider.custom.error.duplicate": "Duplikaat", + "provider.disconnect.toast.disconnected.title": "{{provider}} on katkestatud", + "provider.disconnect.toast.disconnected.description": "{{provider}} mudelit pole enam saadaval.", + "model.tag.free": "Tasuta", + "model.tag.latest": "Viimased", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekst", + "model.input.image": "pilt", + "model.input.audio": "heli", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Lubab: {{inputs}}", + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Sisendid", + "model.tooltip.model": "Mudel", + "model.tooltip.provider": "Pakkuja", + "model.tooltip.reasoning": "Arutluskäik", + "model.tooltip.reasoning.allowed": "Võimaldab arutleda", + "model.tooltip.reasoning.none": "Ei mingit põhjendust", + "model.tooltip.context": "Kontekstipiirang {{limit}}", + "common.search.placeholder": "Otsi", + "common.goBack": "Navigeerige tagasi", + "common.goForward": "Liikuge edasi", + "common.loading": "Laadimine", + "common.loading.ellipsis": "...", + "common.cancel": "Tühista", + "common.clear": "Selge", + "common.open": "Avatud", + "common.connect": "Ühendage", + "common.disconnect": "Katkesta ühendus", + "common.continue": "Jätka", + "common.submit": "Esita", + "common.save": "Salvesta", + "common.saving": "Salvestamine...", + "common.default": "Vaikimisi", + "common.attachment": "manus", + "prompt.placeholder.shell": "Sisestage shellikäsk... {{example}}", + "prompt.placeholder.normal": 'Küsige mida tahes... "{{example}}"', + "prompt.placeholder.simple": "Küsi midagi...", + "prompt.placeholder.summarizeComments": "Tee kommentaaridest kokkuvõte…", + "prompt.placeholder.summarizeComment": "Kommentaari kokkuvõte…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Viip", + "prompt.mode.shell.exit": "esc väljumiseks", + "session.child.promptDisabled": "Alamagendi seansse ei saa küsida.", + "session.child.backToParent": "Tagasi põhiseansse.", + "prompt.example.1": "Parandage koodibaasis TODO", + "prompt.example.2": "Mis on selle projekti tehniline stack?", + "prompt.example.3": "Parandage katkised testid", + "prompt.example.4": "Selgitage, kuidas autentimine töötab", + "prompt.example.5": "Otsige ja parandage turvaauke", + "prompt.example.6": "Lisage kasutajateenuse ühikutestid", + "prompt.example.7": "Muutke see funktsioon loetavamaks", + "prompt.example.8": "Mida see viga tähendab?", + "prompt.example.9": "Aidake mul seda probleemi siluda", + "prompt.example.10": "Loo API dokumentatsioon", + "prompt.example.11": "Optimeerige andmebaasi päringuid", + "prompt.example.12": "Lisage sisendi valideerimine", + "prompt.example.13": "Looge uus komponent ...", + "prompt.example.14": "Kuidas seda projekti juurutada?", + "prompt.example.15": "Heade tavade saamiseks vaadake üle minu kood", + "prompt.example.16": "Lisage sellele funktsioonile veakäsitlus", + "prompt.example.17": "Selgitage seda regex-mustrit", + "prompt.example.18": "Teisenda see väärtuseks TypeScript", + "prompt.example.19": "Lisage logimine kogu koodibaasi", + "prompt.example.20": "Millised sõltuvused on aegunud?", + "prompt.example.21": "Aidake mul migratsiooniskripti kirjutada", + "prompt.example.22": "Rakendage selle lõpp-punkti vahemälu", + "prompt.example.23": "Lisage sellele loendile lehekülgede arv", + "prompt.example.24": "Looge CLI käsk...", + "prompt.example.25": "Kuidas keskkonnamuutujad siin töötavad?", + "prompt.popover.emptyResults": "Vastavaid tulemusi pole", + "prompt.popover.emptyCommands": "Ühtegi sobivat käsku pole", + "prompt.dropzone.label": "Pukseerige siia pildid, PDFs või tekstifailid", + "prompt.dropzone.file.label": "Pukseerige @mainimisfaili", + "prompt.slash.badge.custom": "kohandatud", + "prompt.slash.badge.skill": "oskus", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktiivne", + "prompt.context.includeActiveFile": "Kaasa aktiivne fail", + "prompt.context.removeActiveFile": "Eemaldage aktiivne fail kontekstist", + "prompt.context.removeFile": "Eemaldage fail kontekstist", + "prompt.action.attachFile": "Lisa faile", + "prompt.menu.addImagesAndFiles": "Lisage faile ja palju muud", + "prompt.menu.imagesAndFiles": "Pildid ja failid", + "prompt.menu.commands": "Käsud", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Shelli käsk", + "prompt.attachment.remove": "Eemalda manus", + "prompt.action.send": "Saada", + "prompt.action.stop": "Peatus", + "prompt.toast.pasteUnsupported.title": "Toetamata manus", + "prompt.toast.pasteUnsupported.description": "Siia saab lisada ainult pilte, PDFs või tekstifaile.", + "prompt.toast.attachmentDuplicate.title": "See fail on juba üles laaditud", + "prompt.toast.modelAgentRequired.title": "Valige agent ja mudel", + "prompt.toast.modelAgentRequired.description": "Enne viipa saatmist valige agent ja mudel.", + "prompt.toast.worktreeCreateFailed.title": "Tööpuu loomine ebaõnnestus", + "prompt.toast.sessionCreateFailed.title": "Seansi loomine ebaõnnestus", + "prompt.toast.shellSendFailed.title": "Shelli käsu saatmine ebaõnnestus", + "prompt.toast.commandSendFailed.title": "Käsu saatmine ebaõnnestus", + "prompt.toast.promptSendFailed.title": "Viipa saatmine ebaõnnestus", + "prompt.toast.promptSendFailed.description": "Seanssi ei saa tuua", + "dialog.mcp.title": "MCP-d", + "dialog.mcp.description": "{{enabled}} / {{total}} on lubatud", + "dialog.mcp.empty": "MCP-sid pole konfigureeritud", + "dialog.lsp.empty": "LSP-d tuvastatakse failitüüpide järgi automaatselt", + "dialog.plugins.empty": "Pluginad on konfigureeritud failis opencode.json", + "mcp.status.connected": "ühendatud", + "mcp.status.failed": "ebaõnnestunud", + "mcp.status.needs_auth": "vajab autentimist", + "mcp.status.disabled": "puudega", + "mcp.auth.clickToAuthenticate": "Klõpsake autentimiseks", + "dialog.fork.empty": "Ühtegi sõnumit pole", + "dialog.directory.search.placeholder": "Otsi kaustadest", + "dialog.directory.empty": "Ühtegi kausta ei leitud", + "dialog.directory.action.selectFile": "Valige fail", + "dialog.directory.action.selectFolder": "Valige kaust", + "dialog.directory.root": "Juur", + "dialog.directory.parent": "Lapsevanem", + "dialog.directory.readError": "Seda kausta ei saa lugeda", + "app.server.unreachable": "Ei saanud ühendust {{server}}", + "app.server.retrying": "Automaatne uuesti proovimine...", + "app.server.otherServers": "Muud serverid", + "dialog.server.title": "Serverid", + "dialog.server.description": "Lülitage, millise OpenCode serveriga see rakendus ühenduse loob.", + "dialog.server.search.placeholder": "Otsi serveritest", + "dialog.server.empty": "Servereid veel pole", + "dialog.server.add.title": "Lisa server", + "dialog.server.add.url": "Serveri aadress", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Serveriga ei õnnestunud ühendust luua", + "dialog.server.add.checking": "Kontrollimine...", + "dialog.server.add.button": "Lisa server", + "dialog.server.add.name": "Serveri nimi (valikuline)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Kasutajanimi (valikuline)", + "dialog.server.add.usernamePlaceholder": "kasutajanimi", + "dialog.server.add.password": "Parool (valikuline)", + "dialog.server.add.passwordPlaceholder": "parool", + "dialog.server.edit.title": "Muuda serverit", + "dialog.server.default.title": "Vaikimisi server", + "dialog.server.default.description": + "Ühendage selle serveriga rakenduse käivitamisel kohaliku serveri käivitamise asemel. Nõuab taaskäivitamist.", + "dialog.server.default.none": "Serverit pole valitud", + "dialog.server.default.set": "Määra praegune server vaikeväärtuseks", + "dialog.server.default.clear": "Selge", + "dialog.server.action.remove": "Eemalda server", + "dialog.server.menu.edit": "Muuda", + "dialog.server.menu.default": "Määra vaikeväärtuseks", + "dialog.server.menu.defaultRemove": "Eemalda vaikeseade", + "dialog.server.menu.delete": "Kustuta", + "dialog.server.current": "Praegune server", + "dialog.server.status.default": "Vaikimisi", + "wsl.server.add": "Lisa WSL server", + "wsl.server.addShort": "Lisa WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL server", + "wsl.server.retryStart": "Proovi uuesti alustada", + "wsl.server.updating": "Värskendamine...", + "wsl.onboarding.step.distro": "Valige distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL kontrollimine...", + "wsl.onboarding.restartRequired": "Windows vajab installimise WSL lõpetamiseks taaskäivitamist.", + "wsl.onboarding.ready": "WSL on valmis.", + "wsl.onboarding.required": "Jätkamiseks on vaja WSL.", + "wsl.onboarding.checkingDistros": "Distrode kontrollimine...", + "wsl.onboarding.installingDistro": "{{distro}} installimine...", + "wsl.onboarding.checkingDistro": "{{distro}} kontrollimine...", + "wsl.onboarding.listingDistros": "Distrode loend...", + "wsl.onboarding.distroReady": "{{distro}} on valmis.", + "wsl.onboarding.distroNotInstalled": "{{distro}} pole veel installitud.", + "wsl.onboarding.openDistroOnce": "Seadistamise lõpetamiseks avage üks kord {{distro}}.", + "wsl.onboarding.finishingDistro": "Seadistuse lõpetamine {{distro}} jaoks.", + "wsl.onboarding.pickDistro": "Valige distro või installige see allpool.", + "wsl.onboarding.checkingOpencode": "OpenCode kontrollimine...", + "wsl.onboarding.checkingOpencodeIn": "OpenCode kontrollimine asukohas {{distro}}...", + "wsl.onboarding.updatingOpencode": "OpenCode värskendamine...", + "wsl.onboarding.updatingOpencodeIn": "OpenCode värskendamine asukohas {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Värskenda OpenCode jaotises {{distro}}.", + "wsl.onboarding.updateOpencode": "Värskenda OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode on valmis {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode on valmis.", + "wsl.onboarding.installOpencodeIn": "Installige OpenCode asukohta {{distro}}.", + "wsl.onboarding.installOpencode": "Installige OpenCode", + "wsl.onboarding.chooseDistroFirst": "Valige esmalt distro.", + "wsl.onboarding.loadFailed": "Oleku WSL laadimine ebaõnnestus.", + "wsl.onboarding.loading": "Laadimine...", + "wsl.onboarding.installedDistros": "Installitud distrood", + "wsl.onboarding.checkAgain": "Kontrollige uuesti", + "wsl.onboarding.distroStatus.ready": "Valmis", + "wsl.onboarding.distroStatus.checking": "Kontrollimine...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode pole installitud", + "wsl.onboarding.distroStatus.missingTools": "Puuduvad bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Toetamata · Kasutage WSL 2", + "wsl.onboarding.needAnotherDistro": "Kas vajate teist distrot?", + "wsl.onboarding.needAnotherDistroHint": "Installige Linux distributsioon kataloogist WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL pole installitud", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows alamsüsteem Linux jaoks) on nõutav, enne kui OpenCode saab lisada WSL serveri", + "wsl.onboarding.wslUnavailable.title": "WSL pole saadaval", + "wsl.onboarding.wslUnavailable.description": "OpenCode ei saanud selles masinas kontrollida WSL.", + "wsl.onboarding.installWsl": "Installige WSL", + "wsl.onboarding.windowsRestartRequired": "Taaskäivitage Windows, et installida WSL, seejärel avage uuesti OpenCode.", + "wsl.onboarding.next": "Edasi", + "wsl.onboarding.refresh": "Värskenda", + "wsl.onboarding.allDistrosAdded": "Kõik installitud distrood on juba lisatud.", + "wsl.onboarding.noDistros": "Ühtegi distrosid pole veel tuvastatud.", + "wsl.onboarding.install": "Installige", + "wsl.onboarding.installing": "Installimine...", + "wsl.onboarding.installDistro": "Installige distro", + "wsl.onboarding.searchDistros": "Otsige distrosid", + "wsl.onboarding.wsl2Required": "WSL 2 on nõutav.", + "wsl.onboarding.toolsRequired": "See distro vajab bash ja curl.", + "wsl.onboarding.openTerminal": "Avage terminal", + "wsl.onboarding.path": "Tee: {{path}}", + "wsl.onboarding.notFound": "ei leitud", + "wsl.onboarding.version": "Versioon: {{version}}", + "wsl.onboarding.unknown": "teadmata", + "wsl.onboarding.desktopVersion": "töölaud {{version}}", + "wsl.onboarding.versionMismatch": "Installitud versioon ei ühti töölauarakenduse versiooniga.", + "wsl.onboarding.adding": "Lisamine...", + "help.tabs.toast.ariaLabel": "Tutvustame vahekaarte. Korraldage oma tööd ja aktiivseid seansse vahekaartide abil", + "help.tabs.toast.dismiss": "Loobu vahelehtede teabest", + "help.tabs.title": "Tutvustame vahekaarte", + "help.tabs.description": "Korraldage oma tööd ja aktiivseid seansse vahekaartide abil", + "help.tabs.date": "14. juuli", + "help.tabs.introduction": "OpenCode Töölaud on nüüd üles ehitatud vahekaartide ümber.", + "help.tabs.sessions": + "Alustage vahekaardil uut seanssi või avage mis tahes projektist olemasolev seanss. Kui alustate midagi uut, avage uus vaheleht ja sulgege see, kui olete lõpetanud.", + "help.tabs.organize": + "Mõne vahelehe avatuna hoidmine muudab aktiivsete seansside korraldamise lihtsamaks. Nimetage vahekaardid ümber millekski meeldejäävaks, kui kavatsete need alles jätta.", + "help.tabs.home": "Kõik seansid ja projektid leiate uuelt avalehelt. Seansi valimine avab selle vahekaardil.", + "help.tabs.persistence": "Kui avate rakenduse uuesti, on teie vahelehed endiselt avatud.", + "help.tabs.worktrees": + "Uus disain ei toeta veel Git tööpuud, see on peagi saadaval. Seega, kui eelistate jätkata eelmise paigutuse kasutamist, saate seadetes küljendusi vahetada. Pidage vaid meeles, et uus paigutus muutub mõne nädala pärast püsivaks.", + "server.row.noUsername": "kasutajanime pole", + "dialog.project.edit.title": "Redigeeri projekti", + "dialog.project.edit.name": "Nimi", + "dialog.project.edit.icon": "Ikoon", + "dialog.project.edit.icon.alt": "Projekti ikoon", + "dialog.project.edit.icon.hint": "Klõpsake või lohistage pilti", + "dialog.project.edit.icon.recommended": "Soovitatav: 128 x 128 pikslit", + "dialog.project.edit.color": "Värv", + "dialog.project.edit.color.select": "Valige {{color}} värv", + "dialog.project.edit.worktree.startup": "Tööruumi käivitusskript", + "dialog.project.edit.worktree.startup.description": "Käivitatakse pärast uue tööruumi (tööpuu) loomist.", + "dialog.project.edit.worktree.startup.placeholder": "nt. bun install", + "dialog.releaseNotes.action.getStarted": "Alustage", + "dialog.releaseNotes.action.next": "Edasi", + "dialog.releaseNotes.action.hideFuture": "Ärge näidake neid tulevikus", + "dialog.releaseNotes.media.alt": "Väljalase eelvaade", + "context.breakdown.title": "Konteksti jaotus", + "context.breakdown.note": 'Sisendmärkide ligikaudne jaotus. "Muu" hõlmab tööriistade määratlusi ja üldkulusid.', + "context.breakdown.system": "Süsteem", + "context.breakdown.user": "Kasutaja", + "context.breakdown.assistant": "Assistent", + "context.breakdown.tool": "Tööriistakõned", + "context.breakdown.other": "muud", + "context.systemPrompt.title": "Süsteemi viip", + "context.rawMessages.title": "Toores sõnumid", + "context.export.session": "Ekspordiseanss", + "context.stats.session": "Seanss", + "context.stats.messages": "Sõnumid", + "context.stats.provider": "Pakkuja", + "context.stats.model": "Mudel", + "context.stats.limit": "Konteksti piirang", + "context.stats.totalTokens": "Märke kokku", + "context.stats.usage": "Kasutamine", + "context.stats.inputTokens": "Sisestusmärgid", + "context.stats.outputTokens": "Väljundmärgid", + "context.stats.reasoningTokens": "Arutlusmärgid", + "context.stats.cacheTokens": "Vahemälu märgid (lugemine/kirjutamine)", + "context.stats.userMessages": "Kasutaja sõnumid", + "context.stats.assistantMessages": "Assistendi sõnumid", + "context.stats.totalCost": "Kogukulu", + "context.stats.sessionCreated": "Seanss loodud", + "context.stats.lastActivity": "Viimane tegevus", + "context.usage.tokens": "Tokenid", + "context.usage.usage": "Kasutamine", + "context.usage.cost": "Maksumus", + "context.usage.clickToView": "Klõpsake konteksti vaatamiseks", + "context.usage.view": "Vaadake kontekstikasutust", + "language.en": "inglise keel", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "portugali keel (Brasiilia)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Keel", + "toast.language.description": "Lülitati numbrile {{language}}", + "toast.theme.title": "Teema vahetatud", + "toast.scheme.title": "Värvilahendus", + "toast.workspace.enabled.title": "Tööruumid on lubatud", + "toast.workspace.enabled.description": "Nüüd kuvatakse külgribal mitu tööpuud", + "toast.workspace.disabled.title": "Tööruumid on keelatud", + "toast.workspace.disabled.description": "Külgribal kuvatakse ainult peamine tööpuud", + "toast.permissions.autoaccept.on.title": "Lubade automaatne aktsepteerimine", + "toast.permissions.autoaccept.on.description": "Loataotlused kinnitatakse automaatselt", + "toast.permissions.autoaccept.off.title": "Lubade automaatne vastuvõtmine on peatatud", + "toast.permissions.autoaccept.off.description": "Loataotlused nõuavad kinnitust", + "toast.model.none.title": "Ühtegi mudelit pole valitud", + "toast.model.none.description": "Seansi kokkuvõtte tegemiseks ühendage teenusepakkuja", + "toast.file.loadFailed.title": "Faili laadimine ebaõnnestus", + "toast.file.listFailed.title": "Failide loetlemine ebaõnnestus", + "toast.context.noLineSelection.title": "Rea valikut pole", + "toast.context.noLineSelection.description": "Valige faili vahekaardilt esmalt reavahemik.", + "toast.session.share.copyFailed.title": "URL lõikelauale kopeerimine ebaõnnestus", + "toast.session.share.success.title": "Seanss jagatud", + "toast.session.share.success.description": "Jaga URL kopeeriti lõikelauale!", + "toast.session.share.failed.title": "Seansi jagamine ebaõnnestus", + "toast.session.share.failed.description": "Seansi jagamisel ilmnes viga", + "toast.session.unshare.success.title": "Seansi jagamine tühistati", + "toast.session.unshare.success.description": "Seansi jagamise tühistamine õnnestus!", + "toast.session.unshare.failed.title": "Seansi jagamise tühistamine ebaõnnestus", + "toast.session.unshare.failed.description": "Seansi jagamise tühistamisel ilmnes viga", + "toast.session.export.success.title": "Seanss eksporditud", + "toast.session.export.success.description": "Seanss salvestatud asukohta {{filename}}", + "toast.session.export.failed.title": "Seansi eksportimine ebaõnnestus", + "toast.session.export.failed.description": "Seansi eksportimisel ilmnes viga", + "toast.session.listFailed.title": "{{project}} seansside laadimine ebaõnnestus", + "toast.project.reloadFailed.title": "{{project}} uuesti laadimine ebaõnnestus", + "toast.update.title": "Värskendus saadaval", + "toast.update.description": "Rakenduse OpenCode ({{version}}) uus versioon on nüüd installimiseks saadaval.", + "toast.update.action.installRestart": "Installige ja taaskäivitage", + "toast.update.action.notYet": "Veel mitte", + "error.page.title": "Midagi läks valesti", + "error.page.description": "Rakenduse laadimisel ilmnes viga.", + "error.page.description.localServerStartup": "Kohaliku serveri käivitamisel ilmnes viga.", + "error.page.details.label": "Vea üksikasjad", + "error.page.action.restart": "Taaskäivitage", + "error.page.action.report": "Teata veast", + "error.page.action.reported": "Teatatud viga", + "error.page.action.exportLogs": "Ekspordi logid", + "error.page.action.checking": "Kontrollimine...", + "error.page.action.checkUpdates": "Kontrollige värskendusi", + "error.page.action.updateTo": "Värskenda väärtusele {{version}}", + "error.page.circular": "[Ringkiri]", + "error.page.report.prefix": "Teatage sellest veast OpenCode meeskonnale", + "error.page.report.discord": "kuupäeval Discord", + "error.page.version": "Versioon: {{version}}", + "error.dev.rootNotFound": + "Juurelementi ei leitud. Kas unustasite selle lisada oma loendisse index.html? Või äkki on id-atribuut valesti kirjutatud?", + "error.serverSync.connectFailed": "Serveriga ei õnnestunud ühendust luua. Kas aadressil `{{url}}` töötab server?", + "error.serverSDK.noServerAvailable": "Serverit pole saadaval", + "error.serverSDK.serverNotAvailable": "Server pole saadaval", + "error.childStore.persistedCacheCreateFailed": "Püsiva vahemälu loomine ebaõnnestus", + "error.childStore.persistedProjectMetadataCreateFailed": "Püsiva projekti metaandmete loomine ebaõnnestus", + "error.childStore.persistedProjectIconCreateFailed": "Püsiva projekti ikooni loomine ebaõnnestus", + "error.childStore.storeCreateFailed": "Poe loomine ebaõnnestus", + "directory.error.invalidUrl": "Kehtetu kataloog asukohas URL.", + "error.chain.unknown": "Tundmatu viga", + "error.server.invalidConfiguration": "Kehtetu konfiguratsioon", + "error.chain.causedBy": "Põhjustatud:", + "error.chain.apiError": "API viga", + "error.chain.status": "Olek: {{status}}", + "error.chain.retryable": "Uuesti proovitav: {{retryable}}", + "error.chain.responseBody": "Vastuse keha:\n{{body}}", + "error.chain.didYouMean": "Kas mõtlesite: {{suggestions}}", + "error.chain.modelNotFound": "Mudelit ei leitud: {{provider}}/{{model}}", + "error.chain.checkConfig": "Kontrollige oma konfiguratsiooni (opencode.json) pakkuja/mudeli nimesid", + "error.chain.mcpFailed": 'MCP server "{{name}}" ebaõnnestus. Pange tähele, OpenCode ei toeta veel MCP autentimist.', + "error.chain.providerAuthFailed": "Pakkuja autentimine ebaõnnestus ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Teenusepakkuja "{{provider}}" lähtestamine ebaõnnestus. Kontrollige mandaate ja konfiguratsiooni.', + "error.chain.configJsonInvalid": "Konfiguratsioonifail asukohas {{path}} ei kehti JSON (C)", + "error.chain.configJsonInvalidWithMessage": "Konfiguratsioonifail asukohas {{path}} ei kehti JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Kataloog "{{dir}}" asukohas {{path}} ei kehti. Nimetage kataloog ümber "{{suggestion}}" või eemaldage see. See on tavaline kirjaviga.', + "error.chain.configFrontmatterError": "Frontmatteri sõelumine failis {{path}} ebaõnnestus:\n{{message}}", + "error.chain.configInvalid": "Konfiguratsioonifail aadressil {{path}} on kehtetu", + "error.chain.configInvalidWithMessage": "Konfiguratsioonifail aadressil {{path}} on kehtetu: {{message}}", + "notification.permission.title": "Nõutav luba", + "notification.permission.description": "{{sessionTitle}} asukohas {{projectName}} vajab luba", + "notification.question.title": "küsimus", + "notification.question.description": "{{sessionTitle}} kasutajal {{projectName}} on küsimus", + "notification.action.goToSession": "Mine seansile", + "notification.session.responseReady.title": "Vastus valmis", + "notification.session.error.title": "Seansi viga", + "notification.session.error.fallbackDescription": "Ilmnes viga", + "home.recentProjects": "Viimased projektid", + "home.empty.title": "Viimaseid projekte pole", + "home.empty.description": "Alustage kohaliku projekti avamisega", + "home.title": "Avaleht", + "home.projects": "Projektid", + "home.project.add": "Lisa projekt", + "home.recentlyClosed": "Hiljuti suletud", + "home.server.collapse": "Ahenda serveriprojektid", + "home.server.expand": "Laiendage serveriprojekte", + "home.sessions.search.placeholder": "Otsi seansse", + "home.sessions.search.placeholder.scoped": "Otsige seansse asukohas {{scope}}", + "home.sessions.search.sessions": "Seansid", + "home.sessions.search.noResults": "{{query}} jaoks ei leitud seansse", + "home.sessions.empty": "Siin pole veel midagi", + "home.sessions.empty.description": "Alustamiseks looge seanss", + "home.sessions.group.today": "Täna", + "home.sessions.group.yesterday": "eile", + "home.sessions.group.older": "Vanemad", + "home.providerTip": "Ühendage 75+ teenusepakkujaga, et kasutada muid mudeleid, sealhulgas Claude, GPT, Gemini jne", + "session.tab.session": "Seanss", + "session.tab.review": "Ülevaade", + "session.tab.context": "Kontekst", + "session.tab.unknown": "Tundmatu seanss", + "session.panel.reviewAndFiles": "Ülevaatus ja failid", + "session.error.notFound": "Seda seanssi ei leitud", + "session.error.notFound.description": "See vahekaart osutab seansile, mida selles serveris enam ei eksisteeri.", + "session.error.notFound.closeTab": "Sule vahekaart", + "session.error.serverConnection": "Selle serveriga ei saa ühendust", + "session.review.filesChanged": "Muudetud failid {{count}}", + "session.review.change.one": "Muuda", + "session.review.change.other": "Muudatused", + "session.review.loadingChanges": "Muudatuste laadimine...", + "session.review.empty": "Sellel seansil pole veel muudatusi", + "session.review.noVcs": "Ei tuvastatud Git Versioonikontrollisüsteemi, muudatusi ei kuvata", + "session.review.noVcs.createGit.title": "Loo Git hoidla", + "session.review.noVcs.createGit.description": "Selle projekti muudatuste jälgimine, ülevaatamine ja tagasivõtmine", + "session.review.noVcs.createGit.actionLoading": "Git hoidla loomine...", + "session.review.noVcs.createGit.action": "Loo Git hoidla", + "session.review.noSnapshot": + "Hetketõmmise jälgimine on konfiguratsioonis keelatud, seega pole seansi muudatused saadaval", + "session.review.noChanges": "Muudatusi pole", + "session.review.noUncommittedChanges": "Tehmata muudatusi pole veel tehtud", + "session.review.noBranchChanges": "Filiaali muudatusi veel pole", + "session.files.selectToOpen": "Valige avamiseks fail", + "session.files.all": "Kõik failid", + "session.files.empty": "Faile pole", + "session.files.binaryContent": "Binaarfail (sisu ei saa kuvada)", + "session.messages.renderEarlier": "Renderda varasemaid sõnumeid", + "session.messages.loadingEarlier": "Varasemate sõnumite laadimine...", + "session.messages.loadEarlier": "Laadige varasemad sõnumid", + "session.messages.loading": "Sõnumite laadimine...", + "session.messages.jumpToLatest": "Hüppa uusimale", + "session.context.addToContext": "Lisage konteksti {{selection}}", + "session.todo.title": "Ülesanded", + "session.todo.collapse": "Ahenda", + "session.todo.expand": "Laienda", + "session.todo.progress": "{{done}} ülesannet {{total}}-st lõpetatud", + "session.question.progress": "{{current}} küsimust {{total}}-st", + "session.question.minimize": "Minimeerige küsimus", + "session.question.restore": "Taasta küsimus", + "session.question.pending.one": "{{count}} ootel küsimus", + "session.question.pending.other": "{{count}} ootel küsimust", + "session.followupDock.summary.one": "{{count}} järjekorda pandud sõnum", + "session.followupDock.summary.other": "{{count}} järjekorras olevat sõnumit", + "session.followupDock.sendNow": "Saada kohe", + "session.followupDock.edit": "Muuda", + "session.followupDock.collapse": "Ahenda järjekorda pandud kirjad", + "session.followupDock.expand": "Laiendage järjekorras olevaid sõnumeid", + "session.revertDock.summary.one": "{{count}} tagasikeeratud sõnum", + "session.revertDock.summary.other": "{{count}} tagasipööratud sõnumit", + "session.revertDock.collapse": "Ahenda tagasi keritud sõnumid", + "session.revertDock.expand": "Laienda tagasipööratud sõnumeid", + "session.revertDock.restore": "Taasta sõnum", + "session.new.title": "Ehitage midagi", + "session.new.project.new": "Uus projekt", + "session.new.project.search": "Otsige projekte", + "session.new.project.add": "Lisa projekt", + "session.new.worktree.main": "Peaharu", + "session.new.worktree.mainWithBranch": "Põhiharu ({{branch}})", + "session.new.worktree.create": "Loo uus tööpuu", + "session.new.workspace.runIn": "Käivitage seanss", + "session.new.workspace.triggerLocal": "Kohalik", + "session.new.workspace.local": "Kohalik hoidla", + "session.new.workspace.existing": "Tööruum…", + "session.new.git.none": "Ei Git", + "session.new.lastModified": "Viimati muudetud", + "session.header.search.placeholder": "Otsi {{project}}", + "session.header.searchFiles": "Otsige faile", + "session.header.openIn": "Ava sisse", + "session.header.open.action": "Ava {{app}}", + "session.header.open.ariaLabel": "Avatud kohas {{app}}", + "session.header.open.menu": "Avage valikud", + "session.header.open.copyPath": "Kopeeri tee", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "Failihaldur", + "session.header.reveal.finder": "Avalda Finder", + "session.header.reveal.fileExplorer": "Avalda File Explorer", + "session.header.reveal.containingFolder": "Ava sisaldav kaust", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Olek", + "status.popover.ariaLabel": "Serveri konfiguratsioonid", + "status.popover.tab.servers": "Serverid", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Pluginad", + "status.popover.action.manageServers": "Hallake servereid", + "session.share.popover.title": "Avalda veebis", + "session.share.popover.description.shared": + "See seanss on veebis avalik. See on juurdepääsetav kõigile, kellel on link.", + "session.share.popover.description.unshared": + "Jagage seanssi avalikult veebis. See on juurdepääsetav kõigile, kellel on link.", + "session.share.action.share": "Jaga", + "session.share.action.publish": "Avalda", + "session.share.action.publishing": "Avaldamine...", + "session.share.action.unpublish": "Tühista avaldamine", + "session.share.action.unpublishing": "Avaldamise tühistamine...", + "session.share.action.view": "Vaade", + "session.share.copy.copied": "Kopeeritud", + "session.share.copy.copyLink": "Kopeeri link", + "lsp.tooltip.none": "LSP serverit pole", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Viipa laadimine...", + "terminal.loading": "Terminali laadimine...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Sulgege terminal", + "terminal.connectionLost.title": "Ühendus katkes", + "terminal.connectionLost.abnormalClose": "WebSocket on ebatavaliselt suletud: {{code}}", + "terminal.connectionLost.description": "Terminali ühendus katkes. See võib juhtuda serveri taaskäivitamisel.", + "terminal.connectTicket.csrfError": + "PTY ühenduse pilet lükati tagasi päritolu või CSRF kontrolli tõttu. Kontrollige serveri CORS konfiguratsiooni.", + "terminal.connectTicket.statusError": "PTY ühenduspilet ebaõnnestus kasutajaga {{status}}", + "titlebar.update": "Värskenda", + "titlebar.updateVersion": "Värskenda {{version}}", + "common.closeTab": "Sule vahekaart", + "common.dismiss": "Loobu", + "common.moreCountSuffix": " (+{{count}} veel)", + "common.requestFailed": "Taotlus ebaõnnestus", + "common.moreOptions": "Rohkem valikuid", + "common.learnMore": "Lisateavet", + "common.rename": "Nimeta ümber", + "common.export": "Ekspordi", + "common.reset": "Lähtesta", + "common.archive": "Arhiiv", + "common.delete": "Kustuta", + "common.close": "Sule", + "common.edit": "Muuda", + "common.loadMore": "Laadige rohkem", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "teadmata", + "common.time.justNow": "Just praegu", + "common.time.minutesAgo.short": "{{count}} min tagasi", + "common.time.hoursAgo.short": "{{count}} h tagasi", + "common.time.daysAgo.short": "{{count}}p tagasi", + "sidebar.menu.toggle": "Lülitage menüü", + "sidebar.nav.projectsAndSessions": "Projektid ja istungid", + "sidebar.settings": "Seaded", + "sidebar.help": "Abi", + "sidebar.workspaces.enable": "Luba tööruumid", + "sidebar.workspaces.disable": "Keela tööruumid", + "sidebar.gettingStarted.title": "Alustamine", + "sidebar.gettingStarted.line1": "OpenCode sisaldab tasuta mudeleid, nii et saate kohe alustada.", + "sidebar.gettingStarted.line2": "Mudelite kasutamiseks ühendage mis tahes pakkuja, sh. Claude, GPT, Gemini jne.", + "sidebar.project.recentSessions": "Viimased istungid", + "sidebar.project.viewAllSessions": "Vaadake kõiki seansse", + "sidebar.project.clearNotifications": "Tühjenda märguanded", + "sidebar.empty.title": "Avatud projekte pole", + "sidebar.empty.description": "Alustamiseks avage projekt", + "debugBar.ariaLabel": "Arendustegevuse diagnostika", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Viimane lõpetatud marsruudi üleminek, mis puudutab seansi lehte, mõõdetuna ruuteri algusest kuni esimese värvimiseni pärast selle settimist.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Veerevad kaadrid sekundis viimase 5 sekundi jooksul.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Halvim kaadriaeg viimase 5 sekundi jooksul.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Kaadrid üle 32 ms viimase 5 sekundi jooksul.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Blokeeritud aeg ja pikkade ülesannete arv viimase 5 sekundi jooksul. Maksimaalne ülesanne: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Halvim täheldatud sisendi viivitus viimase 5 sekundi jooksul.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Ligikaudne interaktsiooni kestus viimase 5 sekundi jooksul. See on INP-sarnane, mitte ametlik Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulatiivne paigutuse nihe praeguse rakenduse kasutusaja jooksul.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Kasutatud JS kuhja vs hunniku limiit. Chromium ainult.", + "debugBar.mem.tip": "Kasutatud JS kuhja vs hunniku limiit. {{used}} / {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Sundige fookusstiilid kõigile interaktiivsetele elementidele", + "debugBar.focus.on": "SEES", + "debugBar.focus.off": "VÄLJAS", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Sunnige kogu rakenduse paigutuse suund valitud keelt muutmata", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Töölaud", + "settings.section.desktop": "Töölaud", + "settings.section.server": "Server", + "settings.tab.general": "Kindral", + "settings.tab.shortcuts": "Otseteed", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integratsioon", + "settings.desktop.wsl.description": "Käivitage OpenCode server sees WSL saidil Windows.", + "settings.general.section.appearance": "Välimus", + "settings.general.section.advanced": "Täpsemalt", + "settings.general.section.notifications": "Süsteemi teatised", + "settings.general.section.updates": "Värskendused", + "settings.general.section.sounds": "Heliefektid", + "settings.general.section.feed": "Sööda", + "settings.general.section.display": "Ekraan", + "settings.general.row.language.title": "Keel", + "settings.general.row.language.description": "Muutke OpenCode kuvakeelt", + "settings.general.row.shell.title": "Terminali shell", + "settings.general.row.shell.description": "Terminali ja agenditööriistade kasutatav shell", + "settings.general.row.shell.autoDefault": "Automaatne (vaikimisi)", + "settings.general.row.shell.terminalOnly": "ainult terminal", + "settings.general.row.appearance.title": "Välimus", + "settings.general.row.appearance.description": "Kohandage, kuidas OpenCode teie seadmes välja näeb", + "settings.general.row.colorScheme.title": "Värvilahendus", + "settings.general.row.colorScheme.description": "Valige, kas OpenCode järgib süsteemi, heledat või tumedat teemat", + "settings.general.row.theme.title": "Teema", + "settings.general.row.theme.description": "Kohandage, kuidas OpenCode on teemastatud.", + "settings.general.row.font.title": "Koodi font", + "settings.general.row.font.description": "Kohandage koodiplokkides kasutatavat fonti", + "settings.general.row.terminalFont.title": "Terminali font", + "settings.general.row.terminalFont.description": "Kohandage terminalis kasutatavat fonti", + "settings.general.row.uiFont.title": "Kasutajaliidese font", + "settings.general.row.uiFont.description": "Kohandage kogu liideses kasutatavat fonti", + "settings.general.row.followup.title": "Järelkäitumine", + "settings.general.row.followup.description": "Valige, kas järelviibad juhivad kohe või oodake järjekorras", + "settings.general.row.followup.option.queue": "Järjekord", + "settings.general.row.followup.option.steer": "Juhtida", + "settings.general.row.showFileTree.title": "Failipuu", + "settings.general.row.showFileTree.description": "Failipuu paneeli kuvamine seanssides", + "settings.general.row.showNavigation.title": "Navigeerimisnupud", + "settings.general.row.showNavigation.description": "Näidake töölaua tiitliribal tagasi ja edasi nuppe", + "settings.general.row.showSearch.title": "Käsupalett", + "settings.general.row.showSearch.description": "Kuvage tiitliribal otsingu- ja käsupaleti nupp", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Kuvage töölaua tiitliribal terminali nupp", + "settings.general.row.showStatus.title": "Serveri olek", + "settings.general.row.showStatus.description": "Näita tiitliribal serveri olekunuppu", + "settings.general.row.mobileTitlebarBottom.title": "Alumine navigeerimine", + "settings.general.row.mobileTitlebarBottom.description": + "Asetage tiitliriba ja seansi vahekaardid mobiilis ekraani allossa", + "settings.general.row.showCustomAgents.title": "Näita agenti", + "settings.general.row.showCustomAgents.description": + "Helilooja agentide vahel vahetamine. Kui see on peidetud, on vaikimisi Agent.", + "settings.general.row.reasoningSummaries.title": "Näita põhjenduste kokkuvõtteid", + "settings.general.row.reasoningSummaries.description": "Kuva mudeli arutluskäigu kokkuvõtted ajaskaalal", + "settings.general.row.shellToolPartsExpanded.title": "Laienda shellitööriista osi", + "settings.general.row.shellToolPartsExpanded.description": + "Kuva shellitööriista osad ajaskaalal vaikimisi laiendatuna", + "settings.general.row.editToolPartsExpanded.title": "Laiendage tööriista osade redigeerimist", + "settings.general.row.editToolPartsExpanded.description": + "Kuva ajaskaalal vaikimisi laiendatud redigeerimis-, kirjutamis- ja paigatööriistade osad", + "settings.general.row.newInterface.title": "Uus paigutus", + "settings.general.row.newInterface.badge": "Uus", + "settings.general.row.newInterface.description": + "Kasutage uusi vahekaarte ja avalehe paigutust. Lülituge piiratud aja jooksul paigutuste vahel.", + "settings.general.row.newInterfaceNotice.title": "Kasutate nüüd uut paigutust", + "settings.general.row.newInterfaceNotice.description": "Eelmine paigutus pole enam saadaval", + "settings.general.row.newInterfaceNotice.dismiss": "Loobu", + "settings.general.row.pinchZoom.title": "Suumimiseks näpistage", + "settings.general.row.pinchZoom.description": "Lubage suumimiseks puuteplaadi kokkusurumine ja Ctrl-kerimine", + "settings.general.row.wayland.title": "Kasuta omakeelset Wayland", + "settings.general.row.wayland.description": "Keela X11 varundus Wayland. Nõuab taaskäivitamist.", + "settings.general.row.wayland.tooltip": + "Segatud värskendussagedusega kuvarite puhul Linux võib natiivne Wayland olla stabiilsem.", + "settings.general.row.releaseNotes.title": "Väljalaske märkmed", + "settings.general.row.releaseNotes.description": "Kuva Mis on uut hüpikaknad pärast värskendusi", + "settings.updates.row.startup.title": "Kontrollige käivitamisel värskendusi", + "settings.updates.row.startup.description": "Kontrollige automaatselt värskendusi, kui OpenCode käivitub", + "settings.updates.row.check.title": "Kontrollige värskendusi", + "settings.updates.row.check.description": "Kontrollige värskendusi käsitsi ja installige, kui need on saadaval", + "settings.updates.action.checkNow": "Kontrollige kohe", + "settings.updates.action.checking": "Kontrollimine...", + "settings.updates.action.downloading": "Allalaadimine...", + "settings.updates.action.installing": "Installimine...", + "settings.updates.toast.latest.title": "Oled kursis", + "settings.updates.toast.latest.description": "Kasutate rakenduse OpenCode uusimat versiooni.", + "sound.option.none": "Mitte ühtegi", + "sound.option.alert01": "Hoiatus 01", + "sound.option.alert02": "Hoiatus 02", + "sound.option.alert03": "Hoiatus 03", + "sound.option.alert04": "Hoiatus 04", + "sound.option.alert05": "Hoiatus 05", + "sound.option.alert06": "Hoiatus 06", + "sound.option.alert07": "Hoiatus 07", + "sound.option.alert08": "Hoiatus 08", + "sound.option.alert09": "Hoiatus 09", + "sound.option.alert10": "Hoiatus 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "ei 01", + "sound.option.nope02": "ei 02", + "sound.option.nope03": "ei 03", + "sound.option.nope04": "ei 04", + "sound.option.nope05": "ei 05", + "sound.option.nope06": "ei 06", + "sound.option.nope07": "ei 07", + "sound.option.nope08": "ei 08", + "sound.option.nope09": "ei 09", + "sound.option.nope10": "Ei 10", + "sound.option.nope11": "Ei 11", + "sound.option.nope12": "Ei 12", + "sound.option.yup01": "jah 01", + "sound.option.yup02": "jah 02", + "sound.option.yup03": "jah 03", + "sound.option.yup04": "jah 04", + "sound.option.yup05": "jah 05", + "sound.option.yup06": "jah 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": "Kuva süsteemi teatis, kui agent on valmis või vajab tähelepanu", + "settings.general.notifications.permissions.title": "load", + "settings.general.notifications.permissions.description": "Kuva süsteemi teatis, kui luba on vaja", + "settings.general.notifications.errors.title": "Vead", + "settings.general.notifications.errors.description": "Kuva tõrke korral süsteemi teatis", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Esitage heli, kui agent on valmis või vajab tähelepanu", + "settings.general.sounds.permissions.title": "load", + "settings.general.sounds.permissions.description": "Esitage heli, kui on vaja luba", + "settings.general.sounds.errors.title": "Vead", + "settings.general.sounds.errors.description": "Esita tõrke korral heli", + "settings.shortcuts.title": "Klaviatuuri otseteed", + "settings.shortcuts.reset.button": "Lähtestage vaikeseadetele", + "settings.shortcuts.reset.toast.title": "Otseteed lähtestatakse", + "settings.shortcuts.reset.toast.description": "Klaviatuuri otseteed on lähtestatud vaikeseadetele.", + "settings.shortcuts.conflict.title": "Otsetee on juba kasutusel", + "settings.shortcuts.conflict.description": "{{keybind}} on juba määratud kasutajale {{titles}}.", + "settings.shortcuts.unassigned": "Määramata", + "settings.shortcuts.pressKeys": "Vajutage klahve", + "settings.shortcuts.search.placeholder": "Otsige otseteid", + "settings.shortcuts.search.empty": "Otseteed ei leitud", + "settings.shortcuts.group.general": "Kindral", + "settings.shortcuts.group.session": "Seanss", + "settings.shortcuts.group.navigation": "Navigeerimine", + "settings.shortcuts.group.modelAndAgent": "Modell ja agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Viip", + "settings.providers.title": "Pakkujad", + "settings.providers.description": "Pakkuja seadeid saab siin konfigureerida.", + "settings.providers.section.connected": "Ühendatud pakkujad", + "settings.providers.connected.empty": "Ühendatud pakkujaid pole", + "settings.providers.connected.environmentDescription": "Ühendatud teie keskkonnamuutujatega", + "settings.providers.section.popular": "Populaarsed pakkujad", + "settings.providers.custom.description": "Lisage OpenAI-ga ühilduv pakkuja baasi URL alusel.", + "settings.providers.tag.environment": "Keskkond", + "settings.providers.tag.config": "Konfig", + "settings.providers.tag.custom": "Kohandatud", + "settings.providers.tag.other": "muud", + "settings.models.title": "Mudelid", + "settings.models.description": "Mudeli sätteid saab siin konfigureerida.", + "settings.agents.title": "Agendid", + "settings.agents.description": "Agendi seadeid saab siin konfigureerida.", + "settings.commands.title": "Käsud", + "settings.commands.description": "Käskude sätteid saab siin konfigureerida.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP seadeid saab siin konfigureerida.", + "settings.permissions.title": "load", + "settings.permissions.description": "Saate juhtida, milliseid tööriistu server vaikimisi kasutada saab.", + "settings.permissions.section.tools": "Tööriistad", + "settings.permissions.toast.updateFailed.title": "Lubade värskendamine ebaõnnestus", + "settings.permissions.action.allow": "Luba", + "settings.permissions.action.ask": "Küsi", + "settings.permissions.action.deny": "Keela", + "settings.permissions.tool.read.title": "Lugege", + "settings.permissions.tool.read.description": "Faili lugemine (vastab faili teele)", + "settings.permissions.tool.edit.title": "Muuda", + "settings.permissions.tool.edit.description": "Failide muutmine, sh muudatused, kirjutamised ja paigad", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Sobitage failid globaalsete mustrite abil", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Otsige faili sisu regulaaravaldiste abil", + "settings.permissions.tool.list.title": "Nimekiri", + "settings.permissions.tool.list.description": "Loetlege failid kataloogis", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Käivitage shellikäsud", + "settings.permissions.tool.task.title": "Ülesanne", + "settings.permissions.tool.task.description": "Käivitage allagendid", + "settings.permissions.tool.skill.title": "Oskus", + "settings.permissions.tool.skill.description": "Laadige oskus nime järgi", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Käivitage keeleserveri päringuid", + "settings.permissions.tool.todowrite.title": "Todo Kirjuta", + "settings.permissions.tool.todowrite.description": "Värskendage ülesannete loendit", + "settings.permissions.tool.webfetch.title": "Veebi toomine", + "settings.permissions.tool.webfetch.description": "Sisu toomine asukohast URL", + "settings.permissions.tool.websearch.title": "Veebiotsing", + "settings.permissions.tool.websearch.description": "Otsige veebist", + "settings.permissions.tool.external_directory.title": "Väline kataloog", + "settings.permissions.tool.external_directory.description": "Juurdepääs failidele väljaspool projekti kataloogi", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Tuvastab korduvad tööriistakutsed identse sisendiga", + "session.delete.failed.title": "Seansi kustutamine ebaõnnestus", + "session.delete.title": "Kustuta seanss", + "session.delete.confirm": 'Kas kustutada seanss "{{name}}"?', + "session.delete.button": "Kustuta seanss", + "workspace.new": "Uus tööruum", + "workspace.type.local": "kohalik", + "workspace.type.sandbox": "liivakast", + "workspace.create.failed.title": "Tööruumi loomine ebaõnnestus", + "workspace.delete.failed.title": "Tööruumi kustutamine ebaõnnestus", + "workspace.resetting.title": "Tööruumi lähtestamine", + "workspace.resetting.description": "See võib võtta minuti.", + "workspace.reset.failed.title": "Tööruumi lähtestamine ebaõnnestus", + "workspace.reset.success.title": "Tööruumi lähtestamine", + "workspace.reset.success.description": "Tööala vastab nüüd vaikeharule.", + "workspace.error.stillPreparing": "Tööala on veel ettevalmistamisel", + "workspace.status.checking": "Ühendamata muudatuste otsimine...", + "workspace.status.error": "git-olekut ei saa kinnitada.", + "workspace.status.clean": "Ühtegi ühendamata muudatust ei tuvastatud.", + "workspace.status.dirty": "Selles tööruumis tuvastati ühendamata muudatused.", + "workspace.delete.title": "Kustuta tööruum", + "workspace.delete.confirm": 'Kas kustutada tööruum "{{name}}"?', + "workspace.delete.button": "Kustuta tööruum", + "workspace.reset.title": "Lähtestage tööala", + "workspace.reset.confirm": 'Kas lähtestada tööruum "{{name}}"?', + "workspace.reset.button": "Lähtestage tööala", + "workspace.reset.archived.none": "Ühtegi aktiivset seanssi ei arhiveerita.", + "workspace.reset.archived.one": "1 seanss arhiveeritakse.", + "workspace.reset.archived.many": "{{count}} seanssi arhiivitakse.", + "workspace.reset.note": "See lähtestab tööruumi, et see vastaks vaikeharule.", + "dialog.usageExceeded.dontShowAgain": "Ära kuva enam", +} diff --git a/packages/app/src/i18n/fi.ts b/packages/app/src/i18n/fi.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa9c92574cffa690f95b11875260d8d774c4bbe5 --- /dev/null +++ b/packages/app/src/i18n/fi.ts @@ -0,0 +1,1168 @@ +export const dict = { + "command.category.suggested": "Ehdotetut", + "command.category.view": "Näytä", + "command.category.project": "Projekti", + "command.category.provider": "Palveluntarjoaja", + "command.category.server": "Palvelin", + "command.category.session": "Istunto", + "command.category.theme": "Teema", + "command.category.language": "Kieli", + "command.category.file": "Tiedosto", + "command.category.context": "Konteksti", + "command.category.terminal": "Terminaali", + "command.category.model": "Malli", + "command.category.mcp": "MCP", + "command.category.agent": "Agentti", + "command.category.permissions": "Käyttöoikeudet", + "command.category.workspace": "Työtila", + "command.category.settings": "Asetukset", + "theme.scheme.system": "Järjestelmän mukaan", + "theme.scheme.light": "Vaalea", + "theme.scheme.dark": "Tumma", + "command.sidebar.toggle": "Näytä tai piilota sivupalkki", + "command.project.open": "Avaa projekti", + "command.project.previous": "Edellinen projekti", + "command.project.next": "Seuraava projekti", + "command.project.index": "Vaihda projektiin {{index}}", + "command.provider.connect": "Yhdistä palveluntarjoaja", + "command.server.switch": "Vaihda palvelinta", + "command.settings.open": "Avaa asetukset", + "command.session.previous": "Edellinen istunto", + "command.session.next": "Seuraava istunto", + "command.session.previous.unseen": "Edellinen lukematon istunto", + "command.session.next.unseen": "Seuraava lukematon istunto", + "command.session.archive": "Arkistoi istunto", + "command.palette": "Komentopaletti", + "command.theme.cycle": "Vaihda teemaa", + "command.theme.set": "Käytä teemaa: {{theme}}", + "command.theme.scheme.cycle": "Vaihda värimallia", + "command.theme.scheme.set": "Käytä värimallia: {{scheme}}", + "command.language.cycle": "Vaihda kieltä", + "command.language.set": "Käytä kieltä: {{language}}", + "command.session.new": "Uusi istunto", + "command.file.open": "Avaa tiedosto", + "command.tab.close": "Sulje välilehti", + "command.tab.reopenClosed": "Avaa suljettu välilehti uudelleen", + "command.context.addSelection": "Lisää valinta kontekstiin", + "command.context.addSelection.description": "Lisää valitut rivit nykyisestä tiedostosta", + "command.input.focus": "Kohdista syötekenttään", + "command.terminal.toggle": "Näytä tai piilota terminaali", + "command.fileTree.toggle": "Näytä tai piilota tiedostopuu", + "command.review.toggle": "Näytä tai piilota tarkistus", + "command.terminal.new": "Uusi terminaali", + "command.terminal.new.description": "Luo uusi terminaalivälilehti", + "command.steps.toggle": "Näytä tai piilota vaiheet", + "command.steps.toggle.description": "Näytä tai piilota nykyisen viestin vaiheet", + "command.message.previous": "Edellinen viesti", + "command.message.previous.description": "Siirry edelliseen käyttäjän viestiin", + "command.message.next": "Seuraava viesti", + "command.message.next.description": "Siirry seuraavaan käyttäjäviestiin", + "command.model.choose": "Valitse malli", + "command.model.choose.description": "Valitse eri malli", + "command.mcp.toggle": "Ota MCP:t käyttöön tai poista käytöstä", + "command.mcp.toggle.description": "Ota MCP:t käyttöön tai poista käytöstä", + "command.agent.cycle": "Vaihda agenttia", + "command.agent.cycle.description": "Vaihda seuraavaan agenttiin", + "command.agent.cycle.reverse": "Vaihda agenttia taaksepäin", + "command.agent.cycle.reverse.description": "Vaihda edelliseen agenttiin", + "command.model.variant.cycle": "Vaihda päättelyn tasoa", + "command.model.variant.cycle.description": "Vaihda seuraavalle päättelyn tasolle", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Kehote", + "command.permissions.autoaccept.enable": "Hyväksy käyttöoikeudet automaattisesti", + "command.permissions.autoaccept.disable": "Lopeta käyttöoikeuksien automaattinen hyväksyminen", + "command.workspace.toggle": "Vaihda työtiloja", + "command.workspace.toggle.description": "Ota käyttöön tai poista käytöstä useita työtiloja sivupalkissa", + "command.session.undo": "Kumoa", + "command.session.undo.description": "Kumoa viimeinen viesti", + "command.session.redo": "Toista", + "command.session.redo.description": "Toista viimeisin kumottu viesti", + "command.session.compact": "Tiivistä istunto", + "command.session.compact.description": "Tee yhteenveto istunnosta pienentääksesi kontekstin kokoa", + "command.session.fork": "Haarauta viestistä", + "command.session.fork.description": "Luo uusi istunto edellisestä viestistä", + "command.session.share": "Jaa istunto", + "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", + "palette.group.commands": "Komennot", + "palette.group.files": "Tiedostot", + "dialog.provider.search.placeholder": "Etsi palveluntarjoajia", + "dialog.provider.empty": "Palveluntarjoajia ei löytynyt", + "dialog.provider.group.popular": "Suositut", + "dialog.provider.group.other": "Muut", + "dialog.provider.custom.label": "Mukautettu OpenAI-yhteensopiva palveluntarjoaja", + "dialog.provider.tag.recommended": "Suositeltu", + "dialog.provider.opencode.note": "Kuratoituja malleja, kuten Claude, GPT, Gemini ja muita", + "dialog.provider.opencode.tagline": "Luotettavat optimoidut mallit", + "dialog.provider.opencodeGo.tagline": "Edullinen tilaus kaikille", + "dialog.provider.anthropic.note": "Suora pääsy Claude-malleihin, mukaan lukien Pro ja Max", + "dialog.provider.copilot.note": "AI-malleja koodausapuun GitHub Copilotin kautta", + "dialog.provider.openai.note": "GPT-mallit nopeisiin, suorituskykyisiin yleisiin tekoälytehtäviin", + "dialog.provider.google.note": "Gemini-mallit nopeisiin, jäsenneltyihin vastauksiin", + "dialog.provider.openrouter.note": "Käytä kaikkia tuettuja malleja yhdeltä palveluntarjoajalta", + "dialog.provider.vercel.note": "Yhdistetty pääsy AI-malleihin älykkäällä reitityksellä", + "dialog.model.select.title": "Valitse malli", + "dialog.model.search.placeholder": "Hae malleja", + "dialog.model.empty": "Ei mallituloksia", + "dialog.model.manage": "Hallitse malleja", + "dialog.model.manage.description": "Mukauta, mitkä mallit näkyvät mallivalitsimessa.", + "dialog.model.manage.provider.toggle": "Ota kaikki palveluntarjoajan {{provider}} mallit käyttöön tai pois käytöstä", + "dialog.model.unpaid.freeModels.title": "OpenCoden tarjoamat ilmaiset mallit", + "dialog.model.unpaid.addMore.title": "Lisää malleja suosituilta palveluntarjoajilta", + "dialog.model.unpaid.viewMoreProviders": "Katso yli 70 muuta palveluntarjoajaa", + "dialog.provider.viewAll": "Näytä lisää palveluntarjoajia", + "provider.connect.title": "Yhdistä {{provider}}", + "provider.connect.title.anthropicProMax": "Kirjaudu sisään Claude Pro/Maxilla", + "provider.connect.selectMethod": "Valitse kirjautumistapa palvelulle {{provider}}.", + "provider.connect.method.apiKey": "API-avain", + "provider.connect.method.browser": "Selain", + "provider.connect.method.headless": "Päätön", + "provider.connect.status.inProgress": "Valtuutus käynnissä...", + "provider.connect.status.waiting": "Odotetaan valtuutusta...", + "provider.connect.status.failed": "Valtuutus epäonnistui: {{error}}", + "provider.connect.apiKey.description": + "Anna {{provider}}-API-avaimesi yhdistääksesi tilisi ja käyttääksesi {{provider}}-malleja OpenCodessa.", + "provider.connect.apiKey.label": "{{provider}}-API-avain", + "provider.connect.apiKey.placeholder": "API-avain", + "provider.connect.apiKey.required": "API-avain vaaditaan", + "provider.connect.opencodeZen.line1": + "OpenCode Zen antaa sinulle pääsyn kuratoituun joukkoon luotettavia optimoituja malleja koodausagenteille.", + "provider.connect.opencodeZen.line2": + "Yhdellä API-avaimella pääset käyttämään malleja, kuten Claude, GPT, Gemini, GLM ja paljon muuta.", + "provider.connect.opencodeZen.visit.prefix": "Avaa ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " ja hae API-avaimesi.", + "provider.connect.oauth.code.visit.prefix": "Avaa ", + "provider.connect.oauth.code.visit.link": "tämä linkki", + "provider.connect.oauth.code.visit.suffix": + " ja hae valtuutuskoodi, jolla voit yhdistää tilisi ja käyttää {{provider}}-malleja OpenCodessa.", + "provider.connect.oauth.code.label": "Palvelun {{method}} valtuutuskoodi", + "provider.connect.oauth.code.placeholder": "Valtuutuskoodi", + "provider.connect.oauth.code.required": "Valtuutuskoodi vaaditaan", + "provider.connect.oauth.code.invalid": "Virheellinen valtuutuskoodi", + "provider.connect.oauth.auto.visit.prefix": "Avaa ", + "provider.connect.oauth.auto.visit.link": "tämä linkki", + "provider.connect.oauth.auto.visit.suffix": + " ja anna alla oleva koodi yhdistääksesi tilisi ja käyttääksesi {{provider}}-malleja OpenCodessa.", + "provider.connect.oauth.auto.confirmationCode": "Vahvistuskoodi", + "provider.connect.toast.connected.title": "{{provider}} yhdistetty", + "provider.connect.toast.connected.description": "{{provider}}-mallit ovat nyt käytettävissä.", + "provider.custom.title": "Mukautettu palveluntarjoaja", + "provider.custom.unavailable": "Mukautetut palveluntarjoajat eivät ole käytettävissä tällä palvelimella", + "provider.custom.description.prefix": "Määritä OpenAI-yhteensopiva palveluntarjoaja. Katso ", + "provider.custom.description.link": "palveluntarjoajan asetusdokumentit", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Palveluntarjoajan tunnus", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Pienet kirjaimet, numerot, yhdysmerkit tai alaviivat", + "provider.custom.field.name.label": "Näyttönimi", + "provider.custom.field.name.placeholder": "Oma tekoälypalveluni", + "provider.custom.field.baseURL.label": "Perus-URL-osoite", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API-avain", + "provider.custom.field.apiKey.placeholder": "API-avain", + "provider.custom.field.apiKey.description": "Valinnainen. Jätä tyhjäksi, jos hallitset todennusta otsikoiden avulla.", + "provider.custom.models.label": "Mallit", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Nimi", + "provider.custom.models.name.placeholder": "Näyttönimi", + "provider.custom.models.remove": "Poista malli", + "provider.custom.models.add": "Lisää malli", + "provider.custom.headers.label": "Otsikot (valinnainen)", + "provider.custom.headers.key.label": "Otsikko", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Arvo", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "Poista otsikko", + "provider.custom.headers.add": "Lisää otsikko", + "provider.custom.error.providerID.required": "Palveluntarjoajan tunnus vaaditaan", + "provider.custom.error.providerID.format": "Käytä pieniä kirjaimia, numeroita, tavuviivoja tai alaviivoja", + "provider.custom.error.providerID.exists": "Palveluntarjoajan tunnus on jo olemassa", + "provider.custom.error.name.required": "Näyttönimi vaaditaan", + "provider.custom.error.baseURL.required": "Perus-URL-osoite vaaditaan", + "provider.custom.error.baseURL.format": "Alussa on oltava http:// tai https://", + "provider.custom.error.required": "Pakollinen", + "provider.custom.error.duplicate": "Kaksoiskappale", + "provider.disconnect.toast.disconnected.title": "Yhteys palveluntarjoajaan {{provider}} katkaistu", + "provider.disconnect.toast.disconnected.description": "{{provider}}-mallit eivät ole enää saatavilla.", + "model.tag.free": "Ilmainen", + "model.tag.latest": "Uusin", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "teksti", + "model.input.image": "kuva", + "model.input.audio": "ääni", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Sallitut syötteet: {{inputs}}", + "model.tooltip.context.label": "Konteksti", + "model.tooltip.inputs": "Syötteet", + "model.tooltip.model": "Malli", + "model.tooltip.provider": "Palveluntarjoaja", + "model.tooltip.reasoning": "Päättely", + "model.tooltip.reasoning.allowed": "Mahdollistaa päättelyn", + "model.tooltip.reasoning.none": "Ei päättelyä", + "model.tooltip.context": "Kontekstirajoitus {{limit}}", + "common.search.placeholder": "Etsi", + "common.goBack": "Siirry takaisin", + "common.goForward": "Siirry eteenpäin", + "common.loading": "Ladataan", + "common.loading.ellipsis": "...", + "common.cancel": "Peruuta", + "common.clear": "Tyhjennä", + "common.open": "Avaa", + "common.connect": "Yhdistä", + "common.disconnect": "Katkaise yhteys", + "common.continue": "Jatka", + "common.submit": "Lähetä", + "common.save": "Tallenna", + "common.saving": "Tallennetaan...", + "common.default": "Oletus", + "common.attachment": "liite", + "prompt.placeholder.shell": "Anna shell-komento... {{example}}", + "prompt.placeholder.normal": 'Kysy mitä tahansa... "{{example}}"', + "prompt.placeholder.simple": "Kysy mitä tahansa...", + "prompt.placeholder.summarizeComments": "Tiivistä kommentit…", + "prompt.placeholder.summarizeComment": "Tiivistä kommentti…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Kehote", + "prompt.mode.shell.exit": "poistu painamalla Esc", + "session.child.promptDisabled": "Aliagentti-istuntoihin ei voi lähettää kehotteita.", + "session.child.backToParent": "Takaisin pääistuntoon.", + "prompt.example.1": "Korjaa TODO koodikannassa", + "prompt.example.2": "Mikä on tämän projektin teknologiapino?", + "prompt.example.3": "Korjaa rikkinäiset testit", + "prompt.example.4": "Selitä, kuinka todennus toimii", + "prompt.example.5": "Etsi ja korjaa tietoturva-aukkoja", + "prompt.example.6": "Lisää yksikkötestejä käyttäjäpalveluun", + "prompt.example.7": "Refaktoroi tämä funktio luettavammaksi", + "prompt.example.8": "Mitä tämä virhe tarkoittaa?", + "prompt.example.9": "Auta minua selvittämään tämä ongelma", + "prompt.example.10": "Luo API-dokumentaatio", + "prompt.example.11": "Optimoi tietokantakyselyt", + "prompt.example.12": "Lisää syötteen validointi", + "prompt.example.13": "Luo uusi komponentti...", + "prompt.example.14": "Kuinka otan tämän projektin käyttöön?", + "prompt.example.15": "Tarkista koodini parhaat käytännöt", + "prompt.example.16": "Lisää tähän toimintoon virheenkäsittely", + "prompt.example.17": "Selitä tämä säännöllinen lauseke", + "prompt.example.18": "Muunna tämä TypeScriptiksi", + "prompt.example.19": "Lisää kirjaus koko koodikantaan", + "prompt.example.20": "Mitkä riippuvuudet ovat vanhentuneita?", + "prompt.example.21": "Auta minua kirjoittamaan migraatioskripti", + "prompt.example.22": "Ota välimuisti käyttöön tälle päätepisteelle", + "prompt.example.23": "Lisää sivutus tähän luetteloon", + "prompt.example.24": "Luo CLI-komento...", + "prompt.example.25": "Miten ympäristömuuttujat toimivat tässä?", + "prompt.popover.emptyResults": "Ei vastaavia tuloksia", + "prompt.popover.emptyCommands": "Ei vastaavia komentoja", + "prompt.dropzone.label": "Pudota kuvia, PDF-tiedostoja tai tekstitiedostoja tähän", + "prompt.dropzone.file.label": "Pudota lisätäksesi tiedoston @-mainintana", + "prompt.slash.badge.custom": "mukautettu", + "prompt.slash.badge.skill": "taito", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktiivinen", + "prompt.context.includeActiveFile": "Sisällytä aktiivinen tiedosto", + "prompt.context.removeActiveFile": "Poista aktiivinen tiedosto kontekstista", + "prompt.context.removeFile": "Poista tiedosto kontekstista", + "prompt.action.attachFile": "Lisää tiedostoja", + "prompt.menu.addImagesAndFiles": "Lisää tiedostoja ja paljon muuta", + "prompt.menu.imagesAndFiles": "Kuvia ja tiedostoja", + "prompt.menu.commands": "Komennot", + "prompt.menu.context": "Konteksti", + "prompt.menu.shellCommand": "Shell-komento", + "prompt.attachment.remove": "Poista liite", + "prompt.action.send": "Lähetä", + "prompt.action.stop": "Pysäytä", + "prompt.toast.pasteUnsupported.title": "Liitettä ei tueta", + "prompt.toast.pasteUnsupported.description": "Vain kuvia, PDF-tiedostoja tai tekstitiedostoja voi liittää tähän.", + "prompt.toast.attachmentDuplicate.title": "Tämä tiedosto on jo ladattu", + "prompt.toast.modelAgentRequired.title": "Valitse agentti ja malli", + "prompt.toast.modelAgentRequired.description": "Valitse agentti ja malli ennen kehotteen lähettämistä.", + "prompt.toast.worktreeCreateFailed.title": "Työpuun luominen epäonnistui", + "prompt.toast.sessionCreateFailed.title": "Istunnon luominen epäonnistui", + "prompt.toast.shellSendFailed.title": "Shell-komennon lähettäminen epäonnistui", + "prompt.toast.commandSendFailed.title": "Komennon lähettäminen epäonnistui", + "prompt.toast.promptSendFailed.title": "Kehotteen lähettäminen epäonnistui", + "prompt.toast.promptSendFailed.description": "Istuntoa ei voi noutaa", + "dialog.mcp.title": "MCP:t", + "dialog.mcp.description": "{{enabled}}/{{total}} käytössä", + "dialog.mcp.empty": "MCP:tä ei ole määritetty", + "dialog.lsp.empty": "LSP:t tunnistetaan automaattisesti tiedostotyypeistä", + "dialog.plugins.empty": "Laajennukset määritetään opencode.json-tiedostossa", + "mcp.status.connected": "yhdistetty", + "mcp.status.failed": "epäonnistunut", + "mcp.status.needs_auth": "tarvitsee todennusta", + "mcp.status.disabled": "pois käytöstä", + "mcp.auth.clickToAuthenticate": "Todenna napsauttamalla", + "dialog.fork.empty": "Ei viestejä, joista voisi haarauttaa", + "dialog.directory.search.placeholder": "Hae kansioita", + "dialog.directory.empty": "Kansioita ei löytynyt", + "dialog.directory.action.selectFile": "Valitse tiedosto", + "dialog.directory.action.selectFolder": "Valitse kansio", + "dialog.directory.root": "Juuri", + "dialog.directory.parent": "Ylätaso", + "dialog.directory.readError": "Tätä kansiota ei voi lukea", + "app.server.unreachable": "Palvelimeen {{server}} ei saatu yhteyttä", + "app.server.retrying": "Yritetään automaattisesti uudelleen...", + "app.server.otherServers": "Muut palvelimet", + "dialog.server.title": "Palvelimet", + "dialog.server.description": "Vaihda OpenCode-palvelimeen, johon tämä sovellus muodostaa yhteyden.", + "dialog.server.search.placeholder": "Etsi palvelimia", + "dialog.server.empty": "Ei vielä palvelimia", + "dialog.server.add.title": "Lisää palvelin", + "dialog.server.add.url": "Palvelimen osoite", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Ei voitu muodostaa yhteyttä palvelimeen", + "dialog.server.add.checking": "Tarkistetaan...", + "dialog.server.add.button": "Lisää palvelin", + "dialog.server.add.name": "Palvelimen nimi (valinnainen)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Käyttäjätunnus (valinnainen)", + "dialog.server.add.usernamePlaceholder": "käyttäjätunnus", + "dialog.server.add.password": "Salasana (valinnainen)", + "dialog.server.add.passwordPlaceholder": "salasana", + "dialog.server.edit.title": "Muokkaa palvelinta", + "dialog.server.default.title": "Oletuspalvelin", + "dialog.server.default.description": + "Yhdistä tähän palvelimeen sovelluksen käynnistyksen yhteydessä paikallisen palvelimen käynnistämisen sijaan. Vaatii uudelleenkäynnistyksen.", + "dialog.server.default.none": "Palvelinta ei ole valittu", + "dialog.server.default.set": "Aseta nykyinen palvelin oletukseksi", + "dialog.server.default.clear": "Tyhjennä", + "dialog.server.action.remove": "Poista palvelin", + "dialog.server.menu.edit": "Muokkaa", + "dialog.server.menu.default": "Aseta oletukseksi", + "dialog.server.menu.defaultRemove": "Poista oletuspalvelin", + "dialog.server.menu.delete": "Poista", + "dialog.server.current": "Nykyinen palvelin", + "dialog.server.status.default": "Oletus", + "wsl.server.add": "Lisää WSL-palvelin", + "wsl.server.addShort": "Lisää WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL-palvelin", + "wsl.server.retryStart": "Yritä käynnistystä uudelleen", + "wsl.server.updating": "Päivitetään...", + "wsl.onboarding.step.distro": "Valitse jakelu", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Tarkistetaan WSL...", + "wsl.onboarding.restartRequired": "Windows tarvitsee uudelleenkäynnistyksen WSL:n asennuksen viimeistelemiseksi.", + "wsl.onboarding.ready": "WSL on valmis.", + "wsl.onboarding.required": "WSL vaaditaan jatkamiseen.", + "wsl.onboarding.checkingDistros": "Tarkistetaan jakeluja...", + "wsl.onboarding.installingDistro": "Asennetaan {{distro}}...", + "wsl.onboarding.checkingDistro": "Tarkistetaan {{distro}}...", + "wsl.onboarding.listingDistros": "Luetteloidaan jakeluja...", + "wsl.onboarding.distroReady": "{{distro}} on valmis.", + "wsl.onboarding.distroNotInstalled": "{{distro}} ei ole vielä asennettu.", + "wsl.onboarding.openDistroOnce": "Avaa {{distro}} kerran viimeistelläksesi asetukset.", + "wsl.onboarding.finishingDistro": "Viimeistellään jakelun {{distro}} asennusta.", + "wsl.onboarding.pickDistro": "Valitse jakelu tai asenna sellainen alta.", + "wsl.onboarding.checkingOpencode": "Tarkistetaan OpenCodea...", + "wsl.onboarding.checkingOpencodeIn": "Tarkistetaan OpenCodea kohteessa {{distro}}...", + "wsl.onboarding.updatingOpencode": "Päivitetään OpenCodea...", + "wsl.onboarding.updatingOpencodeIn": "Päivitetään OpenCodea kohteessa {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Päivitä OpenCode jakelussa {{distro}}.", + "wsl.onboarding.updateOpencode": "Päivitä OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode on valmis jakelussa {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode on valmis.", + "wsl.onboarding.installOpencodeIn": "Asenna OpenCode jakeluun {{distro}}.", + "wsl.onboarding.installOpencode": "Asenna OpenCode", + "wsl.onboarding.chooseDistroFirst": "Valitse ensin jakelu.", + "wsl.onboarding.loadFailed": "WSL-tilan lataaminen epäonnistui.", + "wsl.onboarding.loading": "Ladataan...", + "wsl.onboarding.installedDistros": "Asennetut jakelut", + "wsl.onboarding.checkAgain": "Tarkista uudelleen", + "wsl.onboarding.distroStatus.ready": "Valmis", + "wsl.onboarding.distroStatus.checking": "Tarkistetaan...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCodea ei ole asennettu", + "wsl.onboarding.distroStatus.missingTools": "Puuttuu bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Ei tuettu · Käytä WSL 2:ta", + "wsl.onboarding.needAnotherDistro": "Tarvitsetko toisen jakelun?", + "wsl.onboarding.needAnotherDistroHint": "Asenna Linux-jakelu WSL-luettelosta", + "wsl.onboarding.wslNotInstalled.title": "WSL:ää ei ole asennettu", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) vaaditaan ennen kuin OpenCode voi lisätä WSL-palvelimen", + "wsl.onboarding.wslUnavailable.title": "WSL ei ole käytettävissä", + "wsl.onboarding.wslUnavailable.description": "OpenCode ei voinut vahvistaa WSL:ää tällä koneella.", + "wsl.onboarding.installWsl": "Asenna WSL", + "wsl.onboarding.windowsRestartRequired": + "Viimeistele WSL:n asennus käynnistämällä Windows uudelleen ja avaa sitten OpenCode uudelleen.", + "wsl.onboarding.next": "Seuraava", + "wsl.onboarding.refresh": "Päivitä", + "wsl.onboarding.allDistrosAdded": "Kaikki asennetut jakelut on jo lisätty.", + "wsl.onboarding.noDistros": "Jakeluja ei ole vielä havaittu.", + "wsl.onboarding.install": "Asenna", + "wsl.onboarding.installing": "Asennetaan...", + "wsl.onboarding.installDistro": "Asenna jakelu", + "wsl.onboarding.searchDistros": "Etsi jakeluja", + "wsl.onboarding.wsl2Required": "WSL 2 vaaditaan.", + "wsl.onboarding.toolsRequired": "Tämä jakelu tarvitsee bashin ja curlin.", + "wsl.onboarding.openTerminal": "Avaa terminaali", + "wsl.onboarding.path": "Polku: {{path}}", + "wsl.onboarding.notFound": "ei löytynyt", + "wsl.onboarding.version": "Versio: {{version}}", + "wsl.onboarding.unknown": "tuntematon", + "wsl.onboarding.desktopVersion": "Desktop {{version}}", + "wsl.onboarding.versionMismatch": "Asennettu versio ei vastaa työpöytäsovelluksen versiota.", + "wsl.onboarding.adding": "Lisätään...", + "server.row.noUsername": "ei käyttäjätunnusta", + "dialog.project.edit.title": "Muokkaa projektia", + "dialog.project.edit.name": "Nimi", + "dialog.project.edit.icon": "Kuvake", + "dialog.project.edit.icon.alt": "Projektikuvake", + "dialog.project.edit.icon.hint": "Napsauta tai vedä kuvaa", + "dialog.project.edit.icon.recommended": "Suositus: 128x128px", + "dialog.project.edit.color": "Väri", + "dialog.project.edit.color.select": "Valitse väri {{color}}", + "dialog.project.edit.worktree.startup": "Työtilan käynnistysskripti", + "dialog.project.edit.worktree.startup.description": "Suoritetaan uuden työtilan (työpuun) luomisen jälkeen.", + "dialog.project.edit.worktree.startup.placeholder": "esim. bun install", + "dialog.releaseNotes.action.getStarted": "Aloita", + "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.', + "context.breakdown.system": "Järjestelmä", + "context.breakdown.user": "Käyttäjä", + "context.breakdown.assistant": "Avustaja", + "context.breakdown.tool": "Työkalukutsut", + "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", + "context.stats.model": "Malli", + "context.stats.limit": "Kontekstirajoitus", + "context.stats.totalTokens": "Tokenit yhteensä", + "context.stats.usage": "Käyttö", + "context.stats.inputTokens": "Syötetokenit", + "context.stats.outputTokens": "Tulostokenit", + "context.stats.reasoningTokens": "Päättelytokenit", + "context.stats.cacheTokens": "Välimuistitokenit (luku/kirjoitus)", + "context.stats.userMessages": "Käyttäjäviestit", + "context.stats.assistantMessages": "Avustajan viestit", + "context.stats.totalCost": "Kokonaiskustannukset", + "context.stats.sessionCreated": "Istunto luotu", + "context.stats.lastActivity": "Viimeisin toiminta", + "context.usage.tokens": "Tokenit", + "context.usage.usage": "Käyttö", + "context.usage.cost": "Kustannukset", + "context.usage.clickToView": "Näytä konteksti napsauttamalla", + "context.usage.view": "Näytä kontekstin käyttö", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Kieli", + "toast.language.description": "Kieleksi vaihdettu {{language}}", + "toast.theme.title": "Teema vaihdettu", + "toast.scheme.title": "Värimalli", + "toast.workspace.enabled.title": "Työtilat käytössä", + "toast.workspace.enabled.description": "Sivupalkissa näkyy nyt useita työpuita", + "toast.workspace.disabled.title": "Työtilat pois käytöstä", + "toast.workspace.disabled.description": "Vain päätyöpuu näkyy sivupalkissa", + "toast.permissions.autoaccept.on.title": "Käyttöoikeudet hyväksytään automaattisesti", + "toast.permissions.autoaccept.on.description": "Lupapyynnöt hyväksytään automaattisesti", + "toast.permissions.autoaccept.off.title": "Lupien automaattinen hyväksyminen lopetettu", + "toast.permissions.autoaccept.off.description": "Lupapyynnöt vaativat hyväksynnän", + "toast.model.none.title": "Mallia ei ole valittu", + "toast.model.none.description": "Yhdistä palveluntarjoaja tehdäksesi yhteenvedon tästä istunnosta", + "toast.file.loadFailed.title": "Tiedoston lataaminen epäonnistui", + "toast.file.listFailed.title": "Tiedostojen luettelointi epäonnistui", + "toast.context.noLineSelection.title": "Ei rivivalintaa", + "toast.context.noLineSelection.description": "Valitse ensin riviväli tiedostovälilehdeltä.", + "toast.session.share.copyFailed.title": "URL-osoitteen kopioiminen leikepöydälle epäonnistui", + "toast.session.share.success.title": "Istunto jaettu", + "toast.session.share.success.description": "Jakolinkki kopioitu leikepöydälle!", + "toast.session.share.failed.title": "Istunnon jakaminen epäonnistui", + "toast.session.share.failed.description": "Istunnon jakamisessa tapahtui virhe", + "toast.session.unshare.success.title": "Istunnon jako peruttu", + "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", + "toast.update.description": "OpenCoden uusi versio ({{version}}) on nyt saatavana asennettavaksi.", + "toast.update.action.installRestart": "Asenna ja käynnistä uudelleen", + "toast.update.action.notYet": "Ei vielä", + "error.page.title": "Jotain meni pieleen", + "error.page.description": "Sovellusta ladattaessa tapahtui virhe.", + "error.page.description.localServerStartup": "Paikallista palvelinta käynnistettäessä tapahtui virhe.", + "error.page.details.label": "Virheen tiedot", + "error.page.action.restart": "Käynnistä uudelleen", + "error.page.action.report": "Ilmoita virheestä", + "error.page.action.reported": "Virhe raportoitu", + "error.page.action.exportLogs": "Vie lokit", + "error.page.action.checking": "Tarkistetaan...", + "error.page.action.checkUpdates": "Tarkista päivitykset", + "error.page.action.updateTo": "Päivitä versioon {{version}}", + "error.page.circular": "[Kehäviittaus]", + "error.page.report.prefix": "Ilmoita tästä virheestä OpenCode-tiimille", + "error.page.report.discord": "Discordissa", + "error.page.version": "Versio: {{version}}", + "error.dev.rootNotFound": + "Juurielementtiä ei löydy. Unohditko lisätä sen index.html-tiedostoosi? Tai ehkä id-attribuutti on kirjoitettu väärin?", + "error.serverSync.connectFailed": + "Ei voitu muodostaa yhteyttä palvelimeen. Onko osoitteessa `{{url}}` käynnissä palvelin?", + "error.serverSDK.noServerAvailable": "Palvelinta ei ole saatavilla", + "error.serverSDK.serverNotAvailable": "Palvelin ei ole käytettävissä", + "error.childStore.persistedCacheCreateFailed": "Pysyvän välimuistin luominen epäonnistui", + "error.childStore.persistedProjectMetadataCreateFailed": "Projektin pysyvien metatietojen luominen epäonnistui", + "error.childStore.persistedProjectIconCreateFailed": "Pysyvän projektin kuvakkeen luominen epäonnistui", + "error.childStore.storeCreateFailed": "Tietovaraston luominen epäonnistui", + "directory.error.invalidUrl": "Virheellinen hakemisto URL-osoitteessa.", + "error.chain.unknown": "Tuntematon virhe", + "error.server.invalidConfiguration": "Virheellinen määritys", + "error.chain.causedBy": "Aiheuttaja:", + "error.chain.apiError": "API-virhe", + "error.chain.status": "Tila: {{status}}", + "error.chain.retryable": "Voidaan yrittää uudelleen: {{retryable}}", + "error.chain.responseBody": "Vastausteksti:\n{{body}}", + "error.chain.didYouMean": "Tarkoititko: {{suggestions}}", + "error.chain.modelNotFound": "Mallia ei löydy: {{provider}}/{{model}}", + "error.chain.checkConfig": "Tarkista määritysten (opencode.json) palveluntarjoajien ja mallien nimet", + "error.chain.mcpFailed": 'MCP-palvelin "{{name}}" epäonnistui. Huomaa, että OpenCode ei vielä tue MCP-todennusta.', + "error.chain.providerAuthFailed": "Palveluntarjoajan todennus epäonnistui ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Palveluntarjoajan "{{provider}}" alustaminen epäonnistui. Tarkista tunnistetiedot ja määritykset.', + "error.chain.configJsonInvalid": "Polun {{path}} määritystiedosto ei ole kelvollista JSON(C)-muotoa", + "error.chain.configJsonInvalidWithMessage": + "Polun {{path}} määritystiedosto ei ole kelvollista JSON(C)-muotoa: {{message}}", + "error.chain.configDirectoryTypo": + 'Hakemisto "{{dir}}" polussa {{path}} ei kelpaa. Nimeä hakemisto uudelleen nimellä "{{suggestion}}" tai poista se. Tämä on yleinen kirjoitusvirhe.', + "error.chain.configFrontmatterError": "Polun {{path}} front matter -tietojen jäsentäminen epäonnistui:\n{{message}}", + "error.chain.configInvalid": "Polun {{path}} määritystiedosto on virheellinen", + "error.chain.configInvalidWithMessage": "Polun {{path}} määritystiedosto on virheellinen: {{message}}", + "notification.permission.title": "Lupa vaaditaan", + "notification.permission.description": "Istunto {{sessionTitle}} projektissa {{projectName}} tarvitsee luvan", + "notification.question.title": "Kysymys", + "notification.question.description": "Istunto {{sessionTitle}} projektissa {{projectName}} esittää kysymyksen", + "notification.action.goToSession": "Siirry istuntoon", + "notification.session.responseReady.title": "Vastaus valmiina", + "notification.session.error.title": "Istuntovirhe", + "notification.session.error.fallbackDescription": "Tapahtui virhe", + "home.recentProjects": "Viimeaikaiset projektit", + "home.empty.title": "Ei viimeaikaisia projekteja", + "home.empty.description": "Aloita avaamalla paikallinen projekti", + "home.title": "Etusivu", + "home.projects": "Projektit", + "home.project.add": "Lisää projekti", + "home.recentlyClosed": "Äskettäin suljettu", + "home.server.collapse": "Kutista palvelimen projektit", + "home.server.expand": "Laajenna palvelimen projektit", + "home.sessions.search.placeholder": "Hae istuntoja", + "home.sessions.search.placeholder.scoped": "Hae istuntoja kohteesta {{scope}}", + "home.sessions.search.sessions": "Istunnot", + "home.sessions.search.noResults": "Ei istuntoja haulle {{query}}", + "home.sessions.empty": "Ei täällä vielä mitään", + "home.sessions.empty.description": "Aloita luomalla istunto", + "home.sessions.group.today": "Tänään", + "home.sessions.group.yesterday": "Eilen", + "home.sessions.group.older": "Vanhemmat", + "home.providerTip": "Yhdistä yli 75 palveluntarjoajaan käyttääksesi muita malleja, kuten Claude, GPT, Gemini jne", + "session.tab.session": "Istunto", + "session.tab.review": "Tarkistus", + "session.tab.context": "Konteksti", + "session.tab.unknown": "Tuntematon istunto", + "session.panel.reviewAndFiles": "Tarkistus ja tiedostot", + "session.error.notFound": "Tätä istuntoa ei löydy", + "session.error.notFound.description": "Tämä välilehti osoittaa istuntoon, jota ei enää ole tällä palvelimella.", + "session.error.notFound.closeTab": "Sulje välilehti", + "session.error.serverConnection": "Ei voi muodostaa yhteyttä tähän palvelimeen", + "session.review.filesChanged": "Muutettuja tiedostoja: {{count}}", + "session.review.change.one": "Muutos", + "session.review.change.other": "Muutokset", + "session.review.loadingChanges": "Ladataan muutoksia...", + "session.review.empty": "Tässä istunnossa ei vielä muutoksia", + "session.review.noVcs": "Git-versionhallintaa ei havaittu, joten muutoksia ei näytetä", + "session.review.noVcs.createGit.title": "Luo Git-säilö", + "session.review.noVcs.createGit.description": "Seuraa, tarkista ja kumoa muutoksia tässä projektissa", + "session.review.noVcs.createGit.actionLoading": "Luodaan Git-säilöä...", + "session.review.noVcs.createGit.action": "Luo Git-säilö", + "session.review.noSnapshot": + "Tilannevedosten seuranta on poistettu käytöstä määrityksessä, joten istunnon muutokset eivät ole käytettävissä", + "session.review.noChanges": "Ei muutoksia", + "session.review.noUncommittedChanges": "Ei vielä sitomattomia muutoksia", + "session.review.noBranchChanges": "Ei vielä haaran muutoksia", + "session.files.selectToOpen": "Valitse avattava tiedosto", + "session.files.all": "Kaikki tiedostot", + "session.files.empty": "Ei tiedostoja", + "session.files.binaryContent": "Binääritiedosto (sisältöä ei voi näyttää)", + "session.messages.renderEarlier": "Hahmonna aiemmat viestit", + "session.messages.loadingEarlier": "Ladataan aikaisempia viestejä...", + "session.messages.loadEarlier": "Lataa aikaisemmat viestit", + "session.messages.loading": "Ladataan viestejä...", + "session.messages.jumpToLatest": "Siirry uusimpaan", + "session.context.addToContext": "Lisää {{selection}} kontekstiin", + "session.todo.title": "Tehtävät", + "session.todo.collapse": "Kutista", + "session.todo.expand": "Laajenna", + "session.todo.progress": "{{done}}/{{total}} tehtävää suoritettu", + "session.question.progress": "{{current}}/{{total}} kysymystä", + "session.question.minimize": "Pienennä kysymys", + "session.question.restore": "Palauta kysymys", + "session.question.pending.one": "{{count}} odottava kysymys", + "session.question.pending.other": "{{count}} odottavaa kysymystä", + "session.followupDock.summary.one": "{{count}} jonossa oleva viesti", + "session.followupDock.summary.other": "{{count}} jonossa olevaa viestiä", + "session.followupDock.sendNow": "Lähetä nyt", + "session.followupDock.edit": "Muokkaa", + "session.followupDock.collapse": "Kutista jonossa olevat viestit", + "session.followupDock.expand": "Laajenna jonossa olevia viestejä", + "session.revertDock.summary.one": "{{count}} palautettu viesti", + "session.revertDock.summary.other": "{{count}} palautettua viestiä", + "session.revertDock.collapse": "Kutista palautetut viestit", + "session.revertDock.expand": "Laajenna palautetut viestit", + "session.revertDock.restore": "Palauta viesti", + "session.new.title": "Rakenna mitä tahansa", + "session.new.project.new": "Uusi projekti", + "session.new.project.search": "Etsi projekteja", + "session.new.project.add": "Lisää projekti", + "session.new.worktree.main": "Päähaara", + "session.new.worktree.mainWithBranch": "Päähaara ({{branch}})", + "session.new.worktree.create": "Luo uusi työpuu", + "session.new.workspace.runIn": "Suorita istunto kohteessa", + "session.new.workspace.triggerLocal": "Paikallinen", + "session.new.workspace.local": "Paikallinen säilö", + "session.new.workspace.existing": "Työtila…", + "session.new.git.none": "Ei Gitiä", + "session.new.lastModified": "Viimeksi muokattu", + "session.header.search.placeholder": "Hae projektista {{project}}", + "session.header.searchFiles": "Etsi tiedostoja", + "session.header.openIn": "Avaa sovelluksessa", + "session.header.open.action": "Avaa {{app}}", + "session.header.open.ariaLabel": "Avaa sovelluksessa {{app}}", + "session.header.open.menu": "Avausvaihtoehdot", + "session.header.open.copyPath": "Kopioi polku", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Tiedostonhallinta", + "session.header.open.fileManager": "Tiedostonhallinta", + "session.header.reveal.finder": "Näytä Finderissa", + "session.header.reveal.fileExplorer": "Näytä Resurssienhallinnassa", + "session.header.reveal.containingFolder": "Avaa tiedoston sisältävä kansio", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminaali", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Tila", + "status.popover.ariaLabel": "Palvelimen määritykset", + "status.popover.tab.servers": "Palvelimet", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Laajennukset", + "status.popover.action.manageServers": "Hallinnoi palvelimia", + "session.share.popover.title": "Julkaise verkossa", + "session.share.popover.description.shared": + "Tämä istunto on julkinen verkossa. Se on kaikkien linkin saaneiden käytettävissä.", + "session.share.popover.description.unshared": + "Jaa istunto julkisesti verkossa. Se on kaikkien linkin saaneiden käytettävissä.", + "session.share.action.share": "Jaa", + "session.share.action.publish": "Julkaise", + "session.share.action.publishing": "Julkaistaan...", + "session.share.action.unpublish": "Peruuta julkaisu", + "session.share.action.unpublishing": "Peruutetaan julkaisua...", + "session.share.action.view": "Näytä", + "session.share.copy.copied": "Kopioitu", + "session.share.copy.copyLink": "Kopioi linkki", + "lsp.tooltip.none": "Ei LSP-palvelimia", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Ladataan kehotetta...", + "terminal.loading": "Ladataan terminaalia...", + "terminal.title": "Terminaali", + "terminal.title.numbered": "Terminaali {{number}}", + "terminal.close": "Sulje terminaali", + "terminal.connectionLost.title": "Yhteys katkesi", + "terminal.connectionLost.abnormalClose": "WebSocket sulkeutui poikkeavasti: {{code}}", + "terminal.connectionLost.description": + "Terminaaliyhteys katkesi. Näin voi tapahtua, kun palvelin käynnistyy uudelleen.", + "terminal.connectTicket.csrfError": + "PTY-yhteyslippu hylättiin alkuperä- tai CSRF-tarkistuksessa. Tarkista palvelimen CORS-määritys.", + "terminal.connectTicket.statusError": "PTY-yhteyslippu epäonnistui tilakoodilla {{status}}", + "titlebar.update": "Päivitä", + "titlebar.updateVersion": "Päivitä {{version}}", + "common.closeTab": "Sulje välilehti", + "common.dismiss": "Hylkää", + "common.moreCountSuffix": " (+{{count}} muuta)", + "common.requestFailed": "Pyyntö epäonnistui", + "common.moreOptions": "Lisää vaihtoehtoja", + "common.learnMore": "Lue lisää", + "common.rename": "Nimeä uudelleen", + "common.export": "Vie", + "common.reset": "Palauta", + "common.archive": "Arkistoi", + "common.delete": "Poista", + "common.close": "Sulje", + "common.edit": "Muokkaa", + "common.loadMore": "Lataa lisää", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "tuntematon", + "common.time.justNow": "Juuri nyt", + "common.time.minutesAgo.short": "{{count}} min sitten", + "common.time.hoursAgo.short": "{{count}} h sitten", + "common.time.daysAgo.short": "{{count}} pv sitten", + "sidebar.menu.toggle": "Näytä tai piilota valikko", + "sidebar.nav.projectsAndSessions": "Projektit ja istunnot", + "sidebar.settings": "Asetukset", + "sidebar.help": "Ohje", + "sidebar.workspaces.enable": "Ota työtilat käyttöön", + "sidebar.workspaces.disable": "Poista työtilat käytöstä", + "sidebar.gettingStarted.title": "Aloittaminen", + "sidebar.gettingStarted.line1": "OpenCode sisältää ilmaisia malleja, joten voit aloittaa heti.", + "sidebar.gettingStarted.line2": + "Yhdistä mikä tahansa palveluntarjoaja käyttääksesi malleja, mm. Claude, GPT, Gemini jne.", + "sidebar.project.recentSessions": "Viimeaikaiset istunnot", + "sidebar.project.viewAllSessions": "Näytä kaikki istunnot", + "sidebar.project.clearNotifications": "Tyhjennä ilmoitukset", + "sidebar.empty.title": "Ei avoimia projekteja", + "sidebar.empty.description": "Aloita avaamalla projekti", + "debugBar.ariaLabel": "Kehityksen suorituskyvyn diagnostiikka", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Viimeisin valmis reittisiirtymä, joka koskee istuntosivua, mitattuna reitityksen alusta ensimmäiseen piirtoon siirtymän valmistumisen jälkeen.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Liukuva kuvataajuus viimeisten 5 sekunnin ajalta.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Pisin kuvan piirtoaika viimeisten 5 sekunnin aikana.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Yli 32 ms kestäneet kuvat viimeisten 5 sekunnin aikana.", + "debugBar.long.label": "PITKÄ", + "debugBar.long.tip": "Estetty aika ja pitkien tehtävien määrä viimeisten 5 sekunnin aikana. Pisin tehtävä: {{max}}.", + "debugBar.delay.label": "VIIVE", + "debugBar.delay.tip": "Pisin havaittu syöttöviive viimeisten 5 sekunnin aikana.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Vuorovaikutuksen arvioitu kesto viimeisen 5 sekunnin ajalta. Tämä on INP-tyyppinen, ei virallinen Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulatiivinen asettelusiirtymä sovelluksen nykyisen käyttöjakson aikana.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Käytetty JS-keko suhteessa keon rajaan. Vain Chromium.", + "debugBar.mem.tip": "Käytetty JS-keko suhteessa keon rajaan. {{used}} / {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Pakota kohdistustyylit kaikkiin vuorovaikutteisiin elementteihin", + "debugBar.focus.on": "KÄYTÖSSÄ", + "debugBar.focus.off": "EI KÄYTÖSSÄ", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Pakota koko sovelluksen asettelun suunta valittua kieltä muuttamatta", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Työpöytä", + "settings.section.server": "Palvelin", + "settings.tab.general": "Yleiset", + "settings.tab.shortcuts": "Pikanäppäimet", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL-integraatio", + "settings.desktop.wsl.description": "Suorita OpenCode-palvelin WSL:n sisällä Windowsissa.", + "settings.general.section.appearance": "Ulkonäkö", + "settings.general.section.advanced": "Lisäasetukset", + "settings.general.section.notifications": "Järjestelmän ilmoitukset", + "settings.general.section.updates": "Päivitykset", + "settings.general.section.sounds": "Äänitehosteet", + "settings.general.section.feed": "Syöte", + "settings.general.section.display": "Näyttö", + "settings.general.row.language.title": "Kieli", + "settings.general.row.language.description": "Vaihda OpenCoden näyttökieli", + "settings.general.row.shell.title": "Terminaalin komentotulkki", + "settings.general.row.shell.description": "Terminaalin ja agenttityökalujen käyttämä komentotulkki", + "settings.general.row.shell.autoDefault": "Automaattinen (oletus)", + "settings.general.row.shell.terminalOnly": "vain terminaali", + "settings.general.row.appearance.title": "Ulkonäkö", + "settings.general.row.appearance.description": "Mukauta, miltä OpenCode näyttää laitteellasi", + "settings.general.row.colorScheme.title": "Värimalli", + "settings.general.row.colorScheme.description": + "Valitse, käyttääkö OpenCode järjestelmän mukaista, vaaleaa vai tummaa teemaa", + "settings.general.row.theme.title": "Teema", + "settings.general.row.theme.description": "Mukauta OpenCoden teemaa.", + "settings.general.row.font.title": "Koodifontti", + "settings.general.row.font.description": "Mukauta koodilohkoissa käytettyä fonttia", + "settings.general.row.terminalFont.title": "Terminaalin fontti", + "settings.general.row.terminalFont.description": "Mukauta terminaalissa käytettävää fonttia", + "settings.general.row.uiFont.title": "Käyttöliittymän fontti", + "settings.general.row.uiFont.description": "Mukauta koko käyttöliittymässä käytettyä fonttia", + "settings.general.row.followup.title": "Jatkokehotteiden toiminta", + "settings.general.row.followup.description": "Valitse, ohjaavatko jatkokehotteet heti vai odottavatko ne jonossa", + "settings.general.row.followup.option.queue": "Jonota", + "settings.general.row.followup.option.steer": "Ohjaa heti", + "settings.general.row.showFileTree.title": "Tiedostopuu", + "settings.general.row.showFileTree.description": "Näytä tiedostopuupaneeli istunnoissa", + "settings.general.row.showNavigation.title": "Navigointiohjaimet", + "settings.general.row.showNavigation.description": + "Näytä taaksepäin- ja eteenpäin-painikkeet työpöydän otsikkorivillä", + "settings.general.row.showSearch.title": "Komentopaletti", + "settings.general.row.showSearch.description": "Näytä haku- ja komentopalettipainike otsikkorivillä", + "settings.general.row.showTerminal.title": "Terminaali", + "settings.general.row.showTerminal.description": "Näytä terminaalipainike työpöydän otsikkorivillä", + "settings.general.row.showStatus.title": "Palvelimen tila", + "settings.general.row.showStatus.description": "Näytä palvelimen tilapainike otsikkorivillä", + "settings.general.row.mobileTitlebarBottom.title": "Navigointi alareunassa", + "settings.general.row.mobileTitlebarBottom.description": + "Sijoita otsikkopalkki ja istuntovälilehdet mobiililaitteen näytön alareunaan", + "settings.general.row.showCustomAgents.title": "Näytä agentin valinta", + "settings.general.row.showCustomAgents.description": + "Vaihda agenttien välillä viestikentässä. Kun valinta on piilotettu, Build-agenttia käytetään oletuksena.", + "settings.general.row.reasoningSummaries.title": "Näytä päättelyn yhteenvedot", + "settings.general.row.reasoningSummaries.description": "Näytä mallin päättelyn yhteenvedot aikajanalla", + "settings.general.row.shellToolPartsExpanded.title": "Laajenna shell-työkalun osat", + "settings.general.row.shellToolPartsExpanded.description": + "Näytä shell-työkalun osat oletuksena laajennettuina aikajanalla", + "settings.general.row.editToolPartsExpanded.title": "Laajenna muokkaustyökalun osat", + "settings.general.row.editToolPartsExpanded.description": + "Näytä muokkaus-, kirjoitus- ja paikkaustyökalun osat oletusarvoisesti laajennettuina aikajanalla", + "settings.general.row.newInterface.title": "Uusi asettelu", + "settings.general.row.newInterface.badge": "Uusi", + "settings.general.row.newInterface.description": + "Käytä uusia välilehtiä ja etusivun asettelua. Vaihda asettelujen välillä rajoitetun ajan.", + "settings.general.row.newInterfaceNotice.title": "Käytät nyt uutta asettelua", + "settings.general.row.newInterfaceNotice.description": "Edellinen asettelu ei ole enää käytettävissä", + "settings.general.row.newInterfaceNotice.dismiss": "Hylkää", + "settings.general.row.pinchZoom.title": "Zoomaa nipistämällä", + "settings.general.row.pinchZoom.description": "Salli zoomaus ohjauslevyn nipistyseleellä ja Ctrl-vierityksellä", + "settings.general.row.wayland.title": "Käytä alkuperäistä Waylandia", + "settings.general.row.wayland.description": + "Poista X11-varatila käytöstä Waylandissa. Vaatii uudelleenkäynnistyksen.", + "settings.general.row.wayland.tooltip": + "Linuxissa alkuperäinen Wayland voi olla vakaampi käytettäessä näyttöjä, joiden virkistystaajuudet eroavat toisistaan.", + "settings.general.row.releaseNotes.title": "Julkaisutiedot", + "settings.general.row.releaseNotes.description": "Näytä Mitä uutta -ponnahdusikkunat päivitysten jälkeen", + "settings.updates.row.startup.title": "Tarkista päivitykset käynnistyksen yhteydessä", + "settings.updates.row.startup.description": "Tarkista päivitykset automaattisesti, kun OpenCode käynnistyy", + "settings.updates.row.check.title": "Tarkista päivitykset", + "settings.updates.row.check.description": "Tarkista päivitykset manuaalisesti ja asenna, jos niitä on saatavilla", + "settings.updates.action.checkNow": "Tarkista nyt", + "settings.updates.action.checking": "Tarkistetaan...", + "settings.updates.action.downloading": "Ladataan...", + "settings.updates.action.installing": "Asennetaan...", + "settings.updates.toast.latest.title": "Olet ajan tasalla", + "settings.updates.toast.latest.description": "Käytät OpenCoden uusinta versiota.", + "sound.option.none": "Ei mitään", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "Agentti", + "settings.general.notifications.agent.description": + "Näytä järjestelmäilmoitus, kun agentti on valmis tai tarvitsee huomiota", + "settings.general.notifications.permissions.title": "Käyttöoikeudet", + "settings.general.notifications.permissions.description": "Näytä järjestelmäilmoitus, kun lupa vaaditaan", + "settings.general.notifications.errors.title": "Virheet", + "settings.general.notifications.errors.description": "Näytä järjestelmäilmoitus, kun tapahtuu virhe", + "settings.general.sounds.agent.title": "Agentti", + "settings.general.sounds.agent.description": "Toista ääni, kun agentti on valmis tai tarvitsee huomiota", + "settings.general.sounds.permissions.title": "Käyttöoikeudet", + "settings.general.sounds.permissions.description": "Toista ääni, kun vaaditaan lupa", + "settings.general.sounds.errors.title": "Virheet", + "settings.general.sounds.errors.description": "Toista ääni, kun tapahtuu virhe", + "settings.shortcuts.title": "Pikanäppäimet", + "settings.shortcuts.reset.button": "Palauta oletusasetukset", + "settings.shortcuts.reset.toast.title": "Pikanäppäimet palautettu", + "settings.shortcuts.reset.toast.description": "Pikanäppäimet on palautettu oletusasetuksiin.", + "settings.shortcuts.conflict.title": "Pikanäppäin on jo käytössä", + "settings.shortcuts.conflict.description": "{{keybind}} on jo määritetty toiminnolle {{titles}}.", + "settings.shortcuts.unassigned": "Ei määritetty", + "settings.shortcuts.pressKeys": "Paina näppäimiä", + "settings.shortcuts.search.placeholder": "Hae pikanäppäimiä", + "settings.shortcuts.search.empty": "Pikanäppäimiä ei löytynyt", + "settings.shortcuts.group.general": "Yleiset", + "settings.shortcuts.group.session": "Istunto", + "settings.shortcuts.group.navigation": "Navigointi", + "settings.shortcuts.group.modelAndAgent": "Malli ja agentti", + "settings.shortcuts.group.terminal": "Terminaali", + "settings.shortcuts.group.prompt": "Kehote", + "settings.providers.title": "Palveluntarjoajat", + "settings.providers.description": "Palveluntarjoajan asetukset voidaan määrittää täällä.", + "settings.providers.section.connected": "Yhdistetyt palveluntarjoajat", + "settings.providers.connected.empty": "Ei yhdistettyjä palveluntarjoajia", + "settings.providers.connected.environmentDescription": "Yhdistetty ympäristömuuttujien kautta", + "settings.providers.section.popular": "Suositut palveluntarjoajat", + "settings.providers.custom.description": + "Lisää OpenAI-yhteensopiva palveluntarjoaja perus-URL-osoitteen perusteella.", + "settings.providers.tag.environment": "Ympäristö", + "settings.providers.tag.config": "Määritys", + "settings.providers.tag.custom": "Mukautettu", + "settings.providers.tag.other": "Muut", + "settings.models.title": "Mallit", + "settings.models.description": "Mallin asetukset voidaan määrittää täällä.", + "settings.agents.title": "Agentit", + "settings.agents.description": "Agentin asetukset voidaan määrittää täällä.", + "settings.commands.title": "Komennot", + "settings.commands.description": "Komentoasetukset voidaan määrittää täällä.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP-asetukset voidaan määrittää täällä.", + "settings.permissions.title": "Käyttöoikeudet", + "settings.permissions.description": "Hallitse, mitä työkaluja palvelin voi käyttää oletuksena.", + "settings.permissions.section.tools": "Työkalut", + "settings.permissions.toast.updateFailed.title": "Käyttöoikeuksien päivittäminen epäonnistui", + "settings.permissions.action.allow": "Salli", + "settings.permissions.action.ask": "Kysy", + "settings.permissions.action.deny": "Kiellä", + "settings.permissions.tool.read.title": "Lue", + "settings.permissions.tool.read.description": "Tiedoston lukeminen (vastaa tiedostopolkua)", + "settings.permissions.tool.edit.title": "Muokkaa", + "settings.permissions.tool.edit.description": + "Tiedostojen muokkaaminen, mukaan lukien muokkaus-, kirjoitus- ja paikkaustoiminnot", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Etsi glob-malleja vastaavia tiedostoja", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Hae tiedostojen sisällöstä säännöllisten lausekkeiden avulla", + "settings.permissions.tool.list.title": "Luettelo", + "settings.permissions.tool.list.description": "Listaa tiedostot hakemistossa", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Suorita shell-komennot", + "settings.permissions.tool.task.title": "Tehtävä", + "settings.permissions.tool.task.description": "Käynnistä aliagentit", + "settings.permissions.tool.skill.title": "Taito", + "settings.permissions.tool.skill.description": "Lataa taito nimen perusteella", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Suorita kielipalvelinkyselyt", + "settings.permissions.tool.todowrite.title": "Tehtävälistan päivitys", + "settings.permissions.tool.todowrite.description": "Päivitä tehtävälista", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Nouda sisältöä URL-osoitteesta", + "settings.permissions.tool.websearch.title": "Verkkohaku", + "settings.permissions.tool.websearch.description": "Hae verkosta", + "settings.permissions.tool.external_directory.title": "Ulkoinen hakemisto", + "settings.permissions.tool.external_directory.description": "Käytä tiedostoja projektihakemiston ulkopuolella", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Tunnista toistuvat työkalukutsut samalla syötteellä", + "session.delete.failed.title": "Istunnon poistaminen epäonnistui", + "session.delete.title": "Poista istunto", + "session.delete.confirm": 'Poistetaanko istunto "{{name}}"?', + "session.delete.button": "Poista istunto", + "workspace.new": "Uusi työtila", + "workspace.type.local": "paikallinen", + "workspace.type.sandbox": "hiekkalaatikko", + "workspace.create.failed.title": "Työtilan luominen epäonnistui", + "workspace.delete.failed.title": "Työtilan poistaminen epäonnistui", + "workspace.resetting.title": "Palautetaan työtilaa", + "workspace.resetting.description": "Tämä voi kestää minuutin.", + "workspace.reset.failed.title": "Työtilan palauttaminen epäonnistui", + "workspace.reset.success.title": "Työtila palautettu", + "workspace.reset.success.description": "Työtila vastaa nyt oletushaaraa.", + "workspace.error.stillPreparing": "Työtilaa valmistellaan vielä", + "workspace.status.checking": "Tarkistetaan yhdistämättömiä muutoksia...", + "workspace.status.error": "Gitin tilaa ei voi vahvistaa.", + "workspace.status.clean": "Yhdistämättömiä muutoksia ei havaittu.", + "workspace.status.dirty": "Tässä työtilassa havaittu yhdistämättömiä muutoksia.", + "workspace.delete.title": "Poista työtila", + "workspace.delete.confirm": 'Poistetaanko työtila "{{name}}"?', + "workspace.delete.button": "Poista työtila", + "workspace.reset.title": "Palauta työtila", + "workspace.reset.confirm": 'Palautetaanko työtila "{{name}}"?', + "workspace.reset.button": "Palauta työtila", + "workspace.reset.archived.none": "Aktiivisia istuntoja ei arkistoida.", + "workspace.reset.archived.one": "1 istunto arkistoidaan.", + "workspace.reset.archived.many": "{{count}} istuntoa arkistoidaan.", + "workspace.reset.note": "Tämä palauttaa työtilan vastaamaan oletushaaraa.", + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Tiedosto", + "desktop.menu.edit": "Muokkaa", + "desktop.menu.view": "Näytä", + "desktop.menu.go": "Siirry", + "desktop.menu.window": "Ikkuna", + "desktop.menu.help": "Ohje", + "desktop.menu.checkForUpdates": "Tarkista päivitykset...", + "desktop.menu.settings": "Asetukset", + "desktop.menu.reloadWebview": "Lataa verkkonäkymä uudelleen", + "desktop.menu.restart": "Käynnistä uudelleen", + "desktop.menu.exportLogs": "Vie lokit...", + "desktop.menu.newSession": "Uusi istunto", + "desktop.menu.openProject": "Avaa projekti...", + "desktop.menu.newWindow": "Uusi ikkuna", + "desktop.menu.closeWindow": "Sulje ikkuna", + "desktop.menu.undo": "Kumoa", + "desktop.menu.redo": "Tee uudelleen", + "desktop.menu.cut": "Leikkaa", + "desktop.menu.copy": "Kopioi", + "desktop.menu.paste": "Liitä", + "desktop.menu.delete": "Poista", + "desktop.menu.selectAll": "Valitse kaikki", + "desktop.menu.toggleSidebar": "Näytä tai piilota sivupalkki", + "desktop.menu.toggleTerminal": "Näytä tai piilota terminaali", + "desktop.menu.toggleFileTree": "Näytä tai piilota tiedostopuu", + "desktop.menu.reload": "Lataa uudelleen", + "desktop.menu.toggleDeveloperTools": "Näytä tai piilota kehittäjätyökalut", + "desktop.menu.actualSize": "Todellinen koko", + "desktop.menu.zoomIn": "Lähennä", + "desktop.menu.zoomOut": "Loitonna", + "desktop.menu.toggleFullScreen": "Koko näytön tila", + "desktop.menu.back": "Takaisin", + "desktop.menu.forward": "Eteenpäin", + "desktop.menu.previousSession": "Edellinen istunto", + "desktop.menu.nextSession": "Seuraava istunto", + "desktop.menu.previousProject": "Edellinen projekti", + "desktop.menu.nextProject": "Seuraava projekti", + "desktop.menu.minimize": "Pienennä", + "desktop.menu.maximize": "Suurenna", + "desktop.menu.documentation": "OpenCode-dokumentaatio", + "desktop.menu.supportForum": "Tukifoorumi", + "desktop.menu.shareFeedback": "Anna palautetta", + "desktop.menu.reportBug": "Ilmoita ohjelmavirheestä", + "desktop.menu.ariaLabel": "OpenCode-valikko", + "desktop.updater.dialog.checkFailed.message": "Päivitysten tarkistus epäonnistui.", + "desktop.updater.dialog.checkFailed.title": "Päivitysvirhe", + "desktop.updater.dialog.upToDate.message": "Olet ajan tasalla.", + "desktop.updater.dialog.upToDate.title": "Ei päivityksiä", + "desktop.updater.dialog.ready.message": "Päivitys {{version}} on ladattu. Käynnistetäänkö nyt uudelleen?", + "desktop.updater.dialog.ready.title": "Päivitys valmis", + "desktop.updater.dialog.restart": "Käynnistä uudelleen", + "desktop.updater.dialog.later": "Myöhemmin", + "desktop.recovery.action.relaunch": "Käynnistä uudelleen", + "desktop.recovery.action.exportLogs": "Vie lokit", + "desktop.recovery.action.keepWaiting": "Jatka odottamista", + "desktop.recovery.action.quit": "Lopeta", + "desktop.recovery.loadFailed": "OpenCoden lataaminen epäonnistui", + "desktop.recovery.terminated": "OpenCode-ikkuna sulkeutui odottamatta", + "desktop.recovery.unresponsive": "OpenCode ei vastaa", + "desktop.recovery.unresponsive.detail": "Voit käynnistää sovelluksen uudelleen, avata lokit tai jatkaa odottamista.", + "desktop.recovery.loadFailed.detail": "Ikkuna: {{window}}\nURL: {{url}}\nVirhe: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Ikkuna: {{window}}\nSyy: {{reason}}\nKoodi: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Valitse kansio", + "desktop.dialog.chooseFile": "Valitse tiedosto", + "desktop.dialog.saveFile": "Tallenna tiedosto", + "desktop.dialog.files": "Tiedostot", + "desktop.server.local": "Paikallinen palvelin", + "desktop.wsl.error.windowsOnly": "WSL on käytettävissä vain Windowsissa", + "desktop.wsl.error.unavailable": "WSL ei ole käytettävissä", + "desktop.wsl.error.listInstalled": "Asennettujen WSL-jakelujen luettelointi epäonnistui", + "desktop.wsl.error.listOnline": "Verkossa saatavilla olevien WSL-jakelujen luettelointi epäonnistui", + "desktop.wsl.error.executeDistro": "Jakelussa ei voi suorittaa komentoja", + "desktop.wsl.error.installWsl": "WSL:n asennus epäonnistui", + "desktop.wsl.error.installDistro": "Jakelun asentaminen epäonnistui: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCoden asennus epäonnistui", + "desktop.wsl.error.alreadyAdded": "{{distro}} on jo lisätty", + "desktop.wsl.error.opencodeMissing": "opencodea ei ole asennettu tähän jakeluun", + "desktop.wsl.error.opencodeCannotRun": "opencode on asennettu, mutta sitä ei voitu suorittaa", + "desktop.wsl.error.opencodeNotInstalled": "OpenCodea ei ole asennettu jakeluun {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCoden päivitys valmistui, mutta {{distro}} ilmoittaa yhä version {{installed}}; odotettu versio on {{expected}}", + "desktop.wsl.error.noVersion": "ei versiota", + "desktop.wsl.error.serverExited": "WSL-palvelin sulkeutui käynnistyksen jälkeen (koodi={{code}} signaali={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL-palvelin sulkeutui ennen toimintavalmiutta (koodi={{code}} signaali={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Jakelun {{distro}} sivuprosessin kuntotarkistus aikakatkaistiin {{timeout}} ms:n jälkeen", + "desktop.wsl.error.commandTimeout": "Komento {{command}} {{args}} aikakatkaistiin {{timeout}} ms:n jälkeen", + "desktop.wsl.error.failedPort": "Portin noutaminen epäonnistui", + "desktop.picker.error.notSelected": "Tiedostovalitsimessa ei valittu tiedostoa", + "desktop.picker.error.sizeLimit": "Valitut liitteet ylittävät {{limit}} Mt:n rajan", + "command.logs.export": "Vie lokit", + "help.tabs.toast.ariaLabel": "Esittelyssä välilehdet. Järjestä työsi ja aktiiviset istuntosi välilehtien avulla", + "help.tabs.toast.dismiss": "Sulje välilehtien tiedot", + "help.tabs.title": "Esittelyssä välilehdet", + "help.tabs.description": "Järjestä työsi ja aktiiviset istuntosi välilehtien avulla", + "help.tabs.date": "14. heinäkuuta", + "help.tabs.introduction": "OpenCode Desktop perustuu nyt välilehtiin.", + "help.tabs.sessions": + "Aloita uusi istunto välilehdessä tai avaa olemassa oleva istunto mistä tahansa projektistasi. Avaa uusi välilehti, kun aloitat jotain uutta, ja sulje se, kun olet valmis.", + "help.tabs.organize": + "Muutaman välilehden pitäminen avoinna helpottaa aktiivisten istuntojen järjestämistä. Nimeä välilehdet helposti muistettaviksi, jos aiot pitää ne avoinna.", + "help.tabs.home": + "Kaikki istuntosi ja projektisi ovat uudessa etusivunäkymässä. Istunnon valitseminen avaa sen välilehteen.", + "help.tabs.persistence": "Kun avaat sovelluksen uudelleen, välilehtesi ovat yhä avoinna.", + "help.tabs.worktrees": + "Uusi ulkoasu ei vielä tue Git-työpuita, mutta tuki on tulossa pian. Jos haluat jatkaa aiemman ulkoasun käyttöä, voit vaihtaa ulkoasua asetuksissa. Huomaa kuitenkin, että uudesta ulkoasusta tulee pysyvä muutaman viikon kuluttua.", +} diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c89860a44901422662957b4704a3d1f366a4fa1 --- /dev/null +++ b/packages/app/src/i18n/fr.ts @@ -0,0 +1,1199 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Fichier", + "desktop.menu.edit": "Édition", + "desktop.menu.view": "Affichage", + "desktop.menu.go": "Aller", + "desktop.menu.window": "Fenêtre", + "desktop.menu.help": "Aide", + "desktop.menu.checkForUpdates": "Rechercher les mises à jour...", + "desktop.menu.settings": "Paramètres", + "desktop.menu.reloadWebview": "Recharger la vue Web", + "desktop.menu.restart": "Redémarrer", + "desktop.menu.exportLogs": "Exporter les journaux...", + "desktop.menu.newSession": "Nouvelle session", + "desktop.menu.openProject": "Ouvrir un projet...", + "desktop.menu.newWindow": "Nouvelle fenêtre", + "desktop.menu.closeWindow": "Fermer la fenêtre", + "desktop.menu.undo": "Annuler", + "desktop.menu.redo": "Rétablir", + "desktop.menu.cut": "Couper", + "desktop.menu.copy": "Copier", + "desktop.menu.paste": "Coller", + "desktop.menu.delete": "Supprimer", + "desktop.menu.selectAll": "Tout sélectionner", + "desktop.menu.toggleSidebar": "Afficher ou masquer la barre latérale", + "desktop.menu.toggleTerminal": "Afficher ou masquer le terminal", + "desktop.menu.toggleFileTree": "Afficher ou masquer l'arborescence des fichiers", + "desktop.menu.reload": "Recharger", + "desktop.menu.toggleDeveloperTools": "Afficher ou masquer les outils de développement", + "desktop.menu.actualSize": "Taille réelle", + "desktop.menu.zoomIn": "Zoom avant", + "desktop.menu.zoomOut": "Zoom arrière", + "desktop.menu.toggleFullScreen": "Activer ou désactiver le mode plein écran", + "desktop.menu.back": "Précédent", + "desktop.menu.forward": "Suivant", + "desktop.menu.previousSession": "Session précédente", + "desktop.menu.nextSession": "Session suivante", + "desktop.menu.previousProject": "Projet précédent", + "desktop.menu.nextProject": "Projet suivant", + "desktop.menu.minimize": "Réduire", + "desktop.menu.maximize": "Agrandir", + "desktop.menu.documentation": "Documentation d'OpenCode", + "desktop.menu.supportForum": "Forum d'assistance", + "desktop.menu.shareFeedback": "Envoyer des commentaires", + "desktop.menu.reportBug": "Signaler un bogue", + "desktop.menu.ariaLabel": "Menu OpenCode", + + "desktop.updater.dialog.checkFailed.message": "La recherche de mises à jour a échoué.", + "desktop.updater.dialog.checkFailed.title": "Erreur de mise à jour", + "desktop.updater.dialog.upToDate.message": "Vous êtes à jour.", + "desktop.updater.dialog.upToDate.title": "Aucune mise à jour", + "desktop.updater.dialog.ready.message": "La mise à jour {{version}} a été téléchargée. Redémarrer maintenant ?", + "desktop.updater.dialog.ready.title": "Mise à jour prête", + "desktop.updater.dialog.restart": "Redémarrer", + "desktop.updater.dialog.later": "Plus tard", + + "desktop.recovery.action.relaunch": "Relancer", + "desktop.recovery.action.exportLogs": "Exporter les journaux", + "desktop.recovery.action.keepWaiting": "Continuer d'attendre", + "desktop.recovery.action.quit": "Quitter", + "desktop.recovery.loadFailed": "Échec du chargement d'OpenCode", + "desktop.recovery.terminated": "La fenêtre OpenCode s'est arrêtée de manière inattendue", + "desktop.recovery.unresponsive": "OpenCode ne répond pas", + "desktop.recovery.unresponsive.detail": + "Vous pouvez relancer l'application, ouvrir les journaux ou continuer d'attendre.", + "desktop.recovery.loadFailed.detail": "Fenêtre : {{window}}\nURL : {{url}}\nErreur : {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Fenêtre : {{window}}\nMotif : {{reason}}\nCode : {{code}}", + "desktop.recovery.unknown": "", + + "desktop.dialog.chooseFolder": "Sélectionner un dossier", + "desktop.dialog.chooseFile": "Sélectionner un fichier", + "desktop.dialog.saveFile": "Enregistrer le fichier", + "desktop.dialog.files": "Fichiers", + "desktop.server.local": "Serveur local", + + "desktop.wsl.error.windowsOnly": "WSL est disponible uniquement sous Windows", + "desktop.wsl.error.unavailable": "WSL est indisponible", + "desktop.wsl.error.listInstalled": "Impossible de répertorier les distributions WSL installées", + "desktop.wsl.error.listOnline": "Impossible de répertorier les distributions WSL en ligne", + "desktop.wsl.error.executeDistro": "Impossible d'exécuter des commandes dans la distribution", + "desktop.wsl.error.installWsl": "Échec de l'installation de WSL", + "desktop.wsl.error.installDistro": "Échec de l'installation de la distribution : {{distro}}", + "desktop.wsl.error.installOpencode": "Échec de l'installation d'OpenCode", + "desktop.wsl.error.alreadyAdded": "{{distro}} a déjà été ajoutée", + "desktop.wsl.error.opencodeMissing": "opencode n'est pas installé dans cette distribution", + "desktop.wsl.error.opencodeCannotRun": "opencode est installé, mais n'a pas pu s'exécuter", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode n'est pas installé dans {{distro}}", + "desktop.wsl.error.updateVersion": + "La mise à jour d'OpenCode est terminée, mais {{distro}} indique toujours {{installed}} au lieu de {{expected}}", + "desktop.wsl.error.noVersion": "aucune version", + "desktop.wsl.error.serverExited": "Le serveur WSL s'est arrêté après son démarrage (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Le serveur WSL s'est arrêté avant d'être opérationnel (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "La vérification de l'état du processus auxiliaire pour {{distro}} a dépassé le délai de {{timeout}} ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} a dépassé le délai de {{timeout}} ms", + "desktop.wsl.error.failedPort": "Impossible d'obtenir le port", + + "desktop.picker.error.notSelected": "Aucun fichier n'a été sélectionné dans le sélecteur", + "desktop.picker.error.sizeLimit": "Les pièces jointes sélectionnées dépassent la limite de {{limit}} Mo", + + "command.category.suggested": "Suggéré", + "command.category.view": "Affichage", + "command.category.project": "Projet", + "command.category.provider": "Fournisseur", + "command.category.server": "Serveur", + "command.category.session": "Session", + "command.category.theme": "Thème", + "command.category.language": "Langue", + "command.category.file": "Fichier", + "command.category.context": "Contexte", + "command.category.terminal": "Terminal", + "command.category.model": "Modèle", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Permissions", + "command.category.workspace": "Espace de travail", + "command.category.settings": "Paramètres", + "command.logs.export": "Exporter les journaux", + "theme.scheme.system": "Système", + "theme.scheme.light": "Clair", + "theme.scheme.dark": "Sombre", + "command.sidebar.toggle": "Basculer la barre latérale", + "command.project.open": "Ouvrir un projet", + "command.project.previous": "Projet précédent", + "command.project.next": "Projet suivant", + "command.project.index": "Passer au projet {{index}}", + "command.provider.connect": "Connecter un fournisseur", + "command.server.switch": "Changer de serveur", + "command.settings.open": "Ouvrir les paramètres", + "command.session.previous": "Session précédente", + "command.session.next": "Session suivante", + "command.session.previous.unseen": "Session précédente non lue", + "command.session.next.unseen": "Session suivante non lue", + "command.session.archive": "Archiver la session", + "command.palette": "Palette de commandes", + "command.theme.cycle": "Changer de thème", + "command.theme.set": "Utiliser le thème : {{theme}}", + "command.theme.scheme.cycle": "Changer de schéma de couleurs", + "command.theme.scheme.set": "Utiliser le schéma de couleurs : {{scheme}}", + "command.language.cycle": "Changer de langue", + "command.language.set": "Utiliser la langue : {{language}}", + "command.session.new": "Nouvelle session", + "command.file.open": "Ouvrir un fichier", + "command.tab.close": "Fermer l'onglet", + "command.tab.reopenClosed": "Rouvrir l'onglet fermé", + "command.context.addSelection": "Ajouter la sélection au contexte", + "command.context.addSelection.description": "Ajouter les lignes sélectionnées du fichier actuel", + "command.input.focus": "Activer la zone de saisie", + "command.terminal.toggle": "Basculer le terminal", + "command.fileTree.toggle": "Basculer l'arborescence des fichiers", + "command.review.toggle": "Basculer la revue", + "command.terminal.new": "Nouveau terminal", + "command.terminal.new.description": "Créer un nouvel onglet de terminal", + "command.steps.toggle": "Basculer les étapes", + "command.steps.toggle.description": "Afficher ou masquer les étapes du message actuel", + "command.message.previous": "Message précédent", + "command.message.previous.description": "Aller au message utilisateur précédent", + "command.message.next": "Message suivant", + "command.message.next.description": "Aller au message utilisateur suivant", + "command.model.choose": "Choisir le modèle", + "command.model.choose.description": "Sélectionner un modèle différent", + "command.mcp.toggle": "Activer ou désactiver les MCP", + "command.mcp.toggle.description": "Activer ou désactiver les MCP", + "command.agent.cycle": "Changer d'agent", + "command.agent.cycle.description": "Passer à l'agent suivant", + "command.agent.cycle.reverse": "Changer d'agent (inverse)", + "command.agent.cycle.reverse.description": "Passer à l'agent précédent", + "command.model.variant.cycle": "Changer l'effort de réflexion", + "command.model.variant.cycle.description": "Passer au niveau d'effort suivant", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Invite", + "command.permissions.autoaccept.enable": "Accepter automatiquement les permissions", + "command.permissions.autoaccept.disable": "Arrêter d'accepter automatiquement les permissions", + "command.workspace.toggle": "Basculer les espaces de travail", + "command.workspace.toggle.description": "Activer ou désactiver plusieurs espaces de travail dans la barre latérale", + "command.session.undo": "Annuler", + "command.session.undo.description": "Annuler le dernier message", + "command.session.redo": "Rétablir", + "command.session.redo.description": "Rétablir le dernier message annulé", + "command.session.compact": "Compacter la session", + "command.session.compact.description": "Résumer la session pour réduire la taille du contexte", + "command.session.fork": "Bifurquer à partir du message", + "command.session.fork.description": "Créer une nouvelle session à partir d'un message précédent", + "command.session.share": "Partager la session", + "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é", + "palette.group.commands": "Commandes", + "palette.group.files": "Fichiers", + "dialog.provider.search.placeholder": "Rechercher des fournisseurs", + "dialog.provider.empty": "Aucun fournisseur trouvé", + "dialog.provider.group.popular": "Populaires", + "dialog.provider.group.other": "Autres", + "dialog.provider.custom.label": "Fournisseur personnalisé compatible avec OpenAI", + "dialog.provider.tag.recommended": "Recommandé", + "dialog.provider.opencode.note": "Modèles sélectionnés incluant Claude, GPT, Gemini et plus", + "dialog.provider.opencode.tagline": "Modèles optimisés et fiables", + "dialog.provider.opencodeGo.tagline": "Abonnement abordable pour tous", + "dialog.provider.anthropic.note": "Connectez-vous avec Claude Pro/Max ou une clé API", + "dialog.provider.copilot.note": "Connectez-vous avec Copilot ou une clé API", + "dialog.provider.openai.note": "Connectez-vous avec ChatGPT Pro/Plus ou une clé API", + "dialog.provider.google.note": "Modèles Gemini pour des réponses rapides et structurées", + "dialog.provider.openrouter.note": "Accédez à tous les modèles pris en charge depuis un seul fournisseur", + "dialog.provider.vercel.note": "Accès unifié aux modèles d'IA avec routage intelligent", + "dialog.model.select.title": "Sélectionner un modèle", + "dialog.model.search.placeholder": "Rechercher des modèles", + "dialog.model.empty": "Aucun résultat de modèle", + "dialog.model.manage": "Gérer les modèles", + "dialog.model.manage.description": "Personnalisez les modèles qui apparaissent dans le sélecteur.", + "dialog.model.manage.provider.toggle": "Activer ou désactiver tous les modèles {{provider}}", + "dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode", + "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires", + "dialog.model.unpaid.viewMoreProviders": "Voir plus de 70 autres fournisseurs", + "dialog.provider.viewAll": "Voir plus de fournisseurs", + "provider.connect.title": "Connecter {{provider}}", + "provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max", + "provider.connect.selectMethod": "Sélectionnez la méthode de connexion pour {{provider}}.", + "provider.connect.method.apiKey": "Clé API", + "provider.connect.method.browser": "Navigateur", + "provider.connect.method.headless": "Mode headless", + "provider.connect.status.inProgress": "Autorisation en cours...", + "provider.connect.status.waiting": "En attente d'autorisation...", + "provider.connect.status.failed": "Échec de l'autorisation : {{error}}", + "provider.connect.apiKey.description": + "Entrez votre clé API {{provider}} pour connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.", + "provider.connect.apiKey.label": "Clé API {{provider}}", + "provider.connect.apiKey.placeholder": "Clé API", + "provider.connect.apiKey.required": "La clé API est requise", + "provider.connect.opencodeZen.line1": + "OpenCode Zen vous donne accès à un ensemble sélectionné de modèles fiables et optimisés pour les agents de codage.", + "provider.connect.opencodeZen.line2": + "Avec une seule clé API, vous aurez accès à des modèles tels que Claude, GPT, Gemini, GLM et plus encore.", + "provider.connect.opencodeZen.visit.prefix": "Visitez ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " pour récupérer votre clé API.", + "provider.connect.oauth.code.visit.prefix": "Visitez ", + "provider.connect.oauth.code.visit.link": "ce lien", + "provider.connect.oauth.code.visit.suffix": + " pour récupérer votre code d'autorisation afin de connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.", + "provider.connect.oauth.code.label": "Code d'autorisation {{method}}", + "provider.connect.oauth.code.placeholder": "Code d'autorisation", + "provider.connect.oauth.code.required": "Le code d'autorisation est requis", + "provider.connect.oauth.code.invalid": "Code d'autorisation invalide", + "provider.connect.oauth.auto.visit.prefix": "Visitez ", + "provider.connect.oauth.auto.visit.link": "ce lien", + "provider.connect.oauth.auto.visit.suffix": + " et entrez le code ci-dessous pour connecter votre compte et utiliser les modèles {{provider}} dans OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Code de confirmation", + "provider.connect.toast.connected.title": "{{provider}} connecté", + "provider.connect.toast.connected.description": "Les modèles {{provider}} sont maintenant disponibles.", + "provider.custom.title": "Fournisseur personnalisé", + "provider.custom.unavailable": "Les fournisseurs personnalisés ne sont pas disponibles sur ce serveur", + "provider.custom.description.prefix": "Configurez un fournisseur compatible avec OpenAI. Consultez la ", + "provider.custom.description.link": "documentation sur la configuration des fournisseurs", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID du fournisseur", + "provider.custom.field.providerID.placeholder": "monfournisseur", + "provider.custom.field.providerID.description": + "Lettres minuscules, chiffres, traits d'union ou traits de soulignement", + "provider.custom.field.name.label": "Nom d'affichage", + "provider.custom.field.name.placeholder": "Mon fournisseur IA", + "provider.custom.field.baseURL.label": "URL de base", + "provider.custom.field.baseURL.placeholder": "https://api.monfournisseur.com/v1", + "provider.custom.field.apiKey.label": "Clé API", + "provider.custom.field.apiKey.placeholder": "Clé API", + "provider.custom.field.apiKey.description": + "Facultatif. Laissez ce champ vide si vous gérez l'authentification au moyen des en-têtes.", + "provider.custom.models.label": "Modèles", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "id-modele", + "provider.custom.models.name.label": "Nom", + "provider.custom.models.name.placeholder": "Nom d'affichage", + "provider.custom.models.remove": "Supprimer le modèle", + "provider.custom.models.add": "Ajouter un modèle", + "provider.custom.headers.label": "En-têtes (optionnel)", + "provider.custom.headers.key.label": "En-tête", + "provider.custom.headers.key.placeholder": "Nom-En-Tête", + "provider.custom.headers.value.label": "Valeur", + "provider.custom.headers.value.placeholder": "valeur", + "provider.custom.headers.remove": "Supprimer l'en-tête", + "provider.custom.headers.add": "Ajouter un en-tête", + "provider.custom.error.providerID.required": "L'ID du fournisseur est requis", + "provider.custom.error.providerID.format": + "Utilisez des lettres minuscules, des chiffres, des traits d'union ou des traits de soulignement", + "provider.custom.error.providerID.exists": "Cet ID de fournisseur existe déjà", + "provider.custom.error.name.required": "Le nom d'affichage est requis", + "provider.custom.error.baseURL.required": "L'URL de base est requise", + "provider.custom.error.baseURL.format": "Doit commencer par http:// ou https://", + "provider.custom.error.required": "Requis", + "provider.custom.error.duplicate": "Doublon", + "provider.disconnect.toast.disconnected.title": "{{provider}} déconnecté", + "provider.disconnect.toast.disconnected.description": "Les modèles {{provider}} ne sont plus disponibles.", + "model.tag.free": "Gratuit", + "model.tag.latest": "Le plus récent", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "texte", + "model.input.image": "image", + "model.input.audio": "audio", + "model.input.video": "vidéo", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Formats pris en charge : {{inputs}}", + "model.tooltip.context.label": "Contexte", + "model.tooltip.inputs": "Entrées", + "model.tooltip.model": "Modèle", + "model.tooltip.provider": "Fournisseur", + "model.tooltip.reasoning": "Raisonnement", + "model.tooltip.reasoning.allowed": "Raisonnement pris en charge", + "model.tooltip.reasoning.none": "Sans raisonnement", + "model.tooltip.context": "Limite de contexte {{limit}}", + "common.search.placeholder": "Rechercher", + "common.goBack": "Retour", + "common.goForward": "Suivant", + "common.loading": "Chargement", + "common.loading.ellipsis": "...", + "common.cancel": "Annuler", + "common.clear": "Effacer", + "common.connect": "Connecter", + "common.disconnect": "Déconnecter", + "common.continue": "Continuer", + "common.submit": "Soumettre", + "common.save": "Enregistrer", + "common.saving": "Enregistrement...", + "common.default": "Par défaut", + "common.attachment": "pièce jointe", + "prompt.placeholder.shell": "Entrez une commande shell... {{example}}", + "prompt.placeholder.normal": 'Demandez n\'importe quoi... "{{example}}"', + "prompt.placeholder.simple": "Demandez n'importe quoi...", + "prompt.placeholder.summarizeComments": "Résumer les commentaires…", + "prompt.placeholder.summarizeComment": "Résumer le commentaire…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Invite", + "prompt.mode.shell.exit": "Échap pour quitter", + "session.child.promptDisabled": "Les sessions de sous-agents ne peuvent pas recevoir de messages.", + "session.child.backToParent": "Revenir à la session principale.", + "prompt.example.1": "Corriger un TODO dans la base de code", + "prompt.example.2": "Quelle est la pile technique de ce projet ?", + "prompt.example.3": "Réparer les tests échoués", + "prompt.example.4": "Expliquer comment fonctionne l'authentification", + "prompt.example.5": "Trouver et corriger les vulnérabilités de sécurité", + "prompt.example.6": "Ajouter des tests unitaires pour le service utilisateur", + "prompt.example.7": "Refactoriser cette fonction pour la rendre plus lisible", + "prompt.example.8": "Que signifie cette erreur ?", + "prompt.example.9": "Aidez-moi à déboguer ce problème", + "prompt.example.10": "Générer la documentation de l'API", + "prompt.example.11": "Optimiser les requêtes de base de données", + "prompt.example.12": "Ajouter une validation d'entrée", + "prompt.example.13": "Créer un nouveau composant pour...", + "prompt.example.14": "Comment déployer ce projet ?", + "prompt.example.15": "Vérifier que mon code respecte les bonnes pratiques", + "prompt.example.16": "Ajouter la gestion des erreurs à cette fonction", + "prompt.example.17": "Expliquer cette expression régulière", + "prompt.example.18": "Convertir ceci en TypeScript", + "prompt.example.19": "Ajouter une journalisation dans toute la base de code", + "prompt.example.20": "Quelles dépendances sont obsolètes ?", + "prompt.example.21": "Aidez-moi à écrire un script de migration", + "prompt.example.22": "Implémenter la mise en cache pour ce point de terminaison", + "prompt.example.23": "Ajouter la pagination à cette liste", + "prompt.example.24": "Créer une commande CLI pour...", + "prompt.example.25": "Comment fonctionnent les variables d'environnement ici ?", + "prompt.popover.emptyResults": "Aucun résultat correspondant", + "prompt.popover.emptyCommands": "Aucune commande correspondante", + "prompt.dropzone.label": "Déposez des images, des PDF ou des fichiers texte ici", + "prompt.dropzone.file.label": "Déposez pour @mentionner le fichier", + "prompt.slash.badge.custom": "personnalisé", + "prompt.slash.badge.skill": "compétence", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "actif", + "prompt.context.includeActiveFile": "Inclure le fichier actif", + "prompt.context.removeActiveFile": "Retirer le fichier actif du contexte", + "prompt.context.removeFile": "Retirer le fichier du contexte", + "prompt.action.attachFile": "Joindre un fichier", + "prompt.menu.addImagesAndFiles": "Ajouter des fichiers et plus encore", + "prompt.menu.imagesAndFiles": "Images et fichiers", + "prompt.menu.commands": "Commandes", + "prompt.menu.context": "Contexte", + "prompt.menu.shellCommand": "Commande shell", + "prompt.attachment.remove": "Supprimer la pièce jointe", + "prompt.action.send": "Envoyer", + "prompt.action.stop": "Arrêter", + "prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge", + "prompt.toast.attachmentDuplicate.title": "Ce fichier a déjà été téléversé", + "prompt.toast.pasteUnsupported.description": + "Seules les images, les PDF ou les fichiers texte peuvent être joints ici.", + "prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle", + "prompt.toast.modelAgentRequired.description": "Choisissez un agent et un modèle avant d'envoyer une invite.", + "prompt.toast.worktreeCreateFailed.title": "Échec de la création de l'arbre de travail", + "prompt.toast.sessionCreateFailed.title": "Échec de la création de la session", + "prompt.toast.shellSendFailed.title": "Échec de l'envoi de la commande shell", + "prompt.toast.commandSendFailed.title": "Échec de l'envoi de la commande", + "prompt.toast.promptSendFailed.title": "Échec de l'envoi de l'invite", + "prompt.toast.promptSendFailed.description": "Impossible de récupérer la session", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "MCP actifs : {{enabled}} sur {{total}}", + "dialog.mcp.empty": "Aucun MCP configuré", + "dialog.lsp.empty": "Serveurs LSP détectés automatiquement selon le type de fichier", + "dialog.plugins.empty": "Extensions configurées dans opencode.json", + "mcp.status.connected": "connecté", + "mcp.status.failed": "en échec", + "mcp.status.needs_auth": "authentification requise", + "mcp.auth.clickToAuthenticate": "Cliquez pour vous authentifier", + "mcp.status.disabled": "désactivé", + "dialog.fork.empty": "Aucun message à partir duquel bifurquer", + "dialog.directory.search.placeholder": "Rechercher des dossiers", + "dialog.directory.empty": "Aucun dossier trouvé", + "dialog.directory.action.selectFile": "Sélectionner le fichier", + "dialog.directory.action.selectFolder": "Sélectionner le dossier", + "dialog.directory.root": "Racine", + "dialog.directory.parent": "Parent", + "dialog.directory.readError": "Impossible de lire ce dossier", + "dialog.server.title": "Serveurs", + "dialog.server.description": "Changez le serveur OpenCode auquel cette application se connecte.", + "dialog.server.search.placeholder": "Rechercher des serveurs", + "dialog.server.empty": "Aucun serveur pour l'instant", + "dialog.server.add.title": "Ajouter un serveur", + "dialog.server.add.url": "URL du serveur", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Impossible de se connecter au serveur", + "dialog.server.add.checking": "Vérification...", + "dialog.server.add.button": "Ajouter un serveur", + "dialog.server.add.name": "Nom du serveur (optionnel)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Nom d'utilisateur (optionnel)", + "dialog.server.add.password": "Mot de passe (optionnel)", + "dialog.server.edit.title": "Modifier le serveur", + "dialog.server.default.title": "Serveur par défaut", + "dialog.server.default.description": + "Se connecter à ce serveur au lancement de l'application au lieu de démarrer un serveur local. Nécessite un redémarrage.", + "dialog.server.default.none": "Aucun serveur sélectionné", + "dialog.server.default.set": "Définir le serveur actuel comme défaut", + "dialog.server.default.clear": "Effacer", + "dialog.server.action.remove": "Supprimer le serveur", + "dialog.server.menu.edit": "Modifier", + "dialog.server.menu.default": "Définir par défaut", + "dialog.server.menu.defaultRemove": "Ne plus utiliser par défaut", + "dialog.server.menu.delete": "Supprimer", + "dialog.server.current": "Serveur actuel", + "dialog.server.status.default": "Défaut", + "wsl.server.add": "Ajouter un serveur WSL", + "wsl.server.addShort": "Ajouter WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "Serveur WSL", + "wsl.server.retryStart": "Réessayer de démarrer", + "wsl.server.updating": "Mise à jour...", + "wsl.onboarding.step.distro": "Choisir une distribution", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Vérification de WSL...", + "wsl.onboarding.restartRequired": "Windows doit redémarrer pour terminer l'installation de WSL.", + "wsl.onboarding.ready": "WSL est prêt.", + "wsl.onboarding.required": "WSL est requis pour continuer.", + "wsl.onboarding.checkingDistros": "Vérification des distributions...", + "wsl.onboarding.installingDistro": "Installation de {{distro}}...", + "wsl.onboarding.checkingDistro": "Vérification de {{distro}}...", + "wsl.onboarding.listingDistros": "Récupération de la liste des distributions...", + "wsl.onboarding.distroReady": "{{distro}} est prête.", + "wsl.onboarding.distroNotInstalled": "{{distro}} n'est pas encore installée.", + "wsl.onboarding.openDistroOnce": "Ouvrez {{distro}} une fois pour terminer la configuration.", + "wsl.onboarding.finishingDistro": "Finalisation de la configuration de {{distro}}.", + "wsl.onboarding.pickDistro": "Choisissez une distribution ou installez-en une ci-dessous.", + "wsl.onboarding.checkingOpencode": "Vérification d'OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Vérification d'OpenCode dans {{distro}}...", + "wsl.onboarding.updatingOpencode": "Mise à jour d'OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Mise à jour d'OpenCode dans {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Mettez à jour OpenCode dans {{distro}}.", + "wsl.onboarding.updateOpencode": "Mettre à jour OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode est prêt dans {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode est prêt.", + "wsl.onboarding.installOpencodeIn": "Installez OpenCode dans {{distro}}.", + "wsl.onboarding.installOpencode": "Installer OpenCode", + "wsl.onboarding.chooseDistroFirst": "Choisissez d'abord une distribution.", + "wsl.onboarding.loadFailed": "Impossible de charger l'état de WSL.", + "wsl.onboarding.loading": "Chargement...", + "wsl.onboarding.installedDistros": "Distributions installées", + "wsl.onboarding.checkAgain": "Vérifier à nouveau", + "wsl.onboarding.distroStatus.ready": "Prête", + "wsl.onboarding.distroStatus.checking": "Vérification...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode non installé", + "wsl.onboarding.distroStatus.missingTools": "bash et curl manquants", + "wsl.onboarding.distroStatus.unsupported": "Non prise en charge · Utilisez WSL 2", + "wsl.onboarding.needAnotherDistro": "Besoin d'une autre distribution ?", + "wsl.onboarding.needAnotherDistroHint": "Installez une distribution Linux depuis le catalogue WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL n'est pas installé", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) est requis avant qu'OpenCode puisse ajouter un serveur WSL", + "wsl.onboarding.wslUnavailable.title": "WSL indisponible", + "wsl.onboarding.wslUnavailable.description": "OpenCode n'a pas pu vérifier WSL sur cet appareil.", + "wsl.onboarding.installWsl": "Installer WSL", + "wsl.onboarding.windowsRestartRequired": + "Redémarrez Windows pour terminer l'installation de WSL, puis rouvrez OpenCode.", + "wsl.onboarding.next": "Suivant", + "wsl.onboarding.refresh": "Actualiser", + "wsl.onboarding.allDistrosAdded": "Toutes les distributions installées ont déjà été ajoutées.", + "wsl.onboarding.noDistros": "Aucune distribution détectée pour l'instant.", + "wsl.onboarding.install": "Installer", + "wsl.onboarding.installing": "Installation...", + "wsl.onboarding.installDistro": "Installer une distribution", + "wsl.onboarding.searchDistros": "Rechercher des distributions", + "wsl.onboarding.wsl2Required": "WSL 2 est requis.", + "wsl.onboarding.toolsRequired": "Cette distribution nécessite bash et curl.", + "wsl.onboarding.openTerminal": "Ouvrir le terminal", + "wsl.onboarding.path": "Chemin : {{path}}", + "wsl.onboarding.notFound": "introuvable", + "wsl.onboarding.version": "Version : {{version}}", + "wsl.onboarding.unknown": "inconnue", + "wsl.onboarding.desktopVersion": "application de bureau {{version}}", + "wsl.onboarding.versionMismatch": "La version installée ne correspond pas à celle de l'application de bureau.", + "wsl.onboarding.adding": "Ajout...", + "help.tabs.toast.ariaLabel": + "Présentation des onglets. Organisez votre travail et vos sessions actives à l'aide d'onglets", + "help.tabs.toast.dismiss": "Ignorer les informations sur les onglets", + "help.tabs.title": "Présentation des onglets", + "help.tabs.description": "Organisez votre travail et vos sessions actives à l'aide d'onglets", + "help.tabs.date": "14 juillet", + "help.tabs.introduction": "OpenCode Desktop s'articule désormais autour d'onglets.", + "help.tabs.sessions": + "Démarrez une nouvelle session dans un onglet ou ouvrez une session existante depuis l'un de vos projets. Ouvrez un nouvel onglet lorsque vous commencez une nouvelle tâche et fermez-le une fois celle-ci terminée.", + "help.tabs.organize": + "Garder quelques onglets ouverts facilite l'organisation de vos sessions actives. Si vous comptez les conserver, renommez-les pour pouvoir les reconnaître facilement.", + "help.tabs.home": + "Vous trouverez toutes vos sessions et tous vos projets sur le nouvel écran d'accueil. La sélection d'une session l'ouvre dans un onglet.", + "help.tabs.persistence": "Lorsque vous rouvrez l'application, vos onglets sont toujours ouverts.", + "help.tabs.worktrees": + "La nouvelle interface ne prend pas encore en charge les arbres de travail Git, mais cette fonctionnalité sera bientôt disponible. Si vous préférez continuer à utiliser la mise en page précédente, vous pouvez changer de mise en page dans les paramètres. N'oubliez pas que la nouvelle mise en page deviendra définitive dans quelques semaines.", + "dialog.project.edit.title": "Modifier le projet", + "dialog.project.edit.name": "Nom", + "dialog.project.edit.icon": "Icône", + "dialog.project.edit.icon.alt": "Icône du projet", + "dialog.project.edit.icon.hint": "Cliquez ou faites glisser une image", + "dialog.project.edit.icon.recommended": "Recommandé : 128x128px", + "dialog.project.edit.color": "Couleur", + "dialog.project.edit.color.select": "Sélectionner la couleur {{color}}", + "dialog.project.edit.worktree.startup": "Script de démarrage de l'espace de travail", + "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.", + "context.breakdown.system": "Système", + "context.breakdown.user": "Utilisateur", + "context.breakdown.assistant": "Assistant", + "context.breakdown.tool": "Appels d'outils", + "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", + "context.stats.model": "Modèle", + "context.stats.limit": "Limite de contexte", + "context.stats.totalTokens": "Total des jetons", + "context.stats.usage": "Utilisation", + "context.stats.inputTokens": "Jetons d'entrée", + "context.stats.outputTokens": "Jetons de sortie", + "context.stats.reasoningTokens": "Jetons de raisonnement", + "context.stats.cacheTokens": "Jetons de cache (lecture/écriture)", + "context.stats.userMessages": "Messages utilisateur", + "context.stats.assistantMessages": "Messages de l'assistant", + "context.stats.totalCost": "Coût total", + "context.stats.sessionCreated": "Session créée", + "context.stats.lastActivity": "Dernière activité", + "context.usage.tokens": "Jetons", + "context.usage.usage": "Utilisation", + "context.usage.cost": "Coût", + "context.usage.clickToView": "Cliquez pour voir le contexte", + "context.usage.view": "Voir l'utilisation du contexte", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Langue", + "toast.language.description": "Langue définie sur {{language}}", + "toast.theme.title": "Thème changé", + "toast.scheme.title": "Schéma de couleurs", + "toast.workspace.enabled.title": "Espaces de travail activés", + "toast.workspace.enabled.description": "Plusieurs arbres de travail sont désormais affichés dans la barre latérale", + "toast.workspace.disabled.title": "Espaces de travail désactivés", + "toast.workspace.disabled.description": "Seul l'arbre de travail principal est affiché dans la barre latérale", + "toast.permissions.autoaccept.on.title": "Acceptation automatique des permissions", + "toast.permissions.autoaccept.on.description": "Les demandes de permission seront approuvées automatiquement", + "toast.permissions.autoaccept.off.title": "Acceptation automatique des permissions désactivée", + "toast.permissions.autoaccept.off.description": "Les demandes de permission nécessiteront une approbation", + "toast.model.none.title": "Aucun modèle sélectionné", + "toast.model.none.description": "Connectez un fournisseur pour résumer cette session", + "toast.file.loadFailed.title": "Échec du chargement du fichier", + "toast.file.listFailed.title": "Échec de la liste des fichiers", + "toast.context.noLineSelection.title": "Aucune sélection de lignes", + "toast.context.noLineSelection.description": "Sélectionnez d'abord une plage de lignes dans un onglet de fichier.", + "toast.session.share.copyFailed.title": "Échec de la copie de l'URL dans le presse-papiers", + "toast.session.share.success.title": "Session partagée", + "toast.session.share.success.description": "URL de partage copiée dans le presse-papiers !", + "toast.session.share.failed.title": "Échec du partage de la session", + "toast.session.share.failed.description": "Une erreur s'est produite lors du partage de la session", + "toast.session.unshare.success.title": "Partage de la session désactivé", + "toast.session.unshare.success.description": "Le partage de la session a bien été désactivé.", + "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": + "Une nouvelle version d'OpenCode ({{version}}) est maintenant disponible pour installation.", + "toast.update.action.installRestart": "Installer et redémarrer", + "toast.update.action.notYet": "Plus tard", + "error.page.title": "Un problème est survenu", + "error.page.description": "Une erreur s'est produite lors du chargement de l'application.", + "error.page.description.localServerStartup": "Une erreur s'est produite lors du démarrage du serveur local.", + "error.page.details.label": "Détails de l'erreur", + "error.page.action.restart": "Redémarrer", + "error.page.action.report": "Signaler l'erreur", + "error.page.action.reported": "Erreur signalée", + "error.page.action.exportLogs": "Exporter les journaux", + "error.page.action.checking": "Vérification...", + "error.page.action.checkUpdates": "Vérifier les mises à jour", + "error.page.action.updateTo": "Mettre à jour vers {{version}}", + "error.page.report.prefix": "Veuillez signaler cette erreur à l'équipe OpenCode", + "error.page.report.discord": "sur Discord", + "error.page.version": "Version : {{version}}", + "error.dev.rootNotFound": + "Élément racine introuvable. Avez-vous oublié de l'ajouter à votre index.html ? Ou peut-être que l'attribut id est mal orthographié ?", + "error.serverSync.connectFailed": + "Impossible de se connecter au serveur. Y a-t-il un serveur en cours d'exécution à `{{url}}` ?", + "directory.error.invalidUrl": "Répertoire invalide dans l'URL.", + "error.chain.unknown": "Erreur inconnue", + "error.chain.causedBy": "Causé par :", + "error.chain.apiError": "Erreur API", + "error.chain.status": "Statut : {{status}}", + "error.chain.retryable": "Nouvelle tentative possible : {{retryable}}", + "error.chain.responseBody": "Corps de la réponse :\n{{body}}", + "error.chain.didYouMean": "Vouliez-vous dire : {{suggestions}}", + "error.chain.modelNotFound": "Modèle introuvable : {{provider}}/{{model}}", + "error.chain.checkConfig": "Vérifiez les noms de fournisseur et de modèle dans votre configuration (opencode.json)", + "error.chain.mcpFailed": + "Le serveur MCP \"{{name}}\" a échoué. Notez qu'OpenCode ne prend pas encore en charge l'authentification MCP.", + "error.chain.providerAuthFailed": "Échec de l'authentification du fournisseur ({{provider}}) : {{message}}", + "error.chain.providerInitFailed": + 'Échec de l\'initialisation du fournisseur "{{provider}}". Vérifiez les identifiants et la configuration.', + "error.chain.configJsonInvalid": "Le fichier de configuration à {{path}} n'est pas un JSON(C) valide", + "error.chain.configJsonInvalidWithMessage": + "Le fichier de configuration à {{path}} n'est pas un JSON(C) valide : {{message}}", + "error.chain.configDirectoryTypo": + 'Le répertoire "{{dir}}" dans {{path}} n\'est pas valide. Renommez le répertoire en "{{suggestion}}" ou supprimez-le. C\'est une faute de frappe courante.', + "error.chain.configFrontmatterError": "Échec de l'analyse de l'en-tête dans {{path}} :\n{{message}}", + "error.chain.configInvalid": "Le fichier de configuration à {{path}} est invalide", + "error.chain.configInvalidWithMessage": "Le fichier de configuration à {{path}} est invalide : {{message}}", + "notification.permission.title": "Permission requise", + "notification.permission.description": "{{sessionTitle}} dans {{projectName}} a besoin d'une permission", + "notification.question.title": "Question", + "notification.question.description": "{{sessionTitle}} dans {{projectName}} a une question", + "notification.action.goToSession": "Aller à la session", + "notification.session.responseReady.title": "Réponse prête", + "notification.session.error.title": "Erreur de session", + "notification.session.error.fallbackDescription": "Une erreur s'est produite", + "home.recentProjects": "Projets récents", + "home.empty.title": "Aucun projet récent", + "home.empty.description": "Commencez par ouvrir un projet local", + "home.title": "Accueil", + "home.projects": "Projets", + "home.project.add": "Ajouter un projet", + "home.recentlyClosed": "Récemment fermés", + "home.server.collapse": "Réduire les projets du serveur", + "home.server.expand": "Développer les projets du serveur", + "home.sessions.search.placeholder": "Rechercher des sessions", + "home.sessions.search.placeholder.scoped": "Rechercher les sessions dans {{scope}}", + "home.sessions.search.sessions": "Sessions", + "home.sessions.search.noResults": "Aucune session trouvée pour {{query}}", + "home.sessions.empty": "Rien ici pour l'instant", + "home.sessions.empty.description": "Créez une session pour commencer", + "home.sessions.group.today": "Aujourd'hui", + "home.sessions.group.yesterday": "Hier", + "home.sessions.group.older": "Plus anciennes", + "home.providerTip": + "Connectez-vous à plus de 75 fournisseurs pour utiliser d’autres modèles, notamment Claude, GPT, Gemini, etc.", + "session.tab.session": "Session", + "session.tab.review": "Revue", + "session.tab.context": "Contexte", + "session.tab.unknown": "Session inconnue", + "session.panel.reviewAndFiles": "Revue et fichiers", + "session.error.notFound": "Cette session est introuvable", + "session.error.notFound.description": "Cet onglet pointe vers une session qui n'existe plus sur ce serveur.", + "session.error.notFound.closeTab": "Fermer l'onglet", + "session.error.serverConnection": "Impossible de se connecter à ce serveur", + "session.review.filesChanged": "{{count}} fichiers modifiés", + "session.review.change.one": "Modification", + "session.review.change.other": "Modifications", + "session.review.loadingChanges": "Chargement des modifications...", + "session.review.empty": "Aucune modification dans cette session pour l'instant", + "session.review.noChanges": "Aucune modification", + "session.review.noUncommittedChanges": "Aucune modification non validée pour l'instant", + "session.review.noBranchChanges": "Aucune modification de branche pour l'instant", + "session.review.noVcs": "Aucun système de gestion de versions Git détecté ; modifications non affichées", + "session.review.noSnapshot": + "Le suivi des instantanés est désactivé dans la configuration, les modifications de session sont donc indisponibles", + "session.files.selectToOpen": "Sélectionnez un fichier à ouvrir", + "session.files.all": "Tous les fichiers", + "session.files.empty": "Aucun fichier", + "session.files.binaryContent": "Fichier binaire (le contenu ne peut pas être affiché)", + "session.messages.renderEarlier": "Afficher les messages précédents", + "session.messages.loadingEarlier": "Chargement des messages précédents...", + "session.messages.loadEarlier": "Charger les messages précédents", + "session.messages.loading": "Chargement des messages...", + "session.messages.jumpToLatest": "Aller au message le plus récent", + "session.context.addToContext": "Ajouter {{selection}} au contexte", + "session.todo.title": "Tâches", + "session.todo.collapse": "Réduire", + "session.todo.expand": "Développer", + "session.question.minimize": "Réduire la question", + "session.question.restore": "Restaurer la question", + "session.question.pending.one": "{{count}} question en attente", + "session.question.pending.many": "{{count}} de questions en attente", + "session.question.pending.other": "{{count}} questions en attente", + "session.followupDock.summary.one": "{{count}} message en file d'attente", + "session.followupDock.summary.many": "{{count}} de messages en file d'attente", + "session.followupDock.summary.other": "{{count}} messages en file d'attente", + "session.followupDock.sendNow": "Envoyer maintenant", + "session.followupDock.edit": "Modifier", + "session.followupDock.collapse": "Réduire les messages en file d'attente", + "session.followupDock.expand": "Développer les messages en file d'attente", + "session.revertDock.summary.one": "{{count}} message annulé", + "session.revertDock.summary.many": "{{count}} de messages annulés", + "session.revertDock.summary.other": "{{count}} messages annulés", + "session.revertDock.collapse": "Réduire les messages annulés", + "session.revertDock.expand": "Développer les messages annulés", + "session.revertDock.restore": "Restaurer le message", + "session.new.title": "Créez ce que vous voulez", + "session.new.project.new": "Nouveau projet", + "session.new.project.search": "Rechercher des projets", + "session.new.project.add": "Ajouter un projet", + "session.new.worktree.main": "Branche principale", + "session.new.worktree.mainWithBranch": "Branche principale ({{branch}})", + "session.new.worktree.create": "Créer un nouvel arbre de travail", + "session.new.workspace.runIn": "Exécuter la session dans", + "session.new.workspace.triggerLocal": "Local", + "session.new.workspace.local": "Dépôt local", + "session.new.workspace.existing": "Espace de travail…", + "session.new.git.none": "Pas de Git", + "session.new.lastModified": "Dernière modification", + "session.header.search.placeholder": "Rechercher {{project}}", + "session.header.searchFiles": "Rechercher des fichiers", + "session.header.openIn": "Ouvrir dans", + "session.header.open.action": "Ouvrir {{app}}", + "session.header.open.ariaLabel": "Ouvrir dans {{app}}", + "session.header.open.menu": "Options d'ouverture", + "session.header.open.copyPath": "Copier le chemin", + "status.popover.trigger": "Statut", + "status.popover.ariaLabel": "Configuration des serveurs", + "status.popover.tab.servers": "Serveurs", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugins", + "status.popover.action.manageServers": "Gérer les serveurs", + "session.share.popover.title": "Publier sur le Web", + "session.share.popover.description.shared": + "Cette session est publique sur le Web. Elle est accessible à toute personne disposant du lien.", + "session.share.popover.description.unshared": + "Rendre la session publique sur le Web. Elle sera accessible à toute personne disposant du lien.", + "session.share.action.share": "Partager", + "session.share.action.publish": "Publier", + "session.share.action.publishing": "Publication...", + "session.share.action.unpublish": "Dépublier", + "session.share.action.unpublishing": "Dépublication...", + "session.share.action.view": "Voir", + "session.share.copy.copied": "Copié", + "session.share.copy.copyLink": "Copier le lien", + "lsp.tooltip.none": "Aucun serveur LSP", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Chargement de l'invite...", + "terminal.loading": "Chargement du terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Fermer le terminal", + "terminal.connectionLost.title": "Connexion perdue", + "terminal.connectionLost.description": + "La connexion au terminal a été interrompue. Cela peut arriver lorsque le serveur redémarre.", + "common.closeTab": "Fermer l'onglet", + "common.dismiss": "Ignorer", + "common.requestFailed": "La demande a échoué", + "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", + "common.close": "Fermer", + "common.edit": "Modifier", + "common.loadMore": "Charger plus", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "Basculer le menu", + "sidebar.nav.projectsAndSessions": "Projets et sessions", + "sidebar.settings": "Paramètres", + "sidebar.help": "Aide", + "sidebar.workspaces.enable": "Activer les espaces de travail", + "sidebar.workspaces.disable": "Désactiver les espaces de travail", + "sidebar.gettingStarted.title": "Commencer", + "sidebar.gettingStarted.line1": + "OpenCode inclut des modèles gratuits pour que vous puissiez commencer immédiatement.", + "sidebar.gettingStarted.line2": + "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.", + "sidebar.project.recentSessions": "Sessions récentes", + "sidebar.project.viewAllSessions": "Voir toutes les sessions", + "sidebar.project.clearNotifications": "Effacer les notifications", + "sidebar.empty.title": "Aucun projet ouvert", + "sidebar.empty.description": "Ouvrez un projet pour commencer", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Application de bureau", + "settings.section.server": "Serveur", + "settings.tab.general": "Général", + "settings.tab.shortcuts": "Raccourcis", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Intégration WSL", + "settings.desktop.wsl.description": "Exécuter le serveur OpenCode dans WSL sur Windows.", + "settings.general.section.appearance": "Apparence", + "settings.general.section.advanced": "Avancé", + "settings.general.section.notifications": "Notifications système", + "settings.general.section.updates": "Mises à jour", + "settings.general.section.sounds": "Effets sonores", + "settings.general.section.feed": "Flux", + "settings.general.section.display": "Affichage", + "settings.general.row.language.title": "Langue", + "settings.general.row.language.description": "Changer la langue d'affichage pour OpenCode", + "settings.general.row.shell.title": "Interpréteur de commandes du terminal", + "settings.general.row.shell.description": + "Choisissez le shell utilisé par votre terminal. Les shells compatibles sont également utilisés pour les appels d'outils de l'agent.", + "settings.general.row.shell.autoDefault": "Automatique (par défaut)", + "settings.general.row.shell.terminalOnly": "terminal uniquement", + "settings.general.row.appearance.title": "Apparence", + "settings.general.row.appearance.description": "Personnaliser l'apparence d'OpenCode sur votre appareil", + "settings.general.row.colorScheme.title": "Schéma de couleurs", + "settings.general.row.colorScheme.description": "Choisissez si OpenCode suit le thème système, clair ou sombre", + "settings.general.row.theme.title": "Thème", + "settings.general.row.theme.description": "Personnaliser le thème d'OpenCode.", + "settings.general.row.font.title": "Police de code", + "settings.general.row.font.description": "Personnaliser la police utilisée dans les blocs de code", + "settings.general.row.terminalFont.title": "Police du terminal", + "settings.general.row.terminalFont.description": "Personnalisez la police utilisée dans le terminal", + "settings.general.row.uiFont.title": "Police de l'interface", + "settings.general.row.uiFont.description": "Personnaliser la police utilisée dans toute l'interface", + "settings.general.row.followup.title": "Comportement des messages de suivi", + "settings.general.row.followup.description": + "Choisissez si les messages de suivi orientent immédiatement l'agent ou sont placés dans une file d'attente", + "settings.general.row.followup.option.queue": "File d'attente", + "settings.general.row.followup.option.steer": "Orienter", + "settings.general.row.showFileTree.title": "Arborescence des fichiers", + "settings.general.row.showFileTree.description": "Afficher le panneau d'arborescence des fichiers dans les sessions", + "settings.general.row.showNavigation.title": "Commandes de navigation", + "settings.general.row.showNavigation.description": + "Afficher les boutons précédent et suivant dans la barre de titre de l'application de bureau", + "settings.general.row.showSearch.title": "Palette de commandes", + "settings.general.row.showSearch.description": + "Afficher le bouton de recherche et de palette de commandes dans la barre de titre", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": + "Afficher le bouton du terminal dans la barre de titre de l'application de bureau", + "settings.general.row.showStatus.title": "État du serveur", + "settings.general.row.showStatus.description": "Afficher le bouton d'état du serveur dans la barre de titre", + "settings.general.row.mobileTitlebarBottom.title": "Navigation en bas", + "settings.general.row.mobileTitlebarBottom.description": + "Placer la barre de titre et les onglets de session en bas de l'écran sur mobile", + "settings.general.row.showCustomAgents.title": "Agents personnalisés", + "settings.general.row.showCustomAgents.description": "Afficher le sélecteur d'agent dans la zone de saisie", + "settings.general.row.reasoningSummaries.title": "Afficher les résumés de raisonnement", + "settings.general.row.reasoningSummaries.description": + "Afficher les résumés de raisonnement du modèle dans la chronologie", + "settings.general.row.shellToolPartsExpanded.title": "Développer les parties de l'outil shell", + "settings.general.row.shellToolPartsExpanded.description": + "Afficher les parties de l'outil shell développées par défaut dans la chronologie", + "settings.general.row.editToolPartsExpanded.title": "Développer les parties de l'outil edit", + "settings.general.row.editToolPartsExpanded.description": + "Afficher les parties des outils edit, write et patch développées par défaut dans la chronologie", + "settings.general.row.newInterface.title": "Nouvelle mise en page", + "settings.general.row.newInterface.badge": "Nouveau", + "settings.general.row.newInterface.description": + "Utilisez les nouveaux onglets et la mise en page de l'accueil. Vous pouvez alterner entre les mises en page pendant une durée limitée.", + "settings.general.row.newInterfaceNotice.title": "Vous utilisez maintenant la nouvelle mise en page", + "settings.general.row.newInterfaceNotice.description": "La mise en page précédente n'est plus disponible", + "settings.general.row.newInterfaceNotice.dismiss": "Ignorer", + "settings.general.row.pinchZoom.title": "Pincer pour zoomer", + "settings.general.row.pinchZoom.description": + "Autoriser les gestes de pincement du pavé tactile et Ctrl-défilement pour zoomer", + "settings.general.row.wayland.title": "Utiliser Wayland natif", + "settings.general.row.wayland.description": "Désactiver le repli X11 sur Wayland. Nécessite un redémarrage.", + "settings.general.row.wayland.tooltip": + "Sur Linux avec des moniteurs à taux de rafraîchissement mixte, Wayland natif peut être plus stable.", + "settings.general.row.releaseNotes.title": "Notes de version", + "settings.general.row.releaseNotes.description": + "Afficher les fenêtres contextuelles « Quoi de neuf » après les mises à jour", + "settings.updates.row.startup.title": "Vérifier les mises à jour au démarrage", + "settings.updates.row.startup.description": "Vérifier automatiquement les mises à jour au lancement d'OpenCode", + "settings.updates.row.check.title": "Vérifier les mises à jour", + "settings.updates.row.check.description": + "Rechercher manuellement les mises à jour et installer toute mise à jour disponible", + "settings.updates.action.checkNow": "Vérifier maintenant", + "settings.updates.action.checking": "Vérification...", + "settings.updates.action.downloading": "Téléchargement...", + "settings.updates.action.installing": "Installation...", + "settings.updates.toast.latest.title": "Vous êtes à jour", + "settings.updates.toast.latest.description": "Vous utilisez la dernière version d'OpenCode.", + "sound.option.none": "Aucun", + "sound.option.alert01": "Alerte 01", + "sound.option.alert02": "Alerte 02", + "sound.option.alert03": "Alerte 03", + "sound.option.alert04": "Alerte 04", + "sound.option.alert05": "Alerte 05", + "sound.option.alert06": "Alerte 06", + "sound.option.alert07": "Alerte 07", + "sound.option.alert08": "Alerte 08", + "sound.option.alert09": "Alerte 09", + "sound.option.alert10": "Alerte 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Non 01", + "sound.option.nope02": "Non 02", + "sound.option.nope03": "Non 03", + "sound.option.nope04": "Non 04", + "sound.option.nope05": "Non 05", + "sound.option.nope06": "Non 06", + "sound.option.nope07": "Non 07", + "sound.option.nope08": "Non 08", + "sound.option.nope09": "Non 09", + "sound.option.nope10": "Non 10", + "sound.option.nope11": "Non 11", + "sound.option.nope12": "Non 12", + "sound.option.yup01": "Oui 01", + "sound.option.yup02": "Oui 02", + "sound.option.yup03": "Oui 03", + "sound.option.yup04": "Oui 04", + "sound.option.yup05": "Oui 05", + "sound.option.yup06": "Oui 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Afficher une notification système lorsque l'agent a terminé ou nécessite une attention", + "settings.general.notifications.permissions.title": "Permissions", + "settings.general.notifications.permissions.description": + "Afficher une notification système lorsqu'une permission est requise", + "settings.general.notifications.errors.title": "Erreurs", + "settings.general.notifications.errors.description": "Afficher une notification système lorsqu'une erreur se produit", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Jouer un son lorsque l'agent a terminé ou nécessite une attention", + "settings.general.sounds.permissions.title": "Permissions", + "settings.general.sounds.permissions.description": "Jouer un son lorsqu'une permission est requise", + "settings.general.sounds.errors.title": "Erreurs", + "settings.general.sounds.errors.description": "Jouer un son lorsqu'une erreur se produit", + "settings.shortcuts.title": "Raccourcis clavier", + "settings.shortcuts.reset.button": "Rétablir les valeurs par défaut", + "settings.shortcuts.reset.toast.title": "Raccourcis réinitialisés", + "settings.shortcuts.reset.toast.description": "Les raccourcis clavier ont été réinitialisés aux valeurs par défaut.", + "settings.shortcuts.conflict.title": "Raccourci déjà utilisé", + "settings.shortcuts.conflict.description": "{{keybind}} est déjà attribué à {{titles}}.", + "settings.shortcuts.unassigned": "Non attribué", + "settings.shortcuts.pressKeys": "Appuyez sur les touches", + "settings.shortcuts.search.placeholder": "Rechercher des raccourcis", + "settings.shortcuts.search.empty": "Aucun raccourci trouvé", + "settings.shortcuts.group.general": "Général", + "settings.shortcuts.group.session": "Session", + "settings.shortcuts.group.navigation": "Navigation", + "settings.shortcuts.group.modelAndAgent": "Modèle et agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Invite", + "settings.providers.title": "Fournisseurs", + "settings.providers.description": "Les paramètres des fournisseurs seront configurables ici.", + "settings.providers.section.connected": "Fournisseurs connectés", + "settings.providers.connected.empty": "Aucun fournisseur connecté", + "settings.providers.section.popular": "Fournisseurs populaires", + "settings.providers.tag.environment": "Environnement", + "settings.providers.tag.config": "Configuration", + "settings.providers.tag.custom": "Personnalisé", + "settings.providers.tag.other": "Autre", + "settings.models.title": "Modèles", + "settings.models.description": "Les paramètres des modèles seront configurables ici.", + "settings.agents.title": "Agents", + "settings.agents.description": "Les paramètres des agents seront configurables ici.", + "settings.commands.title": "Commandes", + "settings.commands.description": "Les paramètres des commandes seront configurables ici.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Les paramètres MCP seront configurables ici.", + "settings.permissions.title": "Permissions", + "settings.permissions.description": "Contrôlez les outils que le serveur peut utiliser par défaut.", + "settings.permissions.section.tools": "Outils", + "settings.permissions.toast.updateFailed.title": "Échec de la mise à jour des permissions", + "settings.permissions.action.allow": "Autoriser", + "settings.permissions.action.ask": "Demander", + "settings.permissions.action.deny": "Refuser", + "settings.permissions.tool.read.title": "Lire", + "settings.permissions.tool.read.description": "Lecture d'un fichier (correspond au chemin du fichier)", + "settings.permissions.tool.edit.title": "Modifier", + "settings.permissions.tool.edit.description": + "Modifier des fichiers, notamment au moyen des outils edit, write et patch", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Rechercher des fichiers à l'aide de motifs glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": + "Rechercher dans le contenu des fichiers à l'aide d'expressions régulières", + "settings.permissions.tool.list.title": "Lister", + "settings.permissions.tool.list.description": "Lister les fichiers dans un répertoire", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Exécuter des commandes shell", + "settings.permissions.tool.task.title": "Tâche", + "settings.permissions.tool.task.description": "Lancer des sous-agents", + "settings.permissions.tool.skill.title": "Compétence", + "settings.permissions.tool.skill.description": "Charger une compétence par son nom", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Exécuter des requêtes de serveur de langage", + "settings.permissions.tool.todowrite.title": "Écrire les tâches", + "settings.permissions.tool.todowrite.description": "Mettre à jour la liste de tâches", + "settings.permissions.tool.webfetch.title": "Récupération Web", + "settings.permissions.tool.webfetch.description": "Récupérer le contenu d'une URL", + "settings.permissions.tool.websearch.title": "Recherche Web", + "settings.permissions.tool.websearch.description": "Rechercher sur le Web", + "settings.permissions.tool.external_directory.title": "Répertoire externe", + "settings.permissions.tool.external_directory.description": "Accéder aux fichiers en dehors du répertoire du projet", + "settings.permissions.tool.doom_loop.title": "Boucle infernale", + "settings.permissions.tool.doom_loop.description": "Détecter les appels d'outils répétés avec une entrée identique", + "session.delete.failed.title": "Échec de la suppression de la session", + "session.delete.title": "Supprimer la session", + "session.delete.confirm": 'Supprimer la session "{{name}}" ?', + "session.delete.button": "Supprimer la session", + "workspace.new": "Nouvel espace de travail", + "workspace.type.local": "local", + "workspace.type.sandbox": "bac à sable", + "workspace.create.failed.title": "Échec de la création de l'espace de travail", + "workspace.delete.failed.title": "Échec de la suppression de l'espace de travail", + "workspace.resetting.title": "Réinitialisation de l'espace de travail", + "workspace.resetting.description": "Cela peut prendre une minute.", + "workspace.reset.failed.title": "Échec de la réinitialisation de l'espace de travail", + "workspace.reset.success.title": "Espace de travail réinitialisé", + "workspace.reset.success.description": "L'espace de travail correspond maintenant à la branche par défaut.", + "workspace.error.stillPreparing": "L'espace de travail est encore en cours de préparation", + "workspace.status.checking": "Vérification des modifications non fusionnées...", + "workspace.status.error": "Impossible de vérifier l'état de Git.", + "workspace.status.clean": "Aucune modification non fusionnée détectée.", + "workspace.status.dirty": "Modifications non fusionnées détectées dans cet espace de travail.", + "workspace.delete.title": "Supprimer l'espace de travail", + "workspace.delete.confirm": 'Supprimer l\'espace de travail "{{name}}" ?', + "workspace.delete.button": "Supprimer l'espace de travail", + "workspace.reset.title": "Réinitialiser l'espace de travail", + "workspace.reset.confirm": 'Réinitialiser l\'espace de travail "{{name}}" ?', + "workspace.reset.button": "Réinitialiser l'espace de travail", + "workspace.reset.archived.none": "Aucune session active ne sera archivée.", + "workspace.reset.archived.one": "1 session sera archivée.", + "workspace.reset.archived.many": "{{count}} sessions seront archivées.", + "workspace.reset.note": "Cela réinitialisera l'espace de travail pour correspondre à la branche par défaut.", + "common.open": "Ouvrir", + "dialog.releaseNotes.action.getStarted": "Commencer", + "dialog.releaseNotes.action.next": "Suivant", + "dialog.releaseNotes.action.hideFuture": "Ne plus afficher à l'avenir", + "dialog.releaseNotes.media.alt": "Aperçu de la version", + "toast.project.reloadFailed.title": "Échec du rechargement de {{project}}", + "error.server.invalidConfiguration": "Configuration invalide", + "common.moreCountSuffix": " (+{{count}} de plus)", + "common.time.justNow": "À l'instant", + "common.time.minutesAgo.short": "il y a {{count}} min", + "common.time.hoursAgo.short": "il y a {{count}} h", + "common.time.daysAgo.short": "il y a {{count}} j", + "settings.providers.connected.environmentDescription": "Connecté à partir de vos variables d'environnement", + "settings.providers.custom.description": "Ajouter un fournisseur compatible avec OpenAI via l'URL de base.", + + "app.server.unreachable": "Impossible de joindre {{server}}", + "app.server.retrying": "Nouvelle tentative automatique...", + "app.server.otherServers": "Autres serveurs", + "dialog.server.add.usernamePlaceholder": "nom d'utilisateur", + "dialog.server.add.passwordPlaceholder": "mot de passe", + "server.row.noUsername": "aucun nom d'utilisateur", + "session.review.noVcs.createGit.title": "Créer un dépôt Git", + "session.review.noVcs.createGit.description": "Suivre, examiner et annuler les modifications dans ce projet", + "session.review.noVcs.createGit.actionLoading": "Création du dépôt Git...", + "session.review.noVcs.createGit.action": "Créer un dépôt Git", + "session.todo.progress": "{{done}} tâches sur {{total}} terminées", + "session.question.progress": "{{current}} questions sur {{total}}", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Explorateur de fichiers", + "session.header.reveal.finder": "Afficher dans le Finder", + "session.header.reveal.fileExplorer": "Afficher dans l’Explorateur de fichiers", + "session.header.reveal.containingFolder": "Ouvrir le dossier contenant", + "session.header.open.fileManager": "Gestionnaire de fichiers", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Diagnostics de performance de développement", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Dernière transition de route terminée touchant une page de session, mesurée du début du routeur jusqu'au premier affichage après stabilisation.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Fréquence d'images calculée sur les 5 dernières secondes.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Durée d'image maximale sur les 5 dernières secondes.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Images de plus de 32 ms au cours des 5 dernières secondes.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Temps bloqué et nombre de tâches longues au cours des 5 dernières secondes. Tâche max : {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Pire délai d'entrée observé au cours des 5 dernières secondes.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Durée approximative d'interaction au cours des 5 dernières secondes. Il s'agit d'une mesure similaire à l'INP, et non de l'INP officiel des Web Vitals.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Décalage cumulatif de la mise en page pour la durée de vie actuelle de l'application.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Tas JS utilisé vs limite de tas. Chromium uniquement.", + "debugBar.mem.tip": "Tas JS utilisé vs limite de tas. {{used}} sur {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Forcer les styles de focus sur tous les éléments interactifs", + "debugBar.focus.on": "ACTIVÉ", + "debugBar.focus.off": "DÉSACTIVÉ", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Forcer le sens de disposition de toute l’application sans changer la langue sélectionnée", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Maj", + "common.key.meta": "Méta", + "common.key.space": "Espace", + "common.key.backspace": "Retour arrière", + "common.key.enter": "Entrée", + "common.key.tab": "Tab", + "common.key.delete": "Suppr", + "common.key.home": "Début", + "common.key.end": "Fin", + "common.key.pageUp": "Page précédente", + "common.key.pageDown": "Page suivante", + "common.key.insert": "Inser", + "common.unknown": "inconnu", + "error.page.circular": "[Circulaire]", + "error.serverSDK.noServerAvailable": "Aucun serveur disponible", + "error.serverSDK.serverNotAvailable": "Serveur non disponible", + "error.childStore.persistedCacheCreateFailed": "Échec de la création du cache persistant", + "error.childStore.persistedProjectMetadataCreateFailed": + "Échec de la création des métadonnées de projet persistantes", + "error.childStore.persistedProjectIconCreateFailed": "Échec de la création de l'icône de projet persistante", + "error.childStore.storeCreateFailed": "Échec de la création du stockage", + "terminal.connectionLost.abnormalClose": "WebSocket fermé anormalement : {{code}}", + "terminal.connectTicket.csrfError": + "Le ticket de connexion PTY a été rejeté par les contrôles d'origine ou CSRF. Vérifiez la configuration CORS du serveur.", + "terminal.connectTicket.statusError": "Le ticket de connexion PTY a échoué avec le statut {{status}}", + + "titlebar.update": "Mettre à jour", + "titlebar.updateVersion": "Mettre à jour {{version}}", +} diff --git a/packages/app/src/i18n/hi.ts b/packages/app/src/i18n/hi.ts new file mode 100644 index 0000000000000000000000000000000000000000..0aaf8e14dc6dc3a044c42d65c55ff4645c820c1f --- /dev/null +++ b/packages/app/src/i18n/hi.ts @@ -0,0 +1,1163 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "फ़ाइल", + "desktop.menu.edit": "संपादित करें", + "desktop.menu.view": "दृश्य", + "desktop.menu.go": "जाएँ", + "desktop.menu.window": "विंडो", + "desktop.menu.help": "सहायता", + "desktop.menu.checkForUpdates": "अपडेट की जाँच करें...", + "desktop.menu.settings": "सेटिंग्स", + "desktop.menu.reloadWebview": "वेबव्यू पुनः लोड करें", + "desktop.menu.restart": "पुनः आरंभ करें", + "desktop.menu.exportLogs": "लॉग निर्यात करें...", + "desktop.menu.newSession": "नया सेशन", + "desktop.menu.openProject": "प्रोजेक्ट खोलें...", + "desktop.menu.newWindow": "नई विंडो", + "desktop.menu.closeWindow": "विंडो बंद करें", + "desktop.menu.undo": "पूर्ववत करें", + "desktop.menu.redo": "फिर से करें", + "desktop.menu.cut": "काटें", + "desktop.menu.copy": "कॉपी करें", + "desktop.menu.paste": "चिपकाएँ", + "desktop.menu.delete": "हटाएँ", + "desktop.menu.selectAll": "सभी चुनें", + "desktop.menu.toggleSidebar": "साइडबार टॉगल करें", + "desktop.menu.toggleTerminal": "टर्मिनल टॉगल करें", + "desktop.menu.toggleFileTree": "फ़ाइल ट्री टॉगल करें", + "desktop.menu.reload": "पुनः लोड करें", + "desktop.menu.toggleDeveloperTools": "डेवलपर टूल टॉगल करें", + "desktop.menu.actualSize": "वास्तविक आकार", + "desktop.menu.zoomIn": "ज़ूम इन करें", + "desktop.menu.zoomOut": "ज़ूम आउट करें", + "desktop.menu.toggleFullScreen": "पूर्ण स्क्रीन टॉगल करें", + "desktop.menu.back": "पीछे", + "desktop.menu.forward": "आगे", + "desktop.menu.previousSession": "पिछला सेशन", + "desktop.menu.nextSession": "अगला सेशन", + "desktop.menu.previousProject": "पिछला प्रोजेक्ट", + "desktop.menu.nextProject": "अगला प्रोजेक्ट", + "desktop.menu.minimize": "छोटा करें", + "desktop.menu.maximize": "बड़ा करें", + "desktop.menu.documentation": "OpenCode दस्तावेज़", + "desktop.menu.supportForum": "सहायता फ़ोरम", + "desktop.menu.shareFeedback": "प्रतिक्रिया साझा करें", + "desktop.menu.reportBug": "बग रिपोर्ट करें", + "desktop.menu.ariaLabel": "OpenCode मेन्यू", + + "desktop.updater.dialog.checkFailed.message": "अपडेट की जाँच विफल रही।", + "desktop.updater.dialog.checkFailed.title": "अपडेट त्रुटि", + "desktop.updater.dialog.upToDate.message": "आप नवीनतम संस्करण का उपयोग कर रहे हैं।", + "desktop.updater.dialog.upToDate.title": "कोई अपडेट नहीं", + "desktop.updater.dialog.ready.message": "अपडेट {{version}} डाउनलोड हो गया है। अभी पुनः आरंभ करें?", + "desktop.updater.dialog.ready.title": "अपडेट तैयार है", + "desktop.updater.dialog.restart": "पुनः आरंभ करें", + "desktop.updater.dialog.later": "बाद में", + + "desktop.recovery.action.relaunch": "पुनः लॉन्च करें", + "desktop.recovery.action.exportLogs": "लॉग निर्यात करें", + "desktop.recovery.action.keepWaiting": "प्रतीक्षा जारी रखें", + "desktop.recovery.action.quit": "बाहर निकलें", + "desktop.recovery.loadFailed": "OpenCode लोड नहीं हो सका", + "desktop.recovery.terminated": "OpenCode विंडो अनपेक्षित रूप से बंद हो गई", + "desktop.recovery.unresponsive": "OpenCode प्रतिसाद नहीं दे रहा है", + "desktop.recovery.unresponsive.detail": + "आप ऐप को पुनः लॉन्च कर सकते हैं, लॉग खोल सकते हैं या प्रतीक्षा जारी रख सकते हैं।", + "desktop.recovery.loadFailed.detail": "विंडो: {{window}}\nURL: {{url}}\nत्रुटि: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "विंडो: {{window}}\nकारण: {{reason}}\nकोड: {{code}}", + "desktop.recovery.unknown": "<अज्ञात>", + + "desktop.dialog.chooseFolder": "फ़ोल्डर चुनें", + "desktop.dialog.chooseFile": "फ़ाइल चुनें", + "desktop.dialog.saveFile": "फ़ाइल सहेजें", + "desktop.dialog.files": "फ़ाइलें", + "desktop.server.local": "स्थानीय सर्वर", + + "desktop.wsl.error.windowsOnly": "WSL केवल Windows पर उपलब्ध है", + "desktop.wsl.error.unavailable": "WSL उपलब्ध नहीं है", + "desktop.wsl.error.listInstalled": "इंस्टॉल किए गए WSL डिस्ट्रो की सूची नहीं बनाई जा सकी", + "desktop.wsl.error.listOnline": "ऑनलाइन WSL डिस्ट्रो की सूची नहीं बनाई जा सकी", + "desktop.wsl.error.executeDistro": "डिस्ट्रो में कमांड निष्पादित नहीं किए जा सकते", + "desktop.wsl.error.installWsl": "WSL इंस्टॉल नहीं हो सका", + "desktop.wsl.error.installDistro": "डिस्ट्रो इंस्टॉल नहीं हो सका: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode इंस्टॉल नहीं हो सका", + "desktop.wsl.error.alreadyAdded": "{{distro}} पहले ही जोड़ा जा चुका है", + "desktop.wsl.error.opencodeMissing": "इस डिस्ट्रो में opencode इंस्टॉल नहीं है", + "desktop.wsl.error.opencodeCannotRun": "opencode इंस्टॉल है, लेकिन चल नहीं सका", + "desktop.wsl.error.opencodeNotInstalled": "{{distro}} में OpenCode इंस्टॉल नहीं है", + "desktop.wsl.error.updateVersion": + "OpenCode अपडेट पूरा हो गया, लेकिन {{distro}} अब भी {{installed}} रिपोर्ट कर रहा है; अपेक्षित संस्करण {{expected}} है", + "desktop.wsl.error.noVersion": "कोई संस्करण नहीं", + "desktop.wsl.error.serverExited": "WSL सर्वर स्टार्टअप के बाद बंद हो गया (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL सर्वर के सुचारु होने से पहले बंद हो गया (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} के Sidecar की स्वास्थ्य जाँच {{timeout}}ms के बाद समय-सीमा पार कर गई", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} ने {{timeout}}ms के बाद समय-सीमा पार कर दी", + "desktop.wsl.error.failedPort": "पोर्ट प्राप्त नहीं किया जा सका", + + "desktop.picker.error.notSelected": "पिकर में फ़ाइल नहीं चुनी गई", + "desktop.picker.error.sizeLimit": "चुने गए अटैचमेंट {{limit}} MB की सीमा से अधिक हैं", + + "command.category.suggested": "सुझाए गए", + "command.category.view": "दृश्य", + "command.category.project": "प्रोजेक्ट", + "command.category.provider": "प्रोवाइडर", + "command.category.server": "सर्वर", + "command.category.session": "सेशन", + "command.category.theme": "थीम", + "command.category.language": "भाषा", + "command.category.file": "फ़ाइल", + "command.category.context": "कॉन्टेक्स्ट", + "command.category.terminal": "टर्मिनल", + "command.category.model": "मॉडल", + "command.category.mcp": "MCP", + "command.category.agent": "एजेंट", + "command.category.permissions": "अनुमतियाँ", + "command.category.workspace": "वर्कस्पेस", + "command.category.settings": "सेटिंग्स", + "command.logs.export": "लॉग निर्यात करें", + "theme.scheme.system": "सिस्टम", + "theme.scheme.light": "हल्की", + "theme.scheme.dark": "गहरी", + "command.sidebar.toggle": "साइडबार टॉगल करें", + "command.project.open": "प्रोजेक्ट खोलें", + "command.project.previous": "पिछला प्रोजेक्ट", + "command.project.next": "अगला प्रोजेक्ट", + "command.project.index": "प्रोजेक्ट {{index}} पर स्विच करें", + "command.provider.connect": "प्रोवाइडर कनेक्ट करें", + "command.server.switch": "सर्वर स्विच करें", + "command.settings.open": "सेटिंग्स खोलें", + "command.session.previous": "पिछला सेशन", + "command.session.next": "अगला सेशन", + "command.session.previous.unseen": "पिछला अपठित सेशन", + "command.session.next.unseen": "अगला अपठित सेशन", + "command.session.archive": "सेशन संग्रहित करें", + "command.palette": "कमांड पैलेट", + "command.theme.cycle": "थीम बदलें", + "command.theme.set": "थीम का उपयोग करें: {{theme}}", + "command.theme.scheme.cycle": "रंग योजना बदलें", + "command.theme.scheme.set": "रंग योजना का उपयोग करें: {{scheme}}", + "command.language.cycle": "भाषा बदलें", + "command.language.set": "भाषा का प्रयोग करें: {{language}}", + "command.session.new": "नया सेशन", + "command.file.open": "फ़ाइल खोलें", + "command.tab.close": "टैब बंद करें", + "command.tab.reopenClosed": "बंद टैब पुनः खोलें", + "command.context.addSelection": "कॉन्टेक्स्ट में चयन जोड़ें", + "command.context.addSelection.description": "वर्तमान फ़ाइल से चयनित पंक्तियाँ जोड़ें", + "command.input.focus": "इनपुट पर फ़ोकस करें", + "command.terminal.toggle": "टर्मिनल टॉगल करें", + "command.fileTree.toggle": "फ़ाइल ट्री टॉगल करें", + "command.review.toggle": "समीक्षा टॉगल करें", + "command.terminal.new": "नया टर्मिनल", + "command.terminal.new.description": "एक नया टर्मिनल टैब बनाएं", + "command.steps.toggle": "चरण टॉगल करें", + "command.steps.toggle.description": "वर्तमान संदेश के लिए चरण दिखाएँ या छिपाएँ", + "command.message.previous": "पिछला संदेश", + "command.message.previous.description": "पिछले उपयोगकर्ता संदेश पर जाएँ", + "command.message.next": "अगला संदेश", + "command.message.next.description": "अगले उपयोगकर्ता संदेश पर जाएँ", + "command.model.choose": "मॉडल चुनें", + "command.model.choose.description": "एक अलग मॉडल चुनें", + "command.mcp.toggle": "MCPs टॉगल करें", + "command.mcp.toggle.description": "MCPs टॉगल करें", + "command.agent.cycle": "एजेंट बदलें", + "command.agent.cycle.description": "अगले एजेंट पर स्विच करें", + "command.agent.cycle.reverse": "पिछला एजेंट चुनें", + "command.agent.cycle.reverse.description": "पिछले एजेंट पर स्विच करें", + "command.model.variant.cycle": "तर्क का स्तर बदलें", + "command.model.variant.cycle.description": "तर्क के अगले स्तर पर जाएँ", + "command.prompt.mode.shell": "शेल", + "command.prompt.mode.normal": "प्रॉम्प्ट", + "command.permissions.autoaccept.enable": "अनुमतियाँ स्वतः स्वीकार करें", + "command.permissions.autoaccept.disable": "अनुमतियाँ स्वत: स्वीकार करना बंद करें", + "command.workspace.toggle": "वर्कस्पेस टॉगल करें", + "command.workspace.toggle.description": "साइडबार में एकाधिक वर्कस्पेस को सक्षम या अक्षम करें", + "command.session.undo": "पूर्ववत", + "command.session.undo.description": "अंतिम संदेश पूर्ववत करें", + "command.session.redo": "फिर से करें", + "command.session.redo.description": "अंतिम पूर्ववत संदेश को पुनः करें", + "command.session.compact": "सेशन संक्षिप्त करें", + "command.session.compact.description": "कॉन्टेक्स्ट आकार को कम करने के लिए सेशन को सारांशित करें", + "command.session.fork": "संदेश से फ़ोर्क", + "command.session.fork.description": "पिछले संदेश से एक नया सेशन बनाएं", + "command.session.share": "सेशन साझा करें", + "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": "कोई परिणाम नहीं मिला", + "palette.group.commands": "कमांड", + "palette.group.files": "फ़ाइलें", + "dialog.provider.search.placeholder": "प्रोवाइडर खोजें", + "dialog.provider.empty": "कोई प्रोवाइडर नहीं मिला", + "dialog.provider.group.popular": "लोकप्रिय", + "dialog.provider.group.other": "अन्य", + "dialog.provider.custom.label": "कस्टम OpenAI-संगत प्रोवाइडर", + "dialog.provider.tag.recommended": "अनुशंसित", + "dialog.provider.opencode.note": "Claude, GPT, Gemini और अधिक सहित क्यूरेटेड मॉडल", + "dialog.provider.opencode.tagline": "विश्वसनीय अनुकूलित मॉडल", + "dialog.provider.opencodeGo.tagline": "सभी के लिए कम लागत वाली सदस्यता", + "dialog.provider.anthropic.note": "प्रो और मैक्स सहित Claude मॉडल तक सीधी पहुँच", + "dialog.provider.copilot.note": "GitHub Copilot के माध्यम से कोडिंग सहायता के लिए AI मॉडल", + "dialog.provider.openai.note": "तेज़, सक्षम सामान्य AI कार्यों के लिए GPT मॉडल", + "dialog.provider.google.note": "तेज़, संरचित प्रतिक्रियाओं के लिए Gemini मॉडल", + "dialog.provider.openrouter.note": "एक प्रोवाइडर से सभी समर्थित मॉडल तक पहुँचें", + "dialog.provider.vercel.note": "स्मार्ट रूटिंग के साथ AI मॉडल तक एकीकृत पहुँच", + "dialog.model.select.title": "मॉडल चुनें", + "dialog.model.search.placeholder": "मॉडल खोजें", + "dialog.model.empty": "कोई मॉडल परिणाम नहीं", + "dialog.model.manage": "मॉडल प्रबंधित करें", + "dialog.model.manage.description": "तय करें कि मॉडल चयनकर्ता में कौन-से मॉडल दिखाई दें।", + "dialog.model.manage.provider.toggle": "सभी {{provider}} मॉडल को टॉगल करें", + "dialog.model.unpaid.freeModels.title": "OpenCode द्वारा उपलब्ध निःशुल्क मॉडल", + "dialog.model.unpaid.addMore.title": "लोकप्रिय प्रोवाइडर से अधिक मॉडल जोड़ें", + "dialog.model.unpaid.viewMoreProviders": "70+ अधिक प्रोवाइडर देखें", + "dialog.provider.viewAll": "और प्रोवाइडर दिखाएँ", + "provider.connect.title": "{{provider}} कनेक्ट करें", + "provider.connect.title.anthropicProMax": "Claude प्रो/मैक्स के साथ लॉगिन करें", + "provider.connect.selectMethod": "{{provider}} के लिए लॉगिन विधि चुनें।", + "provider.connect.method.apiKey": "API कुंजी", + "provider.connect.method.browser": "ब्राउज़र", + "provider.connect.method.headless": "हेडलेस मोड", + "provider.connect.status.inProgress": "प्राधिकरण प्रगति पर है...", + "provider.connect.status.waiting": "प्राधिकरण की प्रतीक्षा हो रही है...", + "provider.connect.status.failed": "प्राधिकरण विफल: {{error}}", + "provider.connect.apiKey.description": + "अपने खाते को कनेक्ट करने के लिए अपनी {{provider}} API कुंजी दर्ज करें और OpenCode में {{provider}} मॉडल का उपयोग करें।", + "provider.connect.apiKey.label": "{{provider}} API कुंजी", + "provider.connect.apiKey.placeholder": "API कुंजी", + "provider.connect.apiKey.required": "API कुंजी आवश्यक है", + "provider.connect.opencodeZen.line1": + "OpenCode Zen आपको कोडिंग एजेंटों के लिए चुने गए विश्वसनीय, अनुकूलित मॉडलों तक पहुँच देता है।", + "provider.connect.opencodeZen.line2": "एक API कुंजी से आपको Claude, GPT, Gemini, GLM जैसे कई मॉडलों तक पहुँच मिलेगी।", + "provider.connect.opencodeZen.visit.prefix": "यहाँ जाएँ: ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " अपनी API कुंजी प्राप्त करने के लिए।", + "provider.connect.oauth.code.visit.prefix": "यहाँ जाएँ: ", + "provider.connect.oauth.code.visit.link": "इस लिंक", + "provider.connect.oauth.code.visit.suffix": + " अपने खाते को कनेक्ट करने और OpenCode में {{provider}} मॉडल का उपयोग करने के लिए अपना प्राधिकरण कोड प्राप्त करें।", + "provider.connect.oauth.code.label": "{{method}} प्राधिकरण कोड", + "provider.connect.oauth.code.placeholder": "प्राधिकरण कोड", + "provider.connect.oauth.code.required": "प्राधिकरण कोड आवश्यक है", + "provider.connect.oauth.code.invalid": "अमान्य प्राधिकरण कोड", + "provider.connect.oauth.auto.visit.prefix": "यहाँ जाएँ: ", + "provider.connect.oauth.auto.visit.link": "इस लिंक", + "provider.connect.oauth.auto.visit.suffix": + " और अपने खाते को कनेक्ट करने तथा OpenCode में {{provider}} मॉडल का उपयोग करने के लिए नीचे दिया गया कोड दर्ज करें।", + "provider.connect.oauth.auto.confirmationCode": "पुष्टि कोड", + "provider.connect.toast.connected.title": "{{provider}} कनेक्ट हो गया", + "provider.connect.toast.connected.description": "{{provider}} मॉडल अब उपयोग के लिए उपलब्ध हैं।", + "provider.custom.title": "कस्टम प्रोवाइडर", + "provider.custom.unavailable": "इस सर्वर पर कस्टम प्रोवाइडर उपलब्ध नहीं हैं", + "provider.custom.description.prefix": "एक OpenAI-संगत प्रोवाइडर कॉन्फ़िगर करें। देखें ", + "provider.custom.description.link": "प्रोवाइडर कॉन्फ़िगरेशन दस्तावेज़", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "प्रोवाइडर ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "छोटे अक्षर, संख्याएँ, हाइफ़न, या अंडरस्कोर", + "provider.custom.field.name.label": "प्रदर्शित होने वाला नाम", + "provider.custom.field.name.placeholder": "मेरा AI प्रोवाइडर", + "provider.custom.field.baseURL.label": "आधार URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API कुंजी", + "provider.custom.field.apiKey.placeholder": "API कुंजी", + "provider.custom.field.apiKey.description": + "वैकल्पिक। यदि आप हेडर के माध्यम से प्रमाणीकरण प्रबंधित करते हैं तो खाली छोड़ दें।", + "provider.custom.models.label": "मॉडल", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "नाम", + "provider.custom.models.name.placeholder": "प्रदर्शित होने वाला नाम", + "provider.custom.models.remove": "मॉडल हटाएँ", + "provider.custom.models.add": "मॉडल जोड़ें", + "provider.custom.headers.label": "हेडर (वैकल्पिक)", + "provider.custom.headers.key.label": "हेडर", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "मान", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "हेडर हटाएँ", + "provider.custom.headers.add": "हेडर जोड़ें", + "provider.custom.error.providerID.required": "प्रोवाइडर ID आवश्यक है", + "provider.custom.error.providerID.format": "छोटे अक्षरों, संख्याओं, हाइफ़न या अंडरस्कोर का उपयोग करें", + "provider.custom.error.providerID.exists": "वह प्रोवाइडर ID पहले से मौजूद है", + "provider.custom.error.name.required": "प्रदर्शन नाम आवश्यक है", + "provider.custom.error.baseURL.required": "आधार URL आवश्यक है", + "provider.custom.error.baseURL.format": "http:// या https:// से प्रारंभ होना चाहिए", + "provider.custom.error.required": "आवश्यक", + "provider.custom.error.duplicate": "डुप्लिकेट", + "provider.disconnect.toast.disconnected.title": "{{provider}} डिस्कनेक्ट हो गया", + "provider.disconnect.toast.disconnected.description": "{{provider}} मॉडल अब उपलब्ध नहीं हैं।", + "model.tag.free": "निःशुल्क", + "model.tag.latest": "नवीनतम", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "टेक्स्ट", + "model.input.image": "छवि", + "model.input.audio": "ऑडियो", + "model.input.video": "वीडियो", + "model.input.pdf": "PDF", + "model.tooltip.allows": "अनुमति देता है: {{inputs}}", + "model.tooltip.context.label": "कॉन्टेक्स्ट", + "model.tooltip.inputs": "इनपुट", + "model.tooltip.model": "मॉडल", + "model.tooltip.provider": "प्रोवाइडर", + "model.tooltip.reasoning": "तर्क", + "model.tooltip.reasoning.allowed": "तर्क करने की अनुमति देता है", + "model.tooltip.reasoning.none": "कोई तर्क नहीं", + "model.tooltip.context": "कॉन्टेक्स्ट सीमा {{limit}}", + "common.search.placeholder": "खोज", + "common.goBack": "पीछे जाएँ", + "common.goForward": "आगे जाएँ", + "common.loading": "लोड हो रहा है", + "common.loading.ellipsis": "...", + "common.cancel": "रद्द करें", + "common.clear": "साफ़ करें", + "common.open": "खोलें", + "common.connect": "कनेक्ट करें", + "common.disconnect": "डिस्कनेक्ट करें", + "common.continue": "जारी रखें", + "common.submit": "जमा करें", + "common.save": "सहेजें", + "common.saving": "सहेजा जा रहा है...", + "common.default": "डिफ़ॉल्ट", + "common.attachment": "अटैचमेंट", + "prompt.placeholder.shell": "शेल कमांड... {{example}} दर्ज करें", + "prompt.placeholder.normal": 'कुछ भी पूछें... "{{example}}"', + "prompt.placeholder.simple": "कुछ भी पूछें...", + "prompt.placeholder.summarizeComments": "टिप्पणियों को सारांशित करें...", + "prompt.placeholder.summarizeComment": "टिप्पणी को सारांशित करें...", + "prompt.mode.shell": "शेल", + "prompt.mode.normal": "प्रॉम्प्ट", + "prompt.mode.shell.exit": "बाहर निकलने के लिए esc", + "session.child.promptDisabled": "सबएजेंट सेशन में प्रॉम्प्ट नहीं भेजा जा सकता।", + "session.child.backToParent": "मुख्य सेशन पर वापस जाएँ।", + "prompt.example.1": "कोडबेस में एक TODO ठीक करें", + "prompt.example.2": "इस प्रोजेक्ट का तकनीकी स्टैक क्या है?", + "prompt.example.3": "टूटे हुए परीक्षणों को ठीक करें", + "prompt.example.4": "बताएं कि प्रमाणीकरण कैसे काम करता है", + "prompt.example.5": "सुरक्षा कमजोरियाँ खोजें और ठीक करें", + "prompt.example.6": "उपयोगकर्ता सेवा के लिए यूनिट परीक्षण जोड़ें", + "prompt.example.7": "इस फ़ंक्शन को अधिक पठनीय बनाने के लिए रीफ़ैक्टर करें", + "prompt.example.8": "इस त्रुटि का मतलब क्या है?", + "prompt.example.9": "इस मुद्दे को डीबग करने में मेरी सहायता करें", + "prompt.example.10": "API दस्तावेज़ तैयार करें", + "prompt.example.11": "डेटाबेस क्वेरीज़ को अनुकूलित करें", + "prompt.example.12": "इनपुट सत्यापन जोड़ें", + "prompt.example.13": "इसके लिए एक नया कॉम्पोनेंट बनाएँ...", + "prompt.example.14": "मैं इस प्रोजेक्ट को कैसे तैनात करूं?", + "prompt.example.15": "सर्वोत्तम प्रथाओं के लिए मेरे कोड की समीक्षा करें", + "prompt.example.16": "इस फ़ंक्शन में त्रुटि प्रबंधन जोड़ें", + "prompt.example.17": "इस रेगेक्स पैटर्न को समझाइये", + "prompt.example.18": "इसे TypeScript में बदलें", + "prompt.example.19": "पूरे कोडबेस में लॉगिंग जोड़ें", + "prompt.example.20": "कौन सी निर्भरताएँ पुरानी हो चुकी हैं?", + "prompt.example.21": "माइग्रेशन स्क्रिप्ट लिखने में मेरी सहायता करें", + "prompt.example.22": "इस समापन बिंदु के लिए कैशिंग लागू करें", + "prompt.example.23": "इस सूची में पेजिनेशन जोड़ें", + "prompt.example.24": "इसके लिए एक CLI कमांड बनाएं...", + "prompt.example.25": "पर्यावरण चर यहाँ कैसे काम करते हैं?", + "prompt.popover.emptyResults": "कोई मेल खाता परिणाम नहीं", + "prompt.popover.emptyCommands": "कोई मेल खाता कमांड नहीं", + "prompt.dropzone.label": "छवियाँ, PDFs, या टेक्स्ट फ़ाइलें यहां छोड़ें", + "prompt.dropzone.file.label": "फ़ाइल का @mention करने के लिए छोड़ें", + "prompt.slash.badge.custom": "कस्टम", + "prompt.slash.badge.skill": "कौशल", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "सक्रिय", + "prompt.context.includeActiveFile": "सक्रिय फ़ाइल शामिल करें", + "prompt.context.removeActiveFile": "कॉन्टेक्स्ट से सक्रिय फ़ाइल हटाएँ", + "prompt.context.removeFile": "कॉन्टेक्स्ट से फ़ाइल हटाएँ", + "prompt.action.attachFile": "फ़ाइलें जोड़ें", + "prompt.menu.addImagesAndFiles": "फ़ाइलें और बहुत कुछ जोड़ें", + "prompt.menu.imagesAndFiles": "छवियाँ और फ़ाइलें", + "prompt.menu.commands": "कमांड", + "prompt.menu.context": "कॉन्टेक्स्ट", + "prompt.menu.shellCommand": "शेल कमांड", + "prompt.attachment.remove": "अनुलग्नक हटाएँ", + "prompt.action.send": "भेजें", + "prompt.action.stop": "रोकें", + "prompt.toast.pasteUnsupported.title": "असमर्थित अनुलग्नक", + "prompt.toast.pasteUnsupported.description": "यहां केवल छवियां, PDFs, या टेक्स्ट फ़ाइलें संलग्न की जा सकती हैं।", + "prompt.toast.attachmentDuplicate.title": "यह फ़ाइल पहले ही अपलोड की जा चुकी है", + "prompt.toast.modelAgentRequired.title": "एक एजेंट और मॉडल चुनें", + "prompt.toast.modelAgentRequired.description": "प्रॉम्प्ट भेजने से पहले एक एजेंट और मॉडल चुनें।", + "prompt.toast.worktreeCreateFailed.title": "वर्कट्री बनाने में विफल", + "prompt.toast.sessionCreateFailed.title": "सेशन बनाने में विफल", + "prompt.toast.shellSendFailed.title": "शेल कमांड भेजने में विफल", + "prompt.toast.commandSendFailed.title": "कमांड भेजने में विफल", + "prompt.toast.promptSendFailed.title": "प्रॉम्प्ट भेजने में विफल", + "prompt.toast.promptSendFailed.description": "सेशन पुनः प्राप्त करने में असमर्थ", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{total}} में से {{enabled}} सक्षम", + "dialog.mcp.empty": "कोई MCP कॉन्फ़िगर नहीं किया गया", + "dialog.lsp.empty": "फ़ाइल प्रकारों से LSP का स्वतः पता लगाया जाता है", + "dialog.plugins.empty": "प्लगइन opencode.json में कॉन्फ़िगर किए गए हैं", + "mcp.status.connected": "कनेक्टेड", + "mcp.status.failed": "असफल", + "mcp.status.needs_auth": "प्रमाणीकरण की आवश्यकता है", + "mcp.status.disabled": "अक्षम", + "mcp.auth.clickToAuthenticate": "प्रमाणित करने के लिए क्लिक करें", + "dialog.fork.empty": "फ़ोर्क करने के लिए कोई संदेश नहीं", + "dialog.directory.search.placeholder": "फ़ोल्डर खोजें", + "dialog.directory.empty": "कोई फ़ोल्डर नहीं मिला", + "dialog.directory.action.selectFile": "फ़ाइल का चयन करें", + "dialog.directory.action.selectFolder": "फ़ोल्डर चुनें", + "dialog.directory.root": "रूट", + "dialog.directory.parent": "पैरेंट", + "dialog.directory.readError": "इस फ़ोल्डर को पढ़ने में असमर्थ", + "app.server.unreachable": "{{server}} तक नहीं पहुँचा जा सका", + "app.server.retrying": "स्वचालित रूप से पुनः प्रयास किया जा रहा है...", + "app.server.otherServers": "अन्य सर्वर", + "dialog.server.title": "सर्वर", + "dialog.server.description": "स्विच करें कि यह ऐप किस OpenCode सर्वर से कनेक्ट होता है।", + "dialog.server.search.placeholder": "सर्वर खोजें", + "dialog.server.empty": "अभी तक कोई सर्वर नहीं है", + "dialog.server.add.title": "सर्वर जोड़ें", + "dialog.server.add.url": "सर्वर पता", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "सर्वर से कनेक्ट नहीं कर सका", + "dialog.server.add.checking": "जाँच हो रही है...", + "dialog.server.add.button": "सर्वर जोड़ें", + "dialog.server.add.name": "सर्वर नाम (वैकल्पिक)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "उपयोगकर्ता नाम (वैकल्पिक)", + "dialog.server.add.usernamePlaceholder": "उपयोक्तानाम", + "dialog.server.add.password": "पासवर्ड (वैकल्पिक)", + "dialog.server.add.passwordPlaceholder": "पासवर्ड", + "dialog.server.edit.title": "सर्वर संपादित करें", + "dialog.server.default.title": "डिफ़ॉल्ट सर्वर", + "dialog.server.default.description": + "स्थानीय सर्वर शुरू करने के बजाय ऐप लॉन्च होने पर इस सर्वर से कनेक्ट करें। पुनः आरंभ करना आवश्यक है।", + "dialog.server.default.none": "कोई सर्वर चयनित नहीं", + "dialog.server.default.set": "वर्तमान सर्वर को डिफ़ॉल्ट के रूप में सेट करें", + "dialog.server.default.clear": "साफ़ करें", + "dialog.server.action.remove": "सर्वर हटाएँ", + "dialog.server.menu.edit": "संपादित करें", + "dialog.server.menu.default": "डिफाल्ट के रूप में सेट", + "dialog.server.menu.defaultRemove": "डिफ़ॉल्ट हटाएँ", + "dialog.server.menu.delete": "हटाएँ", + "dialog.server.current": "वर्तमान सर्वर", + "dialog.server.status.default": "डिफ़ॉल्ट", + "wsl.server.add": "WSL सर्वर जोड़ें", + "wsl.server.addShort": "WSL जोड़ें", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL सर्वर", + "wsl.server.retryStart": "पुन: प्रयास प्रारंभ करें", + "wsl.server.updating": "अद्यतन किया जा रहा है...", + "wsl.onboarding.step.distro": "डिस्ट्रो चुनें", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL की जाँच हो रही है...", + "wsl.onboarding.restartRequired": "WSL की स्थापना पूरी करने के लिए Windows को पुनः आरंभ करना होगा।", + "wsl.onboarding.ready": "WSL तैयार है।", + "wsl.onboarding.required": "जारी रखने के लिए WSL आवश्यक है।", + "wsl.onboarding.checkingDistros": "डिस्ट्रोज़ की जाँच हो रही है...", + "wsl.onboarding.installingDistro": "{{distro}} इंस्टॉल किया जा रहा है...", + "wsl.onboarding.checkingDistro": "{{distro}} की जाँच हो रही है...", + "wsl.onboarding.listingDistros": "डिस्ट्रो की सूची बनाई जा रही है...", + "wsl.onboarding.distroReady": "{{distro}} तैयार है।", + "wsl.onboarding.distroNotInstalled": "{{distro}} अभी तक इंस्टॉल नहीं है।", + "wsl.onboarding.openDistroOnce": "सेटअप पूरा करने के लिए एक बार {{distro}} खोलें।", + "wsl.onboarding.finishingDistro": "{{distro}} का सेटअप पूरा किया जा रहा है।", + "wsl.onboarding.pickDistro": "एक डिस्ट्रो चुनें या नीचे एक स्थापित करें।", + "wsl.onboarding.checkingOpencode": "OpenCode की जाँच हो रही है...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}} में OpenCode की जाँच हो रही है...", + "wsl.onboarding.updatingOpencode": "OpenCode अपडेट हो रहा है...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}} में OpenCode अपडेट हो रहा है...", + "wsl.onboarding.updateOpencodeIn": "{{distro}} में OpenCode को अपडेट करें।", + "wsl.onboarding.updateOpencode": "OpenCode को अपडेट करें", + "wsl.onboarding.opencodeReadyIn": "OpenCode {{distro}} में तैयार है।", + "wsl.onboarding.opencodeReady": "OpenCode तैयार है।", + "wsl.onboarding.installOpencodeIn": "{{distro}} में OpenCode स्थापित करें।", + "wsl.onboarding.installOpencode": "OpenCode स्थापित करें", + "wsl.onboarding.chooseDistroFirst": "पहले एक डिस्ट्रो चुनें।", + "wsl.onboarding.loadFailed": "WSL स्थिति लोड करने में विफल।", + "wsl.onboarding.loading": "लोड हो रहा है...", + "wsl.onboarding.installedDistros": "स्थापित डिस्ट्रोस", + "wsl.onboarding.checkAgain": "फिर से जाँचें", + "wsl.onboarding.distroStatus.ready": "तैयार", + "wsl.onboarding.distroStatus.checking": "जाँच हो रही है...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode स्थापित नहीं है", + "wsl.onboarding.distroStatus.missingTools": "गायब बैश, कर्ल", + "wsl.onboarding.distroStatus.unsupported": "असमर्थित · WSL 2 का उपयोग करें", + "wsl.onboarding.needAnotherDistro": "एक और डिस्ट्रो की आवश्यकता है?", + "wsl.onboarding.needAnotherDistroHint": "WSL कैटलॉग से Linux वितरण स्थापित करें", + "wsl.onboarding.wslNotInstalled.title": "WSL स्थापित नहीं है", + "wsl.onboarding.wslNotInstalled.description": + "OpenCode द्वारा WSL सर्वर जोड़ने से पहले WSL (Linux के लिए Windows सबसिस्टम) आवश्यक है", + "wsl.onboarding.wslUnavailable.title": "WSL अनुपलब्ध है", + "wsl.onboarding.wslUnavailable.description": "OpenCode इस मशीन पर WSL को सत्यापित नहीं कर सका।", + "wsl.onboarding.installWsl": "WSL स्थापित करें", + "wsl.onboarding.windowsRestartRequired": + "WSL की स्थापना समाप्त करने के लिए Windows को पुनरारंभ करें, फिर OpenCode को फिर से खोलें।", + "wsl.onboarding.next": "अगला", + "wsl.onboarding.refresh": "रीफ़्रेश करें", + "wsl.onboarding.allDistrosAdded": "सभी स्थापित डिस्ट्रो पहले ही जोड़े जा चुके हैं।", + "wsl.onboarding.noDistros": "अभी तक कोई डिस्ट्रो नहीं मिला।", + "wsl.onboarding.install": "स्थापित करें", + "wsl.onboarding.installing": "स्थापित किया जा रहा है...", + "wsl.onboarding.installDistro": "डिस्ट्रो स्थापित करें", + "wsl.onboarding.searchDistros": "डिस्ट्रोस खोजें", + "wsl.onboarding.wsl2Required": "WSL 2 आवश्यक है।", + "wsl.onboarding.toolsRequired": "इस डिस्ट्रो को बैश और कर्ल की जरूरत है।", + "wsl.onboarding.openTerminal": "टर्मिनल खोलें", + "wsl.onboarding.path": "पथ: {{path}}", + "wsl.onboarding.notFound": "नहीं मिला", + "wsl.onboarding.version": "संस्करण: {{version}}", + "wsl.onboarding.unknown": "अज्ञात", + "wsl.onboarding.desktopVersion": "डेस्कटॉप {{version}}", + "wsl.onboarding.versionMismatch": "इंस्टॉल किया गया संस्करण डेस्कटॉप ऐप संस्करण से मेल नहीं खाता।", + "wsl.onboarding.adding": "जोड़ा जा रहा है...", + "help.tabs.toast.ariaLabel": "टैब प्रस्तुत हैं। टैब की सहायता से अपना काम और सक्रिय सेशन व्यवस्थित करें", + "help.tabs.toast.dismiss": "टैब की जानकारी हटाएँ", + "help.tabs.title": "टैब प्रस्तुत हैं", + "help.tabs.description": "टैब की सहायता से अपना काम और सक्रिय सेशन व्यवस्थित करें", + "help.tabs.date": "14 जुलाई", + "help.tabs.introduction": "OpenCode Desktop अब टैब पर आधारित है।", + "help.tabs.sessions": + "किसी टैब में नया सेशन शुरू करें या अपने किसी भी प्रोजेक्ट से मौजूदा सेशन खोलें। कुछ नया शुरू करते समय नया टैब खोलें और काम पूरा होने पर उसे बंद कर दें।", + "help.tabs.organize": + "कुछ टैब खुले रखने से सक्रिय सेशन व्यवस्थित करना आसान होता है। यदि आप टैब खुले रखना चाहते हैं, तो उन्हें कोई याद रहने वाला नाम दें।", + "help.tabs.home": "आपके सभी सेशन और प्रोजेक्ट नई होम स्क्रीन पर मिलेंगे। किसी सेशन को चुनने पर वह टैब में खुलता है।", + "help.tabs.persistence": "ऐप दोबारा खोलने पर भी आपके टैब खुले रहते हैं।", + "help.tabs.worktrees": + "नया डिज़ाइन अभी Git Worktrees का समर्थन नहीं करता है, यह सुविधा जल्द आ रही है। इसलिए यदि आप पिछले लेआउट का उपयोग जारी रखना चाहते हैं, तो सेटिंग्स में लेआउट के बीच स्विच कर सकते हैं। ध्यान रखें कि नया लेआउट कुछ सप्ताह में स्थायी हो जाएगा।", + "server.row.noUsername": "कोई उपयोगकर्ता नाम नहीं", + "dialog.project.edit.title": "प्रोजेक्ट संपादित करें", + "dialog.project.edit.name": "नाम", + "dialog.project.edit.icon": "आइकन", + "dialog.project.edit.icon.alt": "प्रोजेक्ट आइकन", + "dialog.project.edit.icon.hint": "किसी छवि को क्लिक करें या खींचें", + "dialog.project.edit.icon.recommended": "अनुशंसित: 128x128px", + "dialog.project.edit.color": "रंग", + "dialog.project.edit.color.select": "{{color}} रंग चुनें", + "dialog.project.edit.worktree.startup": "वर्कस्पेस स्टार्टअप स्क्रिप्ट", + "dialog.project.edit.worktree.startup.description": "एक नया वर्कस्पेस (वर्कट्री) बनाने के बाद चलता है।", + "dialog.project.edit.worktree.startup.placeholder": "जैसे: bun install", + "dialog.releaseNotes.action.getStarted": "शुरू करें", + "dialog.releaseNotes.action.next": "अगला", + "dialog.releaseNotes.action.hideFuture": "भविष्य में इन्हें न दिखाएँ", + "dialog.releaseNotes.media.alt": "रिलीज़ पूर्वावलोकन", + "dialog.usageExceeded.dontShowAgain": "फिर से न दिखाएँ", + + "context.breakdown.title": "कॉन्टेक्स्ट ब्रेकडाउन", + "context.breakdown.note": 'इनपुट टोकन का अनुमानित विभाजन। "अन्य" में टूल की परिभाषाएँ और अतिरिक्त खर्च शामिल हैं।', + "context.breakdown.system": "सिस्टम", + "context.breakdown.user": "उपयोगकर्ता", + "context.breakdown.assistant": "सहायक", + "context.breakdown.tool": "टूल कॉल", + "context.breakdown.other": "अन्य", + "context.systemPrompt.title": "सिस्टम प्रॉम्प्ट", + "context.rawMessages.title": "मूल संदेश", + "context.export.session": "सेशन निर्यात करें", + + "context.stats.session": "सेशन", + "context.stats.messages": "संदेश", + "context.stats.provider": "प्रोवाइडर", + "context.stats.model": "मॉडल", + "context.stats.limit": "कॉन्टेक्स्ट सीमा", + "context.stats.totalTokens": "कुल टोकन", + "context.stats.usage": "उपयोग", + "context.stats.inputTokens": "इनपुट टोकन", + "context.stats.outputTokens": "आउटपुट टोकन", + "context.stats.reasoningTokens": "तर्क टोकन", + "context.stats.cacheTokens": "कैश टोकन (पढ़ें/लिखें)", + "context.stats.userMessages": "उपयोगकर्ता संदेश", + "context.stats.assistantMessages": "सहायक संदेश", + "context.stats.totalCost": "कुल लागत", + "context.stats.sessionCreated": "सेशन बनाया गया", + "context.stats.lastActivity": "अंतिम गतिविधि", + "context.usage.tokens": "टोकन", + "context.usage.usage": "उपयोग", + "context.usage.cost": "लागत", + "context.usage.clickToView": "कॉन्टेक्स्ट देखने के लिए क्लिक करें", + "context.usage.view": "कॉन्टेक्स्ट उपयोग देखें", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "भाषा", + "toast.language.description": "{{language}} पर स्विच किया गया", + "toast.theme.title": "थीम स्विच किया गया", + "toast.scheme.title": "रंग योजना", + "toast.workspace.enabled.title": "वर्कस्पेस सक्षम", + "toast.workspace.enabled.description": "एकाधिक वर्कट्री अब साइडबार में दिखाए गए हैं", + "toast.workspace.disabled.title": "वर्कस्पेस अक्षम", + "toast.workspace.disabled.description": "साइडबार में केवल मुख्य वर्कट्री दिखाया गया है", + "toast.permissions.autoaccept.on.title": "अनुमतियाँ स्वतः स्वीकार करना", + "toast.permissions.autoaccept.on.description": "अनुमति अनुरोध स्वचालित रूप से स्वीकृत हो जाएंगे", + "toast.permissions.autoaccept.off.title": "अनुमतियों को स्वतः स्वीकार करना बंद कर दिया गया", + "toast.permissions.autoaccept.off.description": "अनुमति अनुरोधों के लिए अनुमोदन की आवश्यकता होगी", + "toast.model.none.title": "कोई मॉडल चयनित नहीं", + "toast.model.none.description": "इस सेशन को सारांशित करने के लिए एक प्रोवाइडर कनेक्ट करें", + "toast.file.loadFailed.title": "फ़ाइल लोड करने में विफल", + "toast.file.listFailed.title": "फ़ाइलें सूचीबद्ध करने में विफल", + "toast.context.noLineSelection.title": "कोई पंक्ति चयन नहीं", + "toast.context.noLineSelection.description": "पहले फ़ाइल टैब में एक पंक्ति श्रेणी का चयन करें।", + "toast.session.share.copyFailed.title": "URL को क्लिपबोर्ड पर कॉपी करने में विफल", + "toast.session.share.success.title": "सेशन साझा किया गया", + "toast.session.share.success.description": "साझा URL क्लिपबोर्ड पर कॉपी किया गया!", + "toast.session.share.failed.title": "सेशन साझा करने में विफल", + "toast.session.share.failed.description": "सेशन साझा करते समय एक त्रुटि उत्पन्न हुई", + "toast.session.unshare.success.title": "सेशन अनशेयर किया गया", + "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": "उपलब्ध अद्यतन", + "toast.update.description": "OpenCode ({{version}}) का एक नया संस्करण अब इंस्टॉल करने के लिए उपलब्ध है।", + "toast.update.action.installRestart": "स्थापित करें और पुनः आरंभ करें", + "toast.update.action.notYet": "अभी नहीं", + "error.page.title": "कुछ गलत हो गया", + "error.page.description": "एप्लिकेशन लोड करते समय एक त्रुटि हुई।", + "error.page.description.localServerStartup": "स्थानीय सर्वर शुरू करते समय एक त्रुटि हुई।", + "error.page.details.label": "त्रुटि विवरण", + "error.page.action.restart": "पुनः आरंभ करें", + "error.page.action.report": "त्रुटि की रिपोर्ट करें", + "error.page.action.reported": "त्रुटि रिपोर्ट की गई", + "error.page.action.exportLogs": "लॉग निर्यात करें", + "error.page.action.checking": "जाँच हो रही है...", + "error.page.action.checkUpdates": "अद्यतन के लिए जाँच", + "error.page.action.updateTo": "{{version}} पर अपडेट करें", + "error.page.circular": "[Circular]", + "error.page.report.prefix": "कृपया इस त्रुटि की रिपोर्ट OpenCode टीम को करें", + "error.page.report.discord": "Discord पर", + "error.page.version": "संस्करण: {{version}}", + "error.dev.rootNotFound": + "मूल तत्व नहीं मिला. क्या आप इसे अपने index.html में जोड़ना भूल गए? या हो सकता है कि आईडी विशेषता गलत वर्तनी हो गई हो?", + "error.serverSync.connectFailed": "सर्वर से कनेक्ट नहीं कर सका। क्या `{{url}}` पर कोई सर्वर चल रहा है?", + "error.serverSDK.noServerAvailable": "कोई सर्वर उपलब्ध नहीं है", + "error.serverSDK.serverNotAvailable": "सर्वर उपलब्ध नहीं है", + "error.childStore.persistedCacheCreateFailed": "सतत कैश बनाने में विफल", + "error.childStore.persistedProjectMetadataCreateFailed": "निरंतर प्रोजेक्ट मेटाडेटा बनाने में विफल", + "error.childStore.persistedProjectIconCreateFailed": "स्थायी प्रोजेक्ट आइकन बनाने में विफल", + "error.childStore.storeCreateFailed": "स्टोर बनाने में विफल", + "directory.error.invalidUrl": "URL में अमान्य निर्देशिका।", + "error.chain.unknown": "अज्ञात त्रुटि", + "error.server.invalidConfiguration": "अमान्य कॉन्फ़िगरेशन", + "error.chain.causedBy": "कारण:", + "error.chain.apiError": "API त्रुटि", + "error.chain.status": "स्थिति: {{status}}", + "error.chain.retryable": "पुनः प्रयास योग्य: {{retryable}}", + "error.chain.responseBody": "प्रतिक्रिया निकाय:\n{{body}}", + "error.chain.didYouMean": "क्या आपका मतलब था: {{suggestions}}", + "error.chain.modelNotFound": "मॉडल नहीं मिला: {{provider}}/{{model}}", + "error.chain.checkConfig": "अपने कॉन्फ़िगरेशन (opencode.json) प्रोवाइडर/मॉडल नामों की जाँच करें", + "error.chain.mcpFailed": + 'MCP सर्वर "{{name}}" विफल रहा। ध्यान दें, OpenCode अभी तक MCP प्रमाणीकरण का समर्थन नहीं करता है।', + "error.chain.providerAuthFailed": "प्रोवाइडर प्रमाणीकरण विफल ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'प्रोवाइडर "{{provider}}" शुरू नहीं किया जा सका। क्रेडेंशियल और कॉन्फ़िगरेशन की जाँच करें।', + "error.chain.configJsonInvalid": "{{path}} पर कॉन्फ़िगरेशन फ़ाइल वैध JSON(C) नहीं है", + "error.chain.configJsonInvalidWithMessage": "{{path}} पर कॉन्फ़िगरेशन फ़ाइल वैध नहीं है JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} में निर्देशिका "{{dir}}" मान्य नहीं है। निर्देशिका का नाम बदलकर "{{suggestion}}" करें या इसे हटा दें। यह वर्तनी की एक सामान्य त्रुटि है।', + "error.chain.configFrontmatterError": "{{path}} में फ्रंटमैटर को पार्स करने में विफल:\n{{message}}", + "error.chain.configInvalid": "{{path}} पर कॉन्फ़िगरेशन फ़ाइल अमान्य है", + "error.chain.configInvalidWithMessage": "{{path}} पर कॉन्फ़िगरेशन फ़ाइल अमान्य है: {{message}}", + "notification.permission.title": "अनुमति आवश्यक है", + "notification.permission.description": "{{projectName}} में {{sessionTitle}} को अनुमति की आवश्यकता है", + "notification.question.title": "प्रश्न", + "notification.question.description": "{{projectName}} में {{sessionTitle}} का एक प्रश्न है", + "notification.action.goToSession": "सेशन पर जाएं", + "notification.session.responseReady.title": "प्रतिक्रिया तैयार", + "notification.session.error.title": "सेशन त्रुटि", + "notification.session.error.fallbackDescription": "एक त्रुटि पाई गई", + "home.recentProjects": "हाल के प्रोजेक्ट", + "home.empty.title": "कोई हालिया प्रोजेक्ट नहीं", + "home.empty.description": "स्थानीय प्रोजेक्ट खोलकर शुरुआत करें", + "home.title": "होम", + "home.projects": "प्रोजेक्ट", + "home.project.add": "प्रोजेक्ट जोड़ें", + "home.recentlyClosed": "हाल ही में बंद हुआ", + "home.server.collapse": "सर्वर प्रोजेक्ट को संक्षिप्त करें", + "home.server.expand": "सर्वर प्रोजेक्ट का विस्तार करें", + "home.sessions.search.placeholder": "सेशन खोजें", + "home.sessions.search.placeholder.scoped": "{{scope}} में सेशन खोजें", + "home.sessions.search.sessions": "सेशन", + "home.sessions.search.noResults": "{{query}} के लिए कोई सेशन नहीं मिला", + "home.sessions.empty": "अभी तक यहां कुछ भी नहीं है", + "home.sessions.empty.description": "आरंभ करने के लिए एक सेशन बनाएं", + "home.sessions.group.today": "आज", + "home.sessions.group.yesterday": "कल", + "home.sessions.group.older": "पुराने", + "home.providerTip": "Claude, GPT, Gemini आदि सहित अन्य मॉडल का उपयोग करने के लिए 75+ प्रोवाइडर से कनेक्ट करें", + "session.tab.session": "सेशन", + "session.tab.review": "समीक्षा", + "session.tab.context": "कॉन्टेक्स्ट", + "session.tab.unknown": "अज्ञात सेशन", + "session.panel.reviewAndFiles": "समीक्षा और फ़ाइलें", + "session.error.notFound": "यह सेशन नहीं मिल सका", + "session.error.notFound.description": "यह टैब एक सेशन की ओर इशारा करता है जो अब इस सर्वर पर मौजूद नहीं है।", + "session.error.notFound.closeTab": "टैब बंद करें", + "session.error.serverConnection": "इस सर्वर से कनेक्ट नहीं किया जा सकता", + "session.review.filesChanged": "बदली गई फ़ाइलें: {{count}}", + "session.review.change.one": "परिवर्तन", + "session.review.change.other": "परिवर्तन", + "session.review.loadingChanges": "परिवर्तन लोड हो रहे हैं...", + "session.review.empty": "इस सेशन में अभी तक कोई बदलाव नहीं हुआ है", + "session.review.noVcs": "कोई Git संस्करण नियंत्रण प्रणाली नहीं पाई गई, परिवर्तन प्रदर्शित नहीं हुए", + "session.review.noVcs.createGit.title": "एक Git रिपॉजिटरी बनाएं", + "session.review.noVcs.createGit.description": + "इस प्रोजेक्ट में परिवर्तनों को ट्रैक करें, समीक्षा करें और पूर्ववत करें", + "session.review.noVcs.createGit.actionLoading": "Git रिपॉजिटरी बनाई जा रही है...", + "session.review.noVcs.createGit.action": "Git रिपॉजिटरी बनाएं", + "session.review.noSnapshot": "कॉन्फ़िगरेशन में स्नैपशॉट ट्रैकिंग अक्षम है, इसलिए सेशन परिवर्तन अनुपलब्ध हैं", + "session.review.noChanges": "कोई परिवर्तन नहीं", + "session.review.noUncommittedChanges": "अभी तक कोई अप्रतिबद्ध परिवर्तन नहीं", + "session.review.noBranchChanges": "अभी तक शाखा में कोई परिवर्तन नहीं", + "session.files.selectToOpen": "खोलने के लिए एक फ़ाइल का चयन करें", + "session.files.all": "सभी फ़ाइलें", + "session.files.empty": "कोई फ़ाइल नहीं", + "session.files.binaryContent": "बाइनरी फ़ाइल (सामग्री प्रदर्शित नहीं की जा सकती)", + "session.messages.renderEarlier": "पहले के संदेश रेंडर करें", + "session.messages.loadingEarlier": "पुराने संदेश लोड हो रहे हैं...", + "session.messages.loadEarlier": "पहले के संदेश लोड करें", + "session.messages.loading": "संदेश लोड हो रहे हैं...", + "session.messages.jumpToLatest": "नवीनतम पर जाएं", + "session.context.addToContext": "{{selection}} को कॉन्टेक्स्ट में जोड़ें", + "session.todo.title": "कार्य सूची", + "session.todo.collapse": "संकुचित करें", + "session.todo.expand": "विस्तृत करें", + "session.todo.progress": "{{total}} में से {{done}} कार्य पूरे हुए", + "session.question.progress": "{{total}} प्रश्नों में से {{current}}", + "session.question.minimize": "प्रश्न छोटा करें", + "session.question.restore": "प्रश्न पुनर्स्थापित करें", + "session.question.pending.one": "{{count}} लंबित प्रश्न", + "session.question.pending.other": "{{count}} लंबित प्रश्न", + "session.followupDock.summary.one": "कतार में {{count}} संदेश", + "session.followupDock.summary.other": "कतार में {{count}} संदेश", + "session.followupDock.sendNow": "अब भेजें", + "session.followupDock.edit": "संपादित करें", + "session.followupDock.collapse": "कतार के संदेश संकुचित करें", + "session.followupDock.expand": "कतार के संदेश विस्तृत करें", + "session.revertDock.summary.one": "{{count}} संदेश वापस लिया गया", + "session.revertDock.summary.other": "{{count}} संदेश वापस लिए गए", + "session.revertDock.collapse": "रोलबैक संदेशों को संक्षिप्त करें", + "session.revertDock.expand": "वापस लाये गए संदेशों का विस्तार करें", + "session.revertDock.restore": "संदेश पुनर्स्थापित करें", + "session.new.title": "कुछ भी बनाएँ", + "session.new.project.new": "नया प्रोजेक्ट", + "session.new.project.search": "प्रोजेक्ट खोजें", + "session.new.project.add": "प्रोजेक्ट जोड़ें", + "session.new.worktree.main": "मुख्य शाखा", + "session.new.worktree.mainWithBranch": "मुख्य शाखा ({{branch}})", + "session.new.worktree.create": "नया वर्कट्री बनाएं", + "session.new.workspace.runIn": "सेशन यहाँ चलाएँ", + "session.new.workspace.triggerLocal": "स्थानीय", + "session.new.workspace.local": "स्थानीय रिपॉजिटरी", + "session.new.workspace.existing": "वर्कस्पेस…", + "session.new.git.none": "कोई Git नहीं", + "session.new.lastModified": "अंतिम बार संशोधित", + "session.header.search.placeholder": "{{project}} खोजें", + "session.header.searchFiles": "फ़ाइलें खोजें", + "session.header.openIn": "में खोलें", + "session.header.open.action": "{{app}} खोलें", + "session.header.open.ariaLabel": "{{app}} में खोलें", + "session.header.open.menu": "विकल्प खोलें", + "session.header.open.copyPath": "पथ कॉपी करें", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "Finder में प्रकट करें", + "session.header.reveal.fileExplorer": "File Explorer में प्रकट करें", + "session.header.reveal.containingFolder": "मूल फ़ोल्डर खोलें", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "टर्मिनल", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "स्थिति", + "status.popover.ariaLabel": "सर्वर कॉन्फ़िगरेशन", + "status.popover.tab.servers": "सर्वर", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "प्लग-इन", + "status.popover.action.manageServers": "सर्वर प्रबंधित करें", + "session.share.popover.title": "वेब पर प्रकाशित करें", + "session.share.popover.description.shared": + "यह सेशन वेब पर सार्वजनिक है। यह लिंक वाले किसी भी व्यक्ति के लिए सुलभ है।", + "session.share.popover.description.unshared": + "सेशन को वेब पर सार्वजनिक रूप से साझा करें। यह लिंक वाले किसी भी व्यक्ति के लिए सुलभ होगा।", + "session.share.action.share": "साझा करें", + "session.share.action.publish": "प्रकाशित करें", + "session.share.action.publishing": "प्रकाशित किया जा रहा है...", + "session.share.action.unpublish": "प्रकाशन हटाएँ", + "session.share.action.unpublishing": "प्रकाशन हटाया जा रहा है...", + "session.share.action.view": "देखें", + "session.share.copy.copied": "कॉपी किया गया", + "session.share.copy.copyLink": "लिंक कॉपी करें", + "lsp.tooltip.none": "कोई LSP सर्वर नहीं", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "प्रॉम्प्ट लोड हो रहा है...", + "terminal.loading": "टर्मिनल लोड हो रहा है...", + "terminal.title": "टर्मिनल", + "terminal.title.numbered": "टर्मिनल {{number}}", + "terminal.close": "टर्मिनल बंद करें", + "terminal.connectionLost.title": "कनेक्शन टूट गया", + "terminal.connectionLost.abnormalClose": "WebSocket असामान्य रूप से बंद हुआ: {{code}}", + "terminal.connectionLost.description": "टर्मिनल कनेक्शन बाधित हो गया था। ऐसा तब हो सकता है जब सर्वर पुनरारंभ हो।", + "terminal.connectTicket.csrfError": + "ओरिजिन या CSRF जाँच के कारण PTY कनेक्ट टिकट अस्वीकार कर दिया गया। सर्वर का CORS कॉन्फ़िगरेशन जाँचें।", + "terminal.connectTicket.statusError": "PTY कनेक्ट टिकट विफल रहा (स्थिति: {{status}})", + "titlebar.update": "अपडेट करें", + "titlebar.updateVersion": "अपडेट करें {{version}}", + "common.closeTab": "टैब बंद करें", + "common.dismiss": "हटाएँ", + "common.moreCountSuffix": " (+{{count}} और)", + "common.requestFailed": "अनुरोध विफल रहा", + "common.moreOptions": "अधिक विकल्प", + "common.learnMore": "और अधिक जानें", + "common.rename": "नाम बदलें", + "common.export": "निर्यात करें", + "common.reset": "रीसेट करें", + "common.archive": "संग्रहित करें", + "common.delete": "हटाएँ", + "common.close": "बंद करें", + "common.edit": "संपादित करें", + "common.loadMore": "और लोड करें", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "अज्ञात", + "common.time.justNow": "अभी-अभी", + "common.time.minutesAgo.short": "{{count}}m पहले", + "common.time.hoursAgo.short": "{{count}}h पहले", + "common.time.daysAgo.short": "{{count}}d पहले", + "sidebar.menu.toggle": "मेनू टॉगल करें", + "sidebar.nav.projectsAndSessions": "प्रोजेक्ट और सेशन", + "sidebar.settings": "सेटिंग्स", + "sidebar.help": "मदद", + "sidebar.workspaces.enable": "वर्कस्पेस सक्षम करें", + "sidebar.workspaces.disable": "वर्कस्पेस अक्षम करें", + "sidebar.gettingStarted.title": "शुरुआत करें", + "sidebar.gettingStarted.line1": "OpenCode में निःशुल्क मॉडल शामिल है ताकि आप तुरंत शुरू कर सकें।", + "sidebar.gettingStarted.line2": "Claude, GPT, Gemini आदि मॉडल उपयोग करने के लिए किसी भी प्रोवाइडर को कनेक्ट करें।", + "sidebar.project.recentSessions": "हाल ही में सेशन", + "sidebar.project.viewAllSessions": "सभी सेशन देखें", + "sidebar.project.clearNotifications": "सूचनाएँ साफ़ करें", + "sidebar.empty.title": "कोई प्रोजेक्ट खुला नहीं", + "sidebar.empty.description": "आरंभ करने के लिए एक प्रोजेक्ट खोलें", + "debugBar.ariaLabel": "विकास प्रदर्शन निदान", + "debugBar.na": "एन/ए", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "सेशन पृष्ठ को छूते हुए अंतिम पूर्ण मार्ग परिवर्तन, राउटर प्रारंभ से लेकर उसके व्यवस्थित होने के बाद पहले पेंट तक मापा जाता है।", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "पिछले 5 सेकंड में फ़्रेम प्रति सेकंड रोलिंग।", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "पिछले 5 सेकंड में सबसे खराब फ़्रेम समय।", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "अंतिम 5 सेकंड में 32 एमएस से अधिक फ़्रेम।", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "पिछले 5 सेकंड में अवरुद्ध समय और लंबे कार्यों की संख्या। सबसे लंबा कार्य: {{max}}।", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "पिछले 5 सेकंड में सबसे खराब इनपुट विलंब देखा गया।", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "पिछले 5 सेकंड में अनुमानित इंटरैक्शन अवधि। यह INP जैसा है, आधिकारिक Web Vitals INP नहीं।", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "वर्तमान ऐप जीवनकाल के लिए संचयी लेआउट बदलाव।", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "उपयोग किया गया JS हीप बनाम हीप सीमा। केवल Chromium।", + "debugBar.mem.tip": "उपयोग किया गया JS हीप बनाम हीप सीमा। {{limit}} में से {{used}}।", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "सभी इंटरैक्टिव एलिमेंट पर फ़ोकस स्टाइल ज़बरदस्ती लागू करें", + "debugBar.focus.on": "चालू", + "debugBar.focus.off": "बंद", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "चुनी गई भाषा बदले बिना पूरे ऐप की लेआउट दिशा ज़बरदस्ती सेट करें", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "डेस्कटॉप", + "settings.section.server": "सर्वर", + "settings.tab.general": "सामान्य", + "settings.tab.shortcuts": "शॉर्टकट", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL एकीकरण", + "settings.desktop.wsl.description": "Windows पर WSL के अंदर OpenCode सर्वर चलाएँ।", + "settings.general.section.appearance": "उपस्थिति", + "settings.general.section.advanced": "उन्नत", + "settings.general.section.notifications": "सिस्टम सूचनाएँ", + "settings.general.section.updates": "अपडेट", + "settings.general.section.sounds": "ध्वनि प्रभाव", + "settings.general.section.feed": "फ़ीड", + "settings.general.section.display": "प्रदर्शन", + "settings.general.row.language.title": "भाषा", + "settings.general.row.language.description": "OpenCode के लिए प्रदर्शन भाषा बदलें", + "settings.general.row.shell.title": "टर्मिनल शेल", + "settings.general.row.shell.description": "शेल का उपयोग टर्मिनल और एजेंट टूल द्वारा किया जाता है", + "settings.general.row.shell.autoDefault": "स्वतः (डिफ़ॉल्ट)", + "settings.general.row.shell.terminalOnly": "केवल टर्मिनल", + "settings.general.row.appearance.title": "उपस्थिति", + "settings.general.row.appearance.description": "अनुकूलित करें कि OpenCode आपके डिवाइस पर कैसा दिखता है", + "settings.general.row.colorScheme.title": "रंग योजना", + "settings.general.row.colorScheme.description": "चुनें कि OpenCode सिस्टम, हल्की या गहरी थीम में से किसका उपयोग करे", + "settings.general.row.theme.title": "थीम", + "settings.general.row.theme.description": "अनुकूलित करें कि OpenCode की थीम कैसी है।", + "settings.general.row.font.title": "कोड फ़ॉन्ट", + "settings.general.row.font.description": "कोड ब्लॉक में प्रयुक्त फ़ॉन्ट को अनुकूलित करें", + "settings.general.row.terminalFont.title": "टर्मिनल फ़ॉन्ट", + "settings.general.row.terminalFont.description": "टर्मिनल में प्रयुक्त फ़ॉन्ट को अनुकूलित करें", + "settings.general.row.uiFont.title": "UI फ़ॉन्ट", + "settings.general.row.uiFont.description": "पूरे इंटरफ़ेस में उपयोग किए गए फ़ॉन्ट को कस्टमाइज़ करें", + "settings.general.row.followup.title": "अनुवर्ती व्यवहार", + "settings.general.row.followup.description": + "चुनें कि क्या अनुवर्ती प्रॉम्प्ट तुरंत आगे बढ़ेगा या कतार में प्रतीक्षा करेगा", + "settings.general.row.followup.option.queue": "कतार", + "settings.general.row.followup.option.steer": "तुरंत निर्देश दें", + "settings.general.row.showFileTree.title": "फ़ाइल वृक्ष", + "settings.general.row.showFileTree.description": "सेशन में फ़ाइल ट्री पैनल दिखाएँ", + "settings.general.row.showNavigation.title": "नेविगेशन नियंत्रण", + "settings.general.row.showNavigation.description": "डेस्कटॉप टाइटल बार में पीछे और आगे के बटन दिखाएँ", + "settings.general.row.showSearch.title": "कमांड पैलेट", + "settings.general.row.showSearch.description": "टाइटल बार में खोज और कमांड पैलेट बटन दिखाएँ", + "settings.general.row.showTerminal.title": "टर्मिनल", + "settings.general.row.showTerminal.description": "डेस्कटॉप टाइटल बार में टर्मिनल बटन दिखाएँ", + "settings.general.row.showStatus.title": "सर्वर की स्थिति", + "settings.general.row.showStatus.description": "टाइटल बार में सर्वर स्थिति बटन दिखाएँ", + "settings.general.row.mobileTitlebarBottom.title": "निचला नेविगेशन", + "settings.general.row.mobileTitlebarBottom.description": "मोबाइल पर स्क्रीन के नीचे टाइटल बार और सेशन टैब रखें", + "settings.general.row.showCustomAgents.title": "एजेंट दिखाएँ", + "settings.general.row.showCustomAgents.description": + "कंपोज़र में एजेंट के बीच स्विच करें। छिपाए जाने पर, डिफ़ॉल्ट रूप से बिल्ड एजेंट हो जाता है।", + "settings.general.row.reasoningSummaries.title": "तर्क सारांश दिखाएँ", + "settings.general.row.reasoningSummaries.description": "टाइमलाइन में मॉडल तर्क सारांश प्रदर्शित करें", + "settings.general.row.shellToolPartsExpanded.title": "शेल टूल पार्ट्स का विस्तार करें", + "settings.general.row.shellToolPartsExpanded.description": + "टाइमलाइन में डिफ़ॉल्ट रूप से विस्तारित शेल टूल पार्ट्स दिखाएं", + "settings.general.row.editToolPartsExpanded.title": "संपादन टूल भागों का विस्तार करें", + "settings.general.row.editToolPartsExpanded.description": + "टाइमलाइन में डिफ़ॉल्ट रूप से विस्तारित संपादित करें, लिखें और पैच टूल पार्ट्स दिखाएं", + "settings.general.row.newInterface.title": "नया लेआउट", + "settings.general.row.newInterface.badge": "नया", + "settings.general.row.newInterface.description": + "नए टैब और होम लेआउट का उपयोग करें. सीमित समय के लिए लेआउट के बीच स्विच करें।", + "settings.general.row.newInterfaceNotice.title": "अब आप नए लेआउट का उपयोग कर रहे हैं", + "settings.general.row.newInterfaceNotice.description": "पिछला लेआउट अब उपलब्ध नहीं है", + "settings.general.row.newInterfaceNotice.dismiss": "हटाएँ", + "settings.general.row.pinchZoom.title": "ज़ूम करने के लिए पिंच करें", + "settings.general.row.pinchZoom.description": "ट्रैकपैड पिंच और Ctrl-स्क्रॉल जेस्चर को ज़ूम करने की अनुमति दें", + "settings.general.row.wayland.title": "नेटिव Wayland का उपयोग करें", + "settings.general.row.wayland.description": "Wayland पर X11 फ़ॉलबैक अक्षम करें। पुनः आरंभ करना आवश्यक है।", + "settings.general.row.wayland.tooltip": + "अलग-अलग रीफ़्रेश दर वाले मॉनिटर के साथ Linux पर नेटिव Wayland अधिक स्थिर हो सकता है।", + "settings.general.row.releaseNotes.title": "रिलीज़ नोट्स", + "settings.general.row.releaseNotes.description": "अपडेट के बाद 'नया क्या है' पॉपअप दिखाएँ", + "settings.updates.row.startup.title": "स्टार्टअप पर अपडेट की जाँच करें", + "settings.updates.row.startup.description": "OpenCode लॉन्च होने पर अपने-आप अपडेट की जाँच करें", + "settings.updates.row.check.title": "अद्यतन के लिए जाँच", + "settings.updates.row.check.description": "अपडेट के लिए मैन्युअल रूप से जाँच करें और यदि उपलब्ध हो तो इंस्टॉल करें", + "settings.updates.action.checkNow": "अभी जाँचें", + "settings.updates.action.checking": "जाँच हो रही है...", + "settings.updates.action.downloading": "डाउनलोड हो रहा है...", + "settings.updates.action.installing": "स्थापित किया जा रहा है...", + "settings.updates.toast.latest.title": "आपके पास नवीनतम संस्करण है", + "settings.updates.toast.latest.description": "आप OpenCode का नवीनतम संस्करण चला रहे हैं।", + "sound.option.none": "कोई नहीं", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "एजेंट", + "settings.general.notifications.agent.description": + "जब एजेंट पूरा हो जाए या ध्यान देने की आवश्यकता हो तो सिस्टम अधिसूचना दिखाएं", + "settings.general.notifications.permissions.title": "अनुमतियाँ", + "settings.general.notifications.permissions.description": "अनुमति की आवश्यकता होने पर सिस्टम अधिसूचना दिखाएं", + "settings.general.notifications.errors.title": "त्रुटियाँ", + "settings.general.notifications.errors.description": "त्रुटि होने पर सिस्टम अधिसूचना दिखाएं", + "settings.general.sounds.agent.title": "एजेंट", + "settings.general.sounds.agent.description": "जब एजेंट पूरा हो जाए या ध्यान देने की आवश्यकता हो तो ध्वनि बजाएं", + "settings.general.sounds.permissions.title": "अनुमतियाँ", + "settings.general.sounds.permissions.description": "अनुमति की आवश्यकता होने पर ध्वनि बजाएं", + "settings.general.sounds.errors.title": "त्रुटियाँ", + "settings.general.sounds.errors.description": "कोई त्रुटि होने पर ध्वनि बजाएं", + "settings.shortcuts.title": "कीबोर्ड शॉर्टकट", + "settings.shortcuts.reset.button": "डिफ़ॉल्ट पर पुनः सेट करें", + "settings.shortcuts.reset.toast.title": "शॉर्टकट रीसेट", + "settings.shortcuts.reset.toast.description": "कीबोर्ड शॉर्टकट को डिफ़ॉल्ट पर रीसेट कर दिया गया है।", + "settings.shortcuts.conflict.title": "शॉर्टकट पहले से ही उपयोग में है", + "settings.shortcuts.conflict.description": "{{keybind}} पहले से ही {{titles}} को सौंपा गया है।", + "settings.shortcuts.unassigned": "सौंपे नहीं गए", + "settings.shortcuts.pressKeys": "कुंजियाँ दबाएँ", + "settings.shortcuts.search.placeholder": "शॉर्टकट खोजें", + "settings.shortcuts.search.empty": "कोई शॉर्टकट नहीं मिला", + "settings.shortcuts.group.general": "सामान्य", + "settings.shortcuts.group.session": "सेशन", + "settings.shortcuts.group.navigation": "नेविगेशन", + "settings.shortcuts.group.modelAndAgent": "मॉडल और एजेंट", + "settings.shortcuts.group.terminal": "टर्मिनल", + "settings.shortcuts.group.prompt": "प्रॉम्प्ट", + "settings.providers.title": "प्रोवाइडर", + "settings.providers.description": "प्रोवाइडर सेटिंग्स यहां कॉन्फ़िगर करने योग्य होंगी।", + "settings.providers.section.connected": "कनेक्टेड प्रोवाइडर", + "settings.providers.connected.empty": "कोई कनेक्टेड प्रोवाइडर नहीं", + "settings.providers.connected.environmentDescription": "आपके एनवायरनमेंट वेरिएबल से कनेक्ट किया गया", + "settings.providers.section.popular": "लोकप्रिय प्रोवाइडर", + "settings.providers.custom.description": "आधार URL द्वारा एक OpenAI-संगत प्रोवाइडर जोड़ें।", + "settings.providers.tag.environment": "एनवायरनमेंट", + "settings.providers.tag.config": "कॉन्फ़िगरेशन", + "settings.providers.tag.custom": "कस्टम", + "settings.providers.tag.other": "अन्य", + "settings.models.title": "मॉडल", + "settings.models.description": "मॉडल सेटिंग्स यहां कॉन्फ़िगर करने योग्य होंगी।", + "settings.agents.title": "एजेंट", + "settings.agents.description": "एजेंट सेटिंग्स यहां कॉन्फ़िगर करने योग्य होंगी।", + "settings.commands.title": "कमांड", + "settings.commands.description": "कमांड सेटिंग्स यहां कॉन्फ़िगर करने योग्य होंगी।", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP सेटिंग्स यहां कॉन्फ़िगर करने योग्य होंगी।", + "settings.permissions.title": "अनुमतियाँ", + "settings.permissions.description": "नियंत्रित करें कि सर्वर डिफ़ॉल्ट रूप से कौन से टूल का उपयोग कर सकता है।", + "settings.permissions.section.tools": "टूल", + "settings.permissions.toast.updateFailed.title": "अनुमतियाँ अद्यतन करने में विफल", + "settings.permissions.action.allow": "अनुमति दें", + "settings.permissions.action.ask": "पूछें", + "settings.permissions.action.deny": "अस्वीकार करें", + "settings.permissions.tool.read.title": "पढ़ें", + "settings.permissions.tool.read.description": "फ़ाइल पढ़ना (फ़ाइल पथ से मेल खाता है)", + "settings.permissions.tool.edit.title": "संपादित करें", + "settings.permissions.tool.edit.description": "संपादन, लेखन और पैच सहित फ़ाइलों को संशोधित करें", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "ग्लोब पैटर्न का उपयोग करके फ़ाइलों का मिलान करें", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "रेगुलर एक्सप्रेशन का उपयोग करके फ़ाइल सामग्री खोजें", + "settings.permissions.tool.list.title": "सूची", + "settings.permissions.tool.list.description": "किसी निर्देशिका में फ़ाइलों की सूची बनाएं", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "शेल कमांड चलाएँ", + "settings.permissions.tool.task.title": "कार्य", + "settings.permissions.tool.task.description": "उप-एजेंट लॉन्च करें", + "settings.permissions.tool.skill.title": "कौशल", + "settings.permissions.tool.skill.description": "किसी कौशल को नाम से लोड करें", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "भाषा सर्वर क्वेरी चलाएँ", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "कार्य सूची अद्यतन करें", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "URL से सामग्री प्राप्त करें", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "वेब खोज", + "settings.permissions.tool.external_directory.title": "बाह्य निर्देशिका", + "settings.permissions.tool.external_directory.description": "प्रोजेक्ट निर्देशिका के बाहर फ़ाइलों तक पहुँचें", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "समान इनपुट के साथ बार-बार टूल कॉल का पता लगाएं", + "session.delete.failed.title": "सेशन को हटाने में विफल", + "session.delete.title": "सेशन हटाएं", + "session.delete.confirm": 'सेशन "{{name}}" हटाएं?', + "session.delete.button": "सेशन हटाएं", + "workspace.new": "नया वर्कस्पेस", + "workspace.type.local": "स्थानीय", + "workspace.type.sandbox": "सैंडबॉक्स", + "workspace.create.failed.title": "वर्कस्पेस बनाने में विफल", + "workspace.delete.failed.title": "वर्कस्पेस को हटाने में विफल", + "workspace.resetting.title": "वर्कस्पेस को रीसेट करना", + "workspace.resetting.description": "इसमें एक मिनट लग सकता है।", + "workspace.reset.failed.title": "वर्कस्पेस को रीसेट करने में विफल", + "workspace.reset.success.title": "वर्कस्पेस रीसेट", + "workspace.reset.success.description": "वर्कस्पेस अब डिफ़ॉल्ट शाखा से मेल खाता है।", + "workspace.error.stillPreparing": "वर्कस्पेस अभी भी तैयारी कर रहा है", + "workspace.status.checking": "मर्ज न किए गए परिवर्तनों की जाँच हो रही है...", + "workspace.status.error": "Git स्थिति सत्यापित नहीं की जा सकी।", + "workspace.status.clean": "मर्ज न किए गए कोई परिवर्तन नहीं मिले।", + "workspace.status.dirty": "इस वर्कस्पेस में मर्ज न किए गए परिवर्तन मिले।", + "workspace.delete.title": "वर्कस्पेस हटाएं", + "workspace.delete.confirm": 'वर्कस्पेस "{{name}}" हटाएं?', + "workspace.delete.button": "वर्कस्पेस हटाएं", + "workspace.reset.title": "वर्कस्पेस रीसेट करें", + "workspace.reset.confirm": 'वर्कस्पेस "{{name}}" रीसेट करें?', + "workspace.reset.button": "वर्कस्पेस रीसेट करें", + "workspace.reset.archived.none": "कोई सक्रिय सेशन संग्रहित नहीं किया जाएगा।", + "workspace.reset.archived.one": "1 सेशन संग्रहित किया जाएगा।", + "workspace.reset.archived.many": "{{count}} सेशन संग्रहित किए जाएँगे।", + "workspace.reset.note": "यह डिफ़ॉल्ट शाखा से मिलान करने के लिए वर्कस्पेस को रीसेट कर देगा।", +} diff --git a/packages/app/src/i18n/hr.ts b/packages/app/src/i18n/hr.ts new file mode 100644 index 0000000000000000000000000000000000000000..2130c88ada2d6aa82a0668db70fea485ef60e34c --- /dev/null +++ b/packages/app/src/i18n/hr.ts @@ -0,0 +1,1168 @@ +import { DESKTOP_NATIVE_KEYS } from "./desktop-native" + +const desktop = [ + "OpenCode", + "Datoteka", + "Uredi", + "Prikaz", + "Idi", + "Prozor", + "Pomoć", + "Provjeri ima li ažuriranja...", + "Postavke", + "Ponovno učitaj Webview", + "Ponovno pokreni", + "Izvezi zapisnike...", + "Nova sesija", + "Otvori projekt...", + "Novi prozor", + "Zatvori prozor", + "Poništi", + "Ponovi", + "Izreži", + "Kopiraj", + "Zalijepi", + "Izbriši", + "Odaberi sve", + "Prikaži ili sakrij bočnu traku", + "Prikaži ili sakrij terminal", + "Prikaži ili sakrij stablo datoteka", + "Ponovno učitaj", + "Prikaži ili sakrij razvojne alate", + "Stvarna veličina", + "Povećaj", + "Smanji", + "Prikaži ili sakrij prikaz preko cijelog zaslona", + "Natrag", + "Naprijed", + "Prethodna sesija", + "Sljedeća sesija", + "Prethodni projekt", + "Sljedeći projekt", + "Minimiziraj", + "Maksimiziraj", + "Dokumentacija za OpenCode", + "Forum za podršku", + "Pošalji povratne informacije", + "Prijavi pogrešku", + "Izbornik OpenCode", + "Provjera ažuriranja nije uspjela.", + "Pogreška pri ažuriranju", + "Imate najnoviju verziju.", + "Nema ažuriranja", + "Ažuriranje {{version}} je preuzeto. Ponovno pokrenuti sada?", + "Ažuriranje je spremno", + "Ponovno pokreni", + "Kasnije", + "Ponovno pokreni aplikaciju", + "Izvezi zapisnike", + "Nastavi čekati", + "Izađi", + "OpenCode se nije uspio učitati", + "Prozor aplikacije OpenCode neočekivano je zatvoren", + "OpenCode ne reagira", + "Možete ponovno pokrenuti aplikaciju, otvoriti zapisnike ili nastaviti čekati.", + "Prozor: {{window}}\nURL: {{url}}\nPogreška: {{code}} {{description}}", + "Prozor: {{window}}\nRazlog: {{reason}}\nKod: {{code}}", + "", + "Odaberite mapu", + "Odaberite datoteku", + "Spremi datoteku", + "Datoteke", + "Lokalni poslužitelj", + "WSL je dostupan samo u sustavu Windows", + "WSL nije dostupan", + "Nije uspjelo prikazivanje instaliranih WSL distribucija", + "Nije uspjelo prikazivanje dostupnih WSL distribucija", + "Nije moguće izvršiti naredbe u distribuciji", + "Instalacija WSL-a nije uspjela", + "Instalacija distribucije nije uspjela: {{distro}}", + "Instalacija aplikacije OpenCode nije uspjela", + "Distribucija {{distro}} već je dodana", + "opencode nije instaliran u ovoj distribuciji", + "opencode je instaliran, ali se ne može pokrenuti", + "OpenCode nije instaliran u distribuciji {{distro}}", + "Ažuriranje aplikacije OpenCode je završeno, ali {{distro}} i dalje prijavljuje {{installed}}; očekivano je {{expected}}", + "nema verzije", + "WSL poslužitelj završio je rad nakon pokretanja (code={{code}} signal={{signal}})", + "WSL poslužitelj završio je rad prije nego što je postao dostupan (code={{code}} signal={{signal}}){{output}}", + "Isteklo je vrijeme provjere dostupnosti komponente Sidecar za {{distro}} nakon {{timeout}} ms", + "Isteklo je vrijeme za {{command}} {{args}} nakon {{timeout}} ms", + "Nije uspjelo dohvaćanje priključka", + "Datoteka nije odabrana u izborniku", + "Odabrani prilozi premašuju ograničenje od {{limit}} MB", +] + +export const dict = { + ...Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key, index) => [key, desktop[index]])), + "command.category.suggested": "Predloženo", + "command.category.view": "Prikaz", + "command.category.project": "Projekt", + "command.category.provider": "Pružatelj usluga", + "command.category.server": "Poslužitelj", + "command.category.session": "Sesija", + "command.category.theme": "Tema", + "command.category.language": "Jezik", + "command.category.file": "Datoteka", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Dozvole", + "command.category.workspace": "Radni prostor", + "command.category.settings": "Postavke", + "command.logs.export": "Izvoz zapisnika", + "theme.scheme.system": "Sustav", + "theme.scheme.light": "Svijetlo", + "theme.scheme.dark": "Tamno", + "command.sidebar.toggle": "Uključi/isključi bočnu traku", + "command.project.open": "Otvori projekt", + "command.project.previous": "Prethodni projekt", + "command.project.next": "Sljedeći projekt", + "command.project.index": "Prijeđi na projekt {{index}}", + "command.provider.connect": "Poveži pružatelja usluga", + "command.server.switch": "Promijeni poslužitelj", + "command.settings.open": "Otvori postavke", + "command.session.previous": "Prethodna sesija", + "command.session.next": "Sljedeća sesija", + "command.session.previous.unseen": "Prethodna nepročitana sesija", + "command.session.next.unseen": "Sljedeća nepročitana sesija", + "command.session.archive": "Arhiviraj sesiju", + "command.palette": "Paleta naredbi", + "command.theme.cycle": "Promijeni temu", + "command.theme.set": "Koristite temu: {{theme}}", + "command.theme.scheme.cycle": "Promijeni shemu boja", + "command.theme.scheme.set": "Koristite shemu boja: {{scheme}}", + "command.language.cycle": "Promijeni jezik", + "command.language.set": "Koristite jezik: {{language}}", + "command.session.new": "Nova sesija", + "command.file.open": "Otvori datoteku", + "command.tab.close": "Zatvori karticu", + "command.tab.reopenClosed": "Ponovno otvori zatvorenu karticu", + "command.context.addSelection": "Dodajte odabir kontekstu", + "command.context.addSelection.description": "Dodaj odabrane retke iz trenutne datoteke", + "command.input.focus": "Fokusiraj unos", + "command.terminal.toggle": "Prebaci terminal", + "command.fileTree.toggle": "Uključi/isključi stablo datoteka", + "command.review.toggle": "Uključi pregled", + "command.terminal.new": "Novi terminal", + "command.terminal.new.description": "Napravite novu karticu terminala", + "command.steps.toggle": "Prebacivanje koraka", + "command.steps.toggle.description": "Prikažite ili sakrijte korake za trenutnu poruku", + "command.message.previous": "Prethodna poruka", + "command.message.previous.description": "Idi na prethodnu korisničku poruku", + "command.message.next": "Sljedeća poruka", + "command.message.next.description": "Idi na sljedeću korisničku poruku", + "command.model.choose": "Odaberite model", + "command.model.choose.description": "Odaberite drugi model", + "command.mcp.toggle": "Uključi/isključi MCPs", + "command.mcp.toggle.description": "Uključi/isključi MCPs", + "command.agent.cycle": "Promijeni agenta", + "command.agent.cycle.description": "Prijeđite na sljedećeg agenta", + "command.agent.cycle.reverse": "Vrati se na prethodnog agenta", + "command.agent.cycle.reverse.description": "Prijeđi na prethodnog agenta", + "command.model.variant.cycle": "Ciklusni napor razmišljanja", + "command.model.variant.cycle.description": "Prijeđite na sljedeću razinu napora", + "command.prompt.mode.shell": "Ljuska", + "command.prompt.mode.normal": "Upit", + "command.permissions.autoaccept.enable": "Automatsko prihvaćanje dopuštenja", + "command.permissions.autoaccept.disable": "Zaustavi automatsko prihvaćanje dozvola", + "command.workspace.toggle": "Uključivanje/isključivanje radnih prostora", + "command.workspace.toggle.description": "Omogućite ili onemogućite više radnih prostora na bočnoj traci", + "command.session.undo": "Poništi", + "command.session.undo.description": "Poništavanje zadnje poruke", + "command.session.redo": "Ponovi", + "command.session.redo.description": "Ponovite zadnju poništenu poruku", + "command.session.compact": "Sažmi sesiju", + "command.session.compact.description": "Sažmite sesiju kako biste smanjili veličinu konteksta", + "command.session.fork": "Odvoji od poruke", + "command.session.fork.description": "Stvorite novu sesiju iz prethodne poruke", + "command.session.share": "Podijeli sesiju", + "command.session.share.description": "Podijelite ovu sesiju i kopirajte URL u međuspremnik", + "command.session.unshare": "Poništi dijeljenje sesije", + "command.session.unshare.description": "Prestani dijeliti ovu sesiju", + "command.session.export": "Izvezi sesiju", + "command.session.export.description": "Izvezite puni transkript sesije kao JSON", + "palette.search.placeholder": "Pretraživanje datoteka, naredbi i sesija", + "palette.search.placeholder.home": "Naredbe i sesije pretraživanja", + "palette.empty": "Nema pronađenih rezultata", + "palette.group.commands": "Naredbe", + "palette.group.files": "Datoteke", + "dialog.provider.search.placeholder": "Pružatelji usluga pretraživanja", + "dialog.provider.empty": "Nije pronađen nijedan pružatelj usluga", + "dialog.provider.group.popular": "Popularan", + "dialog.provider.group.other": "ostalo", + "dialog.provider.custom.label": "Prilagođeni pružatelj kompatibilan sa OpenAI", + "dialog.provider.tag.recommended": "Preporučeno", + "dialog.provider.opencode.note": "Odabrani modeli uključujući Claude, GPT, Gemini i druge", + "dialog.provider.opencode.tagline": "Pouzdani optimizirani modeli", + "dialog.provider.opencodeGo.tagline": "Niska cijena pretplate za sve", + "dialog.provider.anthropic.note": "Izravan pristup Claude modelima, uključujući Pro i Max", + "dialog.provider.copilot.note": "AI modeli za pomoć kodiranja putem GitHub Copilot", + "dialog.provider.openai.note": "GPT modeli za brze, sposobne opće AI zadatke", + "dialog.provider.google.note": "Gemini modeli za brze, strukturirane odgovore", + "dialog.provider.openrouter.note": "Pristupite svim podržanim modelima od jednog pružatelja usluga", + "dialog.provider.vercel.note": "Unificirani pristup AI modelima s pametnim usmjeravanjem", + "dialog.model.select.title": "Odaberite model", + "dialog.model.search.placeholder": "Traži modele", + "dialog.model.empty": "Nema rezultata modela", + "dialog.model.manage": "Upravljanje modelima", + "dialog.model.manage.description": "Prilagodite koji se modeli pojavljuju u biraču modela.", + "dialog.model.manage.provider.toggle": "Uključi sve {{provider}} modele", + "dialog.model.unpaid.freeModels.title": "Besplatne modele nudi OpenCode", + "dialog.model.unpaid.addMore.title": "Dodajte još modela popularnih dobavljača", + "dialog.model.unpaid.viewMoreProviders": "Pogledajte više od 70 pružatelja usluga", + "dialog.provider.viewAll": "Prikaži više pružatelja usluga", + "provider.connect.title": "Povežite {{provider}}", + "provider.connect.title.anthropicProMax": "Prijavite se sa Claude Pro/Max", + "provider.connect.selectMethod": "Odaberite način prijave za {{provider}}.", + "provider.connect.method.apiKey": "Ključ API", + "provider.connect.method.browser": "preglednik", + "provider.connect.method.headless": "Bezglav", + "provider.connect.status.inProgress": "Autorizacija u tijeku...", + "provider.connect.status.waiting": "Čeka se autorizacija...", + "provider.connect.status.failed": "Autorizacija nije uspjela: {{error}}", + "provider.connect.apiKey.description": + "Enter vaš {{provider}} API ključ za povezivanje vašeg računa i korištenje {{provider}} modela u OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API ključ", + "provider.connect.apiKey.placeholder": "Ključ API", + "provider.connect.apiKey.required": "Potreban je ključ API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen vam daje pristup odabranom skupu pouzdanih optimiziranih modela za agente za kodiranje.", + "provider.connect.opencodeZen.line2": + "S jednim ključem API dobit ćete pristup modelima kao što su Claude, GPT, Gemini, GLM i još mnogo toga.", + "provider.connect.opencodeZen.visit.prefix": "Posjetiti", + "provider.connect.opencodeZen.visit.link": "otvoreni kod.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "da preuzmete svoj ključ API.", + "provider.connect.oauth.code.visit.prefix": "Posjetiti", + "provider.connect.oauth.code.visit.link": "ovaj link", + "provider.connect.oauth.code.visit.suffix": + "za prikupljanje vašeg autorizacijskog koda za povezivanje vašeg računa i korištenje {{provider}} modela u OpenCode.", + "provider.connect.oauth.code.label": "{{method}} autorizacijski kod", + "provider.connect.oauth.code.placeholder": "Autorizacijski kod", + "provider.connect.oauth.code.required": "Potreban je autorizacijski kod", + "provider.connect.oauth.code.invalid": "Nevažeći autorizacijski kod", + "provider.connect.oauth.auto.visit.prefix": "Posjetiti", + "provider.connect.oauth.auto.visit.link": "ovaj link", + "provider.connect.oauth.auto.visit.suffix": + "i unesite kod ispod kako biste povezali svoj račun i koristili {{provider}} modele u OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Kod za potvrdu", + "provider.connect.toast.connected.title": "{{provider}} povezan", + "provider.connect.toast.connected.description": "{{provider}} modeli sada su dostupni za korištenje.", + "provider.custom.title": "Prilagođeni pružatelj usluga", + "provider.custom.unavailable": "Prilagođeni pružatelji usluga nisu dostupni na ovom poslužitelju", + "provider.custom.description.prefix": "Konfigurirajte OpenAI-kompatibilnog pružatelja usluga. Vidite", + "provider.custom.description.link": "konfiguracijski dokumenti pružatelja usluga", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID davatelja usluga", + "provider.custom.field.providerID.placeholder": "moj davatelj usluga", + "provider.custom.field.providerID.description": "Mala slova, brojevi, crtice ili podvlake", + "provider.custom.field.name.label": "Ime za prikaz", + "provider.custom.field.name.placeholder": "Moj AI Provider", + "provider.custom.field.baseURL.label": "Baza URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "Ključ API", + "provider.custom.field.apiKey.placeholder": "Ključ API", + "provider.custom.field.apiKey.description": + "Neobavezno. Ostavite prazno ako autentifikacijom upravljate putem zaglavlja.", + "provider.custom.models.label": "Modeli", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Ime", + "provider.custom.models.name.placeholder": "Ime za prikaz", + "provider.custom.models.remove": "Ukloni model", + "provider.custom.models.add": "Dodajte model", + "provider.custom.headers.label": "Zaglavlja (nije obavezno)", + "provider.custom.headers.key.label": "Zaglavlje", + "provider.custom.headers.key.placeholder": "Zaglavlje-Naziv", + "provider.custom.headers.value.label": "Vrijednost", + "provider.custom.headers.value.placeholder": "vrijednost", + "provider.custom.headers.remove": "Ukloni zaglavlje", + "provider.custom.headers.add": "Dodajte zaglavlje", + "provider.custom.error.providerID.required": "Potreban je ID davatelja usluga", + "provider.custom.error.providerID.format": "Koristite mala slova, brojeve, crtice ili podvlake", + "provider.custom.error.providerID.exists": "Taj ID davatelja već postoji", + "provider.custom.error.name.required": "Potrebno je ime za prikaz", + "provider.custom.error.baseURL.required": "Potrebna je baza URL", + "provider.custom.error.baseURL.format": "Mora započeti s http:// ili https://", + "provider.custom.error.required": "Potreban", + "provider.custom.error.duplicate": "Duplikat", + "provider.disconnect.toast.disconnected.title": "{{provider}} isključen", + "provider.disconnect.toast.disconnected.description": "{{provider}} modeli više nisu dostupni.", + "model.tag.free": "Besplatno", + "model.tag.latest": "Najnoviji", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekst", + "model.input.image": "slika", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Dopušta: {{inputs}}", + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Unosi", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Pružatelj usluga", + "model.tooltip.reasoning": "Rasuđivanje", + "model.tooltip.reasoning.allowed": "Omogućuje rasuđivanje", + "model.tooltip.reasoning.none": "Bez obrazloženja", + "model.tooltip.context": "Ograničenje konteksta {{limit}}", + "common.search.placeholder": "Pretraživanje", + "common.goBack": "Navigacija natrag", + "common.goForward": "Navigacija naprijed", + "common.loading": "Učitavanje", + "common.loading.ellipsis": "...", + "common.cancel": "Otkazati", + "common.clear": "Jasan", + "common.open": "Otvoriti", + "common.connect": "Poveži se", + "common.disconnect": "Prekini vezu", + "common.continue": "Nastaviti", + "common.submit": "Pošalji", + "common.save": "Uštedjeti", + "common.saving": "Spremanje...", + "common.default": "Zadano", + "common.attachment": "privitak", + "prompt.placeholder.shell": "Unesite naredbu ljuske... {{example}}", + "prompt.placeholder.normal": 'Pitajte bilo što... "{{example}}"', + "prompt.placeholder.simple": "Pitajte bilo što...", + "prompt.placeholder.summarizeComments": "Sažeti komentare…", + "prompt.placeholder.summarizeComment": "Sažeti komentar…", + "prompt.mode.shell": "Ljuska", + "prompt.mode.normal": "Upit", + "prompt.mode.shell.exit": "esc za izlaz", + "session.child.promptDisabled": "Sesije subagenta ne mogu se tražiti.", + "session.child.backToParent": "Povratak na glavnu sesiju.", + "prompt.example.1": "Ispravite TODO u bazi koda", + "prompt.example.2": "Koji je tehnološki skup ovog projekta?", + "prompt.example.3": "Popravi pokvarene testove", + "prompt.example.4": "Objasnite kako funkcionira autentifikacija", + "prompt.example.5": "Pronađite i popravite sigurnosne propuste", + "prompt.example.6": "Dodajte jedinične testove za korisničku uslugu", + "prompt.example.7": "Prepravite ovu funkciju da bude čitljivija", + "prompt.example.8": "Što ova greška znači?", + "prompt.example.9": "Pomozite mi otkloniti ovu pogrešku", + "prompt.example.10": "Generirajte API dokumentaciju", + "prompt.example.11": "Optimizirajte upite baze podataka", + "prompt.example.12": "Dodajte potvrdu unosa", + "prompt.example.13": "Stvorite novu komponentu za...", + "prompt.example.14": "Kako mogu implementirati ovaj projekt?", + "prompt.example.15": "Pregledajte moj kod radi najboljih praksi", + "prompt.example.16": "Dodajte rukovanje pogreškama ovoj funkciji", + "prompt.example.17": "Objasnite ovaj uzorak regularnog izraza", + "prompt.example.18": "Pretvorite ovo u TypeScript", + "prompt.example.19": "Dodajte bilježenje kroz bazu koda", + "prompt.example.20": "Koje su ovisnosti zastarjele?", + "prompt.example.21": "Pomozite mi napisati skriptu za migraciju", + "prompt.example.22": "Implementirajte predmemoriranje za ovu krajnju točku", + "prompt.example.23": "Dodajte paginaciju ovom popisu", + "prompt.example.24": "Kreirajte naredbu CLI za...", + "prompt.example.25": "Kako ovdje rade varijable okruženja?", + "prompt.popover.emptyResults": "Nema odgovarajućih rezultata", + "prompt.popover.emptyCommands": "Nema odgovarajućih naredbi", + "prompt.dropzone.label": "Ovdje ispustite slike, PDF-ove ili tekstualne datoteke", + "prompt.dropzone.file.label": "Ispustite u datoteku @mention", + "prompt.slash.badge.custom": "običaj", + "prompt.slash.badge.skill": "vještina", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktivan", + "prompt.context.includeActiveFile": "Uključi aktivnu datoteku", + "prompt.context.removeActiveFile": "Ukloni aktivnu datoteku iz konteksta", + "prompt.context.removeFile": "Ukloni datoteku iz konteksta", + "prompt.action.attachFile": "Dodajte datoteke", + "prompt.menu.addImagesAndFiles": "Dodajte datoteke i više", + "prompt.menu.imagesAndFiles": "Slike i datoteke", + "prompt.menu.commands": "Naredbe", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Naredba ljuske", + "prompt.attachment.remove": "Ukloni privitak", + "prompt.action.send": "Poslati", + "prompt.action.stop": "Zaustavi", + "prompt.toast.pasteUnsupported.title": "Nepodržani privitak", + "prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.", + "prompt.toast.attachmentDuplicate.title": "Ova datoteka je već učitana", + "prompt.toast.modelAgentRequired.title": "Odaberite agenta i model", + "prompt.toast.modelAgentRequired.description": "Odaberite agenta i model prije slanja upita.", + "prompt.toast.worktreeCreateFailed.title": "Izrada radnog stabla nije uspjela", + "prompt.toast.sessionCreateFailed.title": "Stvaranje sesije nije uspjelo", + "prompt.toast.shellSendFailed.title": "Slanje naredbe ljuske nije uspjelo", + "prompt.toast.commandSendFailed.title": "Slanje naredbe nije uspjelo", + "prompt.toast.promptSendFailed.title": "Slanje upita nije uspjelo", + "prompt.toast.promptSendFailed.description": "Nije moguće dohvatiti sesiju", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} od {{total}} omogućen", + "dialog.mcp.empty": "Nema konfiguriranih MCP-ova", + "dialog.lsp.empty": "LSPs automatski detektiran iz vrsta datoteka", + "dialog.plugins.empty": "Dodaci konfigurirani u opencode.json", + "mcp.status.connected": "povezan", + "mcp.status.failed": "nije uspio", + "mcp.status.needs_auth": "treba autorizaciju", + "mcp.status.disabled": "onemogućeno", + "mcp.auth.clickToAuthenticate": "Kliknite za autentifikaciju", + "dialog.fork.empty": "Nema poruka za račvanje", + "dialog.directory.search.placeholder": "Traži mape", + "dialog.directory.empty": "Nema pronađenih mapa", + "dialog.directory.action.selectFile": "Odaberite datoteku", + "dialog.directory.action.selectFolder": "Odaberite mapu", + "dialog.directory.root": "Korijen", + "dialog.directory.parent": "Roditelj", + "dialog.directory.readError": "Nije moguće pročitati ovu mapu", + "app.server.unreachable": "Nije moguće doći do {{server}}", + "app.server.retrying": "Automatski ponovni pokušaj...", + "app.server.otherServers": "Ostali poslužitelji", + "dialog.server.title": "poslužitelji", + "dialog.server.description": "Promijenite na koji se OpenCode poslužitelj povezuje ova aplikacija.", + "dialog.server.search.placeholder": "Pretraživanje poslužitelja", + "dialog.server.empty": "Još nema poslužitelja", + "dialog.server.add.title": "Dodaj poslužitelj", + "dialog.server.add.url": "Adresa poslužitelja", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Nije moguće spojiti se na poslužitelj", + "dialog.server.add.checking": "Provjera...", + "dialog.server.add.button": "Dodaj poslužitelj", + "dialog.server.add.name": "Naziv poslužitelja (neobavezno)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "korisničko ime (nije obavezno)", + "dialog.server.add.usernamePlaceholder": "korisničko ime", + "dialog.server.add.password": "Lozinka (nije obavezno)", + "dialog.server.add.passwordPlaceholder": "lozinka", + "dialog.server.edit.title": "Uredi poslužitelj", + "dialog.server.default.title": "Zadani poslužitelj", + "dialog.server.default.description": + "Povežite se s ovim poslužiteljem prilikom pokretanja aplikacije umjesto pokretanja lokalnog poslužitelja. Zahtijeva ponovno pokretanje.", + "dialog.server.default.none": "Nijedan poslužitelj nije odabran", + "dialog.server.default.set": "Postavi trenutni poslužitelj kao zadani", + "dialog.server.default.clear": "Jasan", + "dialog.server.action.remove": "Ukloni poslužitelj", + "dialog.server.menu.edit": "Uredi", + "dialog.server.menu.default": "Postavi kao zadano", + "dialog.server.menu.defaultRemove": "Ukloni zadano", + "dialog.server.menu.delete": "Izbriši", + "dialog.server.current": "Trenutni poslužitelj", + "dialog.server.status.default": "Zadano", + "wsl.server.add": "Dodajte WSL poslužitelj", + "wsl.server.addShort": "Dodaj WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL poslužitelj", + "wsl.server.retryStart": "Ponovi početak", + "wsl.server.updating": "Ažuriranje...", + "wsl.onboarding.step.distro": "Odaberite distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Provjera WSL...", + "wsl.onboarding.restartRequired": "Windows treba ponovno pokretanje kako bi završio instalaciju WSL.", + "wsl.onboarding.ready": "WSL je spreman.", + "wsl.onboarding.required": "Za nastavak je potreban WSL.", + "wsl.onboarding.checkingDistros": "Provjera distribucija...", + "wsl.onboarding.installingDistro": "Instaliranje {{distro}}...", + "wsl.onboarding.checkingDistro": "Provjera {{distro}}...", + "wsl.onboarding.listingDistros": "Popis distribucija...", + "wsl.onboarding.distroReady": "{{distro}} je spreman.", + "wsl.onboarding.distroNotInstalled": "{{distro}} još nije instaliran.", + "wsl.onboarding.openDistroOnce": "Otvorite {{distro}} jednom da završite postavljanje.", + "wsl.onboarding.finishingDistro": "Dovršavanje postavljanja za {{distro}}.", + "wsl.onboarding.pickDistro": "Odaberite distribuciju ili je instalirajte u nastavku.", + "wsl.onboarding.checkingOpencode": "Provjera OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Provjera OpenCode u {{distro}}...", + "wsl.onboarding.updatingOpencode": "Ažuriranje OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Ažuriranje OpenCode u {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Ažurirajte OpenCode u {{distro}}.", + "wsl.onboarding.updateOpencode": "Ažurirajte OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode je spreman u {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode je spreman.", + "wsl.onboarding.installOpencodeIn": "Instalirajte OpenCode u {{distro}}.", + "wsl.onboarding.installOpencode": "Instalirajte OpenCode", + "wsl.onboarding.chooseDistroFirst": "Prvo odaberite distribuciju.", + "wsl.onboarding.loadFailed": "Nije uspjelo učitavanje stanja WSL.", + "wsl.onboarding.loading": "Učitavanje...", + "wsl.onboarding.installedDistros": "Instalirane distribucije", + "wsl.onboarding.checkAgain": "Provjerite ponovno", + "wsl.onboarding.distroStatus.ready": "Spreman", + "wsl.onboarding.distroStatus.checking": "Provjera...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode nije instaliran", + "wsl.onboarding.distroStatus.missingTools": "Nedostaje bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Nepodržano · Koristite WSL 2", + "wsl.onboarding.needAnotherDistro": "Trebate drugu distribuciju?", + "wsl.onboarding.needAnotherDistroHint": "Instalirajte Linux distribuciju iz WSL kataloga", + "wsl.onboarding.wslNotInstalled.title": "WSL nije instaliran", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows podsustav za Linux) je potreban prije nego što OpenCode može dodati WSL poslužitelj", + "wsl.onboarding.wslUnavailable.title": "WSL nedostupan", + "wsl.onboarding.wslUnavailable.description": "OpenCode nije mogao potvrditi WSL na ovom stroju.", + "wsl.onboarding.installWsl": "Instalirajte WSL", + "wsl.onboarding.windowsRestartRequired": + "Ponovo pokrenite Windows da dovršite instalaciju WSL, zatim ponovno otvorite OpenCode.", + "wsl.onboarding.next": "Sljedeći", + "wsl.onboarding.refresh": "Osvježiti", + "wsl.onboarding.allDistrosAdded": "Sve instalirane distribucije su već dodane.", + "wsl.onboarding.noDistros": "Još nije otkrivena nijedna distribucija.", + "wsl.onboarding.install": "Instalirati", + "wsl.onboarding.installing": "Instaliranje...", + "wsl.onboarding.installDistro": "Instalirajte distro", + "wsl.onboarding.searchDistros": "Pretražite distribucije", + "wsl.onboarding.wsl2Required": "Potreban je WSL 2.", + "wsl.onboarding.toolsRequired": "Ova distribucija treba bash i curl.", + "wsl.onboarding.openTerminal": "Otvori terminal", + "wsl.onboarding.path": "Put: {{path}}", + "wsl.onboarding.notFound": "nije pronađeno", + "wsl.onboarding.version": "Verzija: {{version}}", + "wsl.onboarding.unknown": "nepoznato", + "wsl.onboarding.desktopVersion": "radna površina {{version}}", + "wsl.onboarding.versionMismatch": "Instalirana verzija ne odgovara verziji aplikacije za stolna računala.", + "wsl.onboarding.adding": "Dodavanje...", + "help.tabs.toast.ariaLabel": "Predstavljamo Tabs. Organizirajte svoj rad i aktivne sesije pomoću kartica", + "help.tabs.toast.dismiss": "Odbaci Tab informacije", + "help.tabs.title": "Predstavljamo Tabs", + "help.tabs.description": "Organizirajte svoj rad i aktivne sesije pomoću kartica", + "help.tabs.date": "14. srpnja", + "help.tabs.introduction": "Radna površina OpenCode sada je izgrađena oko kartica.", + "help.tabs.sessions": + "Započnite novu sesiju na kartici ili otvorite postojeću sesiju iz bilo kojeg od svojih projekata. Otvorite novu karticu kada započnete nešto novo i zatvorite je kada završite.", + "help.tabs.organize": + "Održavanje nekoliko otvorenih kartica olakšava organiziranje vaših aktivnih sesija. Preimenujte kartice u nešto nezaboravno ako ih planirate zadržati.", + "help.tabs.home": + "Sve svoje sesije i projekte pronaći ćete na novom Home zaslonu. Odabirom sesije otvara se u kartici.", + "help.tabs.persistence": "Kada ponovno otvorite aplikaciju, vaše kartice su i dalje otvorene.", + "help.tabs.worktrees": + "Novi dizajn još ne podržava Git Worktrees, uskoro će biti dostupan. Dakle, ako biste radije nastavili koristiti prethodni izgled, možete se prebacivati ​​između izgleda u Postavkama. Samo imajte na umu da će novi izgled postati trajan za nekoliko tjedana.", + "server.row.noUsername": "nema korisničkog imena", + "dialog.project.edit.title": "Uredi projekt", + "dialog.project.edit.name": "Ime", + "dialog.project.edit.icon": "Ikona", + "dialog.project.edit.icon.alt": "Ikona projekta", + "dialog.project.edit.icon.hint": "Pritisnite ili povucite sliku", + "dialog.project.edit.icon.recommended": "Preporučeno: 128x128px", + "dialog.project.edit.color": "Boja", + "dialog.project.edit.color.select": "Odaberite boju {{color}}", + "dialog.project.edit.worktree.startup": "Skripta za pokretanje radnog prostora", + "dialog.project.edit.worktree.startup.description": + "Pokreće se nakon stvaranja novog radnog prostora (radnog stabla).", + "dialog.project.edit.worktree.startup.placeholder": "npr. lepinja instalirati", + "dialog.releaseNotes.action.getStarted": "Započnite", + "dialog.releaseNotes.action.next": "Sljedeći", + "dialog.releaseNotes.action.hideFuture": "Nemoj ih ubuduće prikazivati", + "dialog.releaseNotes.media.alt": "Pregled izdanja", + "context.breakdown.title": "Raščlamba konteksta", + "context.breakdown.note": + 'Približna raščlamba ulaznih tokena. "Ostalo" uključuje definicije alata i režijske troškove.', + "context.breakdown.system": "Sustav", + "context.breakdown.user": "Korisnik", + "context.breakdown.assistant": "Pomoćnik", + "context.breakdown.tool": "Pozivi alata", + "context.breakdown.other": "ostalo", + "context.systemPrompt.title": "Sustavski upit", + "context.rawMessages.title": "Sirove poruke", + "context.export.session": "Izvezi sesiju", + "context.stats.session": "Sesija", + "context.stats.messages": "Poruke", + "context.stats.provider": "Pružatelj usluga", + "context.stats.model": "Model", + "context.stats.limit": "Ograničenje konteksta", + "context.stats.totalTokens": "Ukupno žetona", + "context.stats.usage": "Korištenje", + "context.stats.inputTokens": "Ulazni tokeni", + "context.stats.outputTokens": "Izlazni žetoni", + "context.stats.reasoningTokens": "Reasoning Tokeni", + "context.stats.cacheTokens": "Tokeni predmemorije (čitanje/pisanje)", + "context.stats.userMessages": "Korisničke poruke", + "context.stats.assistantMessages": "Poruke pomoćnika", + "context.stats.totalCost": "Ukupni trošak", + "context.stats.sessionCreated": "Sesija je stvorena", + "context.stats.lastActivity": "Zadnja aktivnost", + "context.usage.tokens": "Tokeni", + "context.usage.usage": "Korištenje", + "context.usage.cost": "trošak", + "context.usage.clickToView": "Kliknite za pregled konteksta", + "context.usage.view": "Pogledajte korištenje konteksta", + "language.en": "engleski", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "španjolski", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "poljski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "português (Brazil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "turski", + "toast.language.title": "Jezik", + "toast.language.description": "Prešao na {{language}}", + "toast.theme.title": "Tema je promijenjena", + "toast.scheme.title": "Shema boja", + "toast.workspace.enabled.title": "Radni prostori omogućeni", + "toast.workspace.enabled.description": "Više radnih stabala sada je prikazano na bočnoj traci", + "toast.workspace.disabled.title": "Radni prostori onemogućeni", + "toast.workspace.disabled.description": "Na bočnoj traci prikazano je samo glavno radno stablo", + "toast.permissions.autoaccept.on.title": "Automatsko prihvaćanje dopuštenja", + "toast.permissions.autoaccept.on.description": "Zahtjevi za dopuštenje bit će automatski odobreni", + "toast.permissions.autoaccept.off.title": "Zaustavljeno automatsko prihvaćanje dopuštenja", + "toast.permissions.autoaccept.off.description": "Za zahtjeve za dopuštenje bit će potrebno odobrenje", + "toast.model.none.title": "Nijedan model nije odabran", + "toast.model.none.description": "Povežite se s pružateljem usluga kako biste rezimirali ovu sesiju", + "toast.file.loadFailed.title": "Učitavanje datoteke nije uspjelo", + "toast.file.listFailed.title": "Popis datoteka nije uspio", + "toast.context.noLineSelection.title": "Bez odabira linije", + "toast.context.noLineSelection.description": "Najprije odaberite raspon redaka na kartici datoteke.", + "toast.session.share.copyFailed.title": "Kopiranje URL u međuspremnik nije uspjelo", + "toast.session.share.success.title": "Sesija podijeljena", + "toast.session.share.success.description": "Podijeli URL kopirano u međuspremnik!", + "toast.session.share.failed.title": "Dijeljenje sesije nije uspjelo", + "toast.session.share.failed.description": "Došlo je do pogreške prilikom dijeljenja sesije", + "toast.session.unshare.success.title": "Dijeljenje sesije je prekinuto", + "toast.session.unshare.success.description": "Dijeljenje sesije uspješno je prekinuto.", + "toast.session.unshare.failed.title": "Poništavanje dijeljenja sesije nije uspjelo", + "toast.session.unshare.failed.description": "Došlo je do pogreške prilikom prekida dijeljenja sesije", + "toast.session.export.success.title": "Sesija je izvezena", + "toast.session.export.success.description": "Spremljena sesija u {{filename}}", + "toast.session.export.failed.title": "Izvoz sesije nije uspio", + "toast.session.export.failed.description": "Došlo je do pogreške prilikom izvoza sesije", + "toast.session.listFailed.title": "Neuspješno učitavanje sesija za {{project}}", + "toast.project.reloadFailed.title": "Ponovno učitavanje {{project}} nije uspjelo", + "toast.update.title": "Ažuriranje dostupno", + "toast.update.description": "Nova verzija OpenCode ({{version}}) sada je dostupna za instalaciju.", + "toast.update.action.installRestart": "Instalirajte i ponovno pokrenite", + "toast.update.action.notYet": "Ne još", + "error.page.title": "Nešto nije u redu", + "error.page.description": "Došlo je do pogreške prilikom učitavanja aplikacije.", + "error.page.description.localServerStartup": "Došlo je do pogreške prilikom pokretanja lokalnog poslužitelja.", + "error.page.details.label": "Pojedinosti o pogrešci", + "error.page.action.restart": "Ponovno pokretanje", + "error.page.action.report": "Prijavi pogrešku", + "error.page.action.reported": "Prijavljena pogreška", + "error.page.action.exportLogs": "Izvoz zapisnika", + "error.page.action.checking": "Provjera...", + "error.page.action.checkUpdates": "Provjerite ima li ažuriranja", + "error.page.action.updateTo": "Ažurirajte na {{version}}", + "error.page.circular": "[Kružni]", + "error.page.report.prefix": "Prijavite ovu grešku OpenCode timu", + "error.page.report.discord": "na Discord", + "error.page.version": "Verzija: {{version}}", + "error.dev.rootNotFound": + "Korijenski element nije pronađen. Jeste li ga zaboravili dodati u svoj index.html? Ili je možda atribut id-a pogrešno napisan?", + "error.serverSync.connectFailed": + "Nije moguće spojiti se na poslužitelj. Postoji li poslužitelj koji radi na `{{url}}`?", + "error.serverSDK.noServerAvailable": "Nema dostupnog poslužitelja", + "error.serverSDK.serverNotAvailable": "Poslužitelj nije dostupan", + "error.childStore.persistedCacheCreateFailed": "Stvaranje trajne predmemorije nije uspjelo", + "error.childStore.persistedProjectMetadataCreateFailed": "Stvaranje trajnih metapodataka projekta nije uspjelo", + "error.childStore.persistedProjectIconCreateFailed": "Stvaranje trajne ikone projekta nije uspjelo", + "error.childStore.storeCreateFailed": "Izrada trgovine nije uspjela", + "directory.error.invalidUrl": "Nevažeći direktorij u URL.", + "error.chain.unknown": "Nepoznata greška", + "error.server.invalidConfiguration": "Nevažeća konfiguracija", + "error.chain.causedBy": "Uzrokovano:", + "error.chain.apiError": "API greška", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Može se ponoviti: {{retryable}}", + "error.chain.responseBody": "Tijelo odgovora:\n{{body}}", + "error.chain.didYouMean": "Jeste li mislili: {{suggestions}}", + "error.chain.modelNotFound": "Model nije pronađen: {{provider}}/{{model}}", + "error.chain.checkConfig": "Provjerite nazive dobavljača/modela svoje konfiguracije (opencode.json).", + "error.chain.mcpFailed": + 'MCP poslužitelj "{{name}}" nije uspio. Napomena, OpenCode još ne podržava MCP autentifikaciju.', + "error.chain.providerAuthFailed": "Provjera autentičnosti davatelja nije uspjela ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Inicijalizacija pružatelja "{{provider}}" nije uspjela. Provjerite vjerodajnice i konfiguraciju.', + "error.chain.configJsonInvalid": "Konfiguracijska datoteka na {{path}} nije važeća JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Konfiguracijska datoteka na {{path}} nije valjana JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Imenik "{{dir}}" u {{path}} nije valjan. Preimenujte direktorij u "{{suggestion}}" ili ga uklonite. Ovo je uobičajena tipfelerska pogreška.', + "error.chain.configFrontmatterError": "Neuspješno analiziranje prednjeg materijala u {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfiguracijska datoteka na {{path}} je nevažeća", + "error.chain.configInvalidWithMessage": "Konfiguracijska datoteka na {{path}} je nevažeća: {{message}}", + "notification.permission.title": "Potrebno dopuštenje", + "notification.permission.description": "{{sessionTitle}} u {{projectName}} treba dopuštenje", + "notification.question.title": "Pitanje", + "notification.question.description": "{{sessionTitle}} u {{projectName}} ima pitanje", + "notification.action.goToSession": "Idi na sesiju", + "notification.session.responseReady.title": "Odgovor spreman", + "notification.session.error.title": "Pogreška sesije", + "notification.session.error.fallbackDescription": "Došlo je do pogreške", + "home.recentProjects": "Nedavni projekti", + "home.empty.title": "Nema nedavnih projekata", + "home.empty.description": "Započnite otvaranjem lokalnog projekta", + "home.title": "Početna", + "home.projects": "Projekti", + "home.project.add": "Dodaj projekt", + "home.recentlyClosed": "Nedavno zatvoreno", + "home.server.collapse": "Sažmi projekte poslužitelja", + "home.server.expand": "Proširite poslužiteljske projekte", + "home.sessions.search.placeholder": "Sesije pretraživanja", + "home.sessions.search.placeholder.scoped": "Pretražite sesije u {{scope}}", + "home.sessions.search.sessions": "Sjednice", + "home.sessions.search.noResults": "Nema pronađenih sesija za {{query}}", + "home.sessions.empty": "Ovdje još nema ništa", + "home.sessions.empty.description": "Izradite sesiju da biste započeli", + "home.sessions.group.today": "Danas", + "home.sessions.group.yesterday": "Jučer", + "home.sessions.group.older": "Starije", + "home.providerTip": + "Povežite se s više od 75 pružatelja usluga za korištenje drugih modela, uključujući Claude, GPT, Gemini itd.", + "session.tab.session": "Sesija", + "session.tab.review": "Pregled", + "session.tab.context": "Kontekst", + "session.tab.unknown": "Nepoznata sesija", + "session.panel.reviewAndFiles": "Pregled i datoteke", + "session.error.notFound": "Ovu sesiju nije moguće pronaći", + "session.error.notFound.description": "Ova kartica ukazuje na sesiju koja više ne postoji na ovom poslužitelju.", + "session.error.notFound.closeTab": "Zatvori Tab", + "session.error.serverConnection": "Ne mogu se spojiti na ovaj poslužitelj", + "session.review.filesChanged": "Datoteke promijenjene {{count}}", + "session.review.change.one": "Promijeniti", + "session.review.change.other": "Promjene", + "session.review.loadingChanges": "Učitavanje promjena...", + "session.review.empty": "Još nema promjena u ovoj sesiji", + "session.review.noVcs": "Sustav kontrole verzija Git nije otkriven, promjene nisu prikazane", + "session.review.noVcs.createGit.title": "Napravite Git spremište", + "session.review.noVcs.createGit.description": "Pratite, pregledajte i poništite promjene u ovom projektu", + "session.review.noVcs.createGit.actionLoading": "Stvaranje Git repozitorija...", + "session.review.noVcs.createGit.action": "Napravi Git spremište", + "session.review.noSnapshot": "Praćenje snimke onemogućeno je u konfiguraciji, tako da promjene sesije nisu dostupne", + "session.review.noChanges": "Nema promjena", + "session.review.noUncommittedChanges": "Još nema neizvršenih promjena", + "session.review.noBranchChanges": "Još nema promjena grane", + "session.files.selectToOpen": "Odaberite datoteku za otvaranje", + "session.files.all": "Sve datoteke", + "session.files.empty": "Nema datoteka", + "session.files.binaryContent": "Binarna datoteka (sadržaj se ne može prikazati)", + "session.messages.renderEarlier": "Prikaži ranije poruke", + "session.messages.loadingEarlier": "Učitavanje ranijih poruka...", + "session.messages.loadEarlier": "Učitaj ranije poruke", + "session.messages.loading": "Učitavanje poruka...", + "session.messages.jumpToLatest": "Skoči na najnovije", + "session.context.addToContext": "Dodajte {{selection}} u kontekst", + "session.todo.title": "Zadaci", + "session.todo.collapse": "Kolaps", + "session.todo.expand": "Proširiti", + "session.todo.progress": "{{done}} od {{total}} zadataka dovršen", + "session.question.progress": "{{current}} od {{total}} pitanja", + "session.question.minimize": "Smanjite pitanje", + "session.question.restore": "Vrati pitanje", + "session.question.pending.one": "{{count}} pitanje na čekanju", + "session.question.pending.other": "{{count}} pitanja na čekanju", + "session.question.pending.few": "{{count}} pitanja na čekanju", + "session.followupDock.summary.one": "{{count}} poruka u čekanju", + "session.followupDock.summary.other": "{{count}} poruke u redu čekanja", + "session.followupDock.summary.few": "{{count}} poruke u redu čekanja", + "session.followupDock.sendNow": "Pošalji sada", + "session.followupDock.edit": "Uredi", + "session.followupDock.collapse": "Sažmi poruke u redu čekanja", + "session.followupDock.expand": "Proširi poruke u čekanju", + "session.revertDock.summary.one": "{{count}} vratio poruku", + "session.revertDock.summary.other": "{{count}} je vratio poruke", + "session.revertDock.summary.few": "{{count}} vraćene poruke", + "session.revertDock.collapse": "Sažmi vraćene poruke", + "session.revertDock.expand": "Proširi vraćene poruke", + "session.revertDock.restore": "Vrati poruku", + "session.new.title": "Gradite bilo što", + "session.new.project.new": "Novi projekt", + "session.new.project.search": "Pretražite projekte", + "session.new.project.add": "Dodaj projekt", + "session.new.worktree.main": "Glavna grana", + "session.new.worktree.mainWithBranch": "Glavna grana ({{branch}})", + "session.new.worktree.create": "Stvorite novo radno stablo", + "session.new.workspace.runIn": "Pokrenite sesiju", + "session.new.workspace.triggerLocal": "Lokalni", + "session.new.workspace.local": "Lokalno spremište", + "session.new.workspace.existing": "Radni prostor…", + "session.new.git.none": "Ne Git", + "session.new.lastModified": "Zadnja izmjena", + "session.header.search.placeholder": "Traži {{project}}", + "session.header.searchFiles": "Pretraživanje datoteka", + "session.header.openIn": "Otvori u", + "session.header.open.action": "Otvorite {{app}}", + "session.header.open.ariaLabel": "Otvori u {{app}}", + "session.header.open.menu": "Otvorene opcije", + "session.header.open.copyPath": "Kopiraj put", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "Upravitelj datoteka", + "session.header.reveal.finder": "Otkrij u Finder", + "session.header.reveal.fileExplorer": "Otkrij u File Explorer", + "session.header.reveal.containingFolder": "Otvori mapu koja sadrži", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Stanje", + "status.popover.ariaLabel": "Konfiguracije poslužitelja", + "status.popover.tab.servers": "poslužitelji", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Dodaci", + "status.popover.action.manageServers": "Upravljanje poslužiteljima", + "session.share.popover.title": "Objavi na webu", + "session.share.popover.description.shared": "Ova je sesija javna na webu. Dostupan je svima s vezom.", + "session.share.popover.description.unshared": "Podijelite sesiju javno na webu. Bit će dostupna svima s vezom.", + "session.share.action.share": "Udio", + "session.share.action.publish": "Objaviti", + "session.share.action.publishing": "Objavljivanje...", + "session.share.action.unpublish": "Poništi objavu", + "session.share.action.unpublishing": "Poništavanje objave...", + "session.share.action.view": "Prikaz", + "session.share.copy.copied": "Kopirano", + "session.share.copy.copyLink": "Kopiraj vezu", + "lsp.tooltip.none": "Nema LSP poslužitelja", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Učitavanje upita...", + "terminal.loading": "Učitavanje terminala...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Zatvori terminal", + "terminal.connectionLost.title": "Veza izgubljena", + "terminal.connectionLost.abnormalClose": "WebSocket nenormalno zatvoren: {{code}}", + "terminal.connectionLost.description": + "Veza terminala je prekinuta. To se može dogoditi kada se poslužitelj ponovno pokrene.", + "terminal.connectTicket.csrfError": + "PTY karta za spajanje odbijena provjerama porijekla ili CSRF. Provjerite konfiguraciju poslužitelja CORS.", + "terminal.connectTicket.statusError": "PTY povezivanje karte nije uspjelo sa {{status}}", + "titlebar.update": "Ažurirati", + "titlebar.updateVersion": "Ažurirajte {{version}}", + "common.closeTab": "Zatvori karticu", + "common.dismiss": "Odbaciti", + "common.moreCountSuffix": "(još još {{count}})", + "common.requestFailed": "Zahtjev nije uspio", + "common.moreOptions": "Više opcija", + "common.learnMore": "Saznajte više", + "common.rename": "Preimenovati", + "common.export": "Izvoz", + "common.reset": "Resetiraj", + "common.archive": "Arhiva", + "common.delete": "Izbriši", + "common.close": "Zatvoriti", + "common.edit": "Uredi", + "common.loadMore": "Učitaj više", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Izbriši", + "common.key.home": "Početna", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Umetnuti", + "common.unknown": "nepoznato", + "common.time.justNow": "Upravo sada", + "common.time.minutesAgo.short": "Prije {{count}}m", + "common.time.hoursAgo.short": "prije {{count}}h", + "common.time.daysAgo.short": "Prije {{count}}d", + "sidebar.menu.toggle": "Prebaci izbornik", + "sidebar.nav.projectsAndSessions": "Projekti i sesije", + "sidebar.settings": "Postavke", + "sidebar.help": "Pomoć", + "sidebar.workspaces.enable": "Omogući radne prostore", + "sidebar.workspaces.disable": "Onemogući radne prostore", + "sidebar.gettingStarted.title": "Početak rada", + "sidebar.gettingStarted.line1": "OpenCode uključuje besplatne modele tako da možete odmah početi.", + "sidebar.gettingStarted.line2": "Povežite bilo kojeg pružatelja za korištenje modela, uklj. Claude, GPT, Gemini itd.", + "sidebar.project.recentSessions": "Nedavne sesije", + "sidebar.project.viewAllSessions": "Pogledajte sve sesije", + "sidebar.project.clearNotifications": "Očistite obavijesti", + "sidebar.empty.title": "Nema otvorenih projekata", + "sidebar.empty.description": "Otvorite projekt da biste započeli", + "debugBar.ariaLabel": "Dijagnostika izvedbe razvoja", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Zadnji dovršeni prijelaz rute koji dodiruje stranicu sesije, mjereno od početka usmjerivača do prve boje nakon što se slegne.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Broj sličica u sekundi tijekom zadnjih 5 sekundi.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Najgore vrijeme kadra u zadnjih 5 sekundi.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Sličice dulje od 32 ms u zadnjih 5 sekundi.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blokirano vrijeme i broj dugih zadataka u zadnjih 5 sekundi. Maksimalni zadatak: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Najgore uočeno kašnjenje unosa u zadnjih 5 sekundi.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Približno trajanje interakcije u zadnjih 5 sekundi. Ovo je slično INP, a ne službeni Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativna promjena izgleda za trenutni vijek trajanja aplikacije.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Iskorišteni JS hrpa u odnosu na ograničenje hrpe. Samo Chromium.", + "debugBar.mem.tip": "Iskorišteni JS hrpa u odnosu na ograničenje hrpe. {{used}} od {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Prisilno fokusiranje stilova na sve interaktivne elemente", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Nametnite cijeli smjer izgleda aplikacije bez promjene odabranog jezika", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Radna površina", + "settings.section.desktop": "Radna površina", + "settings.section.server": "Poslužitelj", + "settings.tab.general": "Općenito", + "settings.tab.shortcuts": "Prečaci", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integracija", + "settings.desktop.wsl.description": "Pokrenite OpenCode poslužitelj unutar WSL na Windows.", + "settings.general.section.appearance": "Izgled", + "settings.general.section.advanced": "Napredno", + "settings.general.section.notifications": "Obavijesti sustava", + "settings.general.section.updates": "Ažuriranja", + "settings.general.section.sounds": "Zvučni efekti", + "settings.general.section.feed": "hraniti se", + "settings.general.section.display": "Prikaz", + "settings.general.row.language.title": "Jezik", + "settings.general.row.language.description": "Promijenite jezik prikaza za OpenCode", + "settings.general.row.shell.title": "Terminal školjka", + "settings.general.row.shell.description": "Ljuska koju koriste alati terminala i agenta", + "settings.general.row.shell.autoDefault": "Automatski (zadano)", + "settings.general.row.shell.terminalOnly": "samo terminal", + "settings.general.row.appearance.title": "Izgled", + "settings.general.row.appearance.description": "Prilagodite kako OpenCode izgleda na vašem uređaju", + "settings.general.row.colorScheme.title": "Shema boja", + "settings.general.row.colorScheme.description": + "Odaberite hoće li OpenCode slijediti sistemsku, svijetlu ili tamnu temu", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Prilagodite način na koji je OpenCode tematiziran.", + "settings.general.row.font.title": "Font koda", + "settings.general.row.font.description": "Prilagodite font koji se koristi u blokovima koda", + "settings.general.row.terminalFont.title": "Font Terminal", + "settings.general.row.terminalFont.description": "Prilagodite font koji se koristi u terminalu", + "settings.general.row.uiFont.title": "UI font", + "settings.general.row.uiFont.description": "Prilagodite font koji se koristi u cijelom sučelju", + "settings.general.row.followup.title": "Praćenje ponašanja", + "settings.general.row.followup.description": + "Odaberite hoće li se naknadni upiti usmjeravati odmah ili čekati u redu", + "settings.general.row.followup.option.queue": "Red", + "settings.general.row.followup.option.steer": "Upravljati", + "settings.general.row.showFileTree.title": "Stablo datoteka", + "settings.general.row.showFileTree.description": "Prikaži ploču stabla datoteka u sesijama", + "settings.general.row.showNavigation.title": "Navigacijske kontrole", + "settings.general.row.showNavigation.description": + "Prikaži gumbe za povratak i naprijed u naslovnoj traci radne površine", + "settings.general.row.showSearch.title": "Paleta naredbi", + "settings.general.row.showSearch.description": "Prikaži gumb palete za pretraživanje i naredbe u naslovnoj traci", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Prikaži gumb terminala u naslovnoj traci radne površine", + "settings.general.row.showStatus.title": "Status poslužitelja", + "settings.general.row.showStatus.description": "Prikaži gumb statusa poslužitelja u naslovnoj traci", + "settings.general.row.mobileTitlebarBottom.title": "Donja navigacija", + "settings.general.row.mobileTitlebarBottom.description": + "Postavite naslovnu traku i kartice sesije na dno zaslona na mobilnom telefonu", + "settings.general.row.showCustomAgents.title": "Prikaži agenta", + "settings.general.row.showCustomAgents.description": + "Prebacivanje između agenata u skladatelju. Kada je skriven, zadana je Build agent.", + "settings.general.row.reasoningSummaries.title": "Prikaži sažetke obrazloženja", + "settings.general.row.reasoningSummaries.description": "Prikažite sažetke obrazloženja modela na vremenskoj traci", + "settings.general.row.shellToolPartsExpanded.title": "Proširite dijelove alata školjke", + "settings.general.row.shellToolPartsExpanded.description": + "Prikažite dijelove alata ljuske proširene prema zadanim postavkama na vremenskoj traci", + "settings.general.row.editToolPartsExpanded.title": "Proširi uređivanje dijelova alata", + "settings.general.row.editToolPartsExpanded.description": + "Prikažite dijelove alata za uređivanje, pisanje i zakrpe prema zadanim postavkama na vremenskoj traci", + "settings.general.row.newInterface.title": "Novi izgled", + "settings.general.row.newInterface.badge": "Novi", + "settings.general.row.newInterface.description": + "Koristite nove kartice i početni izgled. Prebacivanje između izgleda na ograničeno vrijeme.", + "settings.general.row.newInterfaceNotice.title": "Sada koristite novi izgled", + "settings.general.row.newInterfaceNotice.description": "Prethodni izgled više nije dostupan", + "settings.general.row.newInterfaceNotice.dismiss": "Odbaciti", + "settings.general.row.pinchZoom.title": "Stisnite prste za zumiranje", + "settings.general.row.pinchZoom.description": + "Za zumiranje dopustite prstohvat dodirne površine i Ctrl pokrete pomicanja", + "settings.general.row.wayland.title": "Koristite izvorni Wayland", + "settings.general.row.wayland.description": "Onemogući rezervni X11 na Wayland. Zahtijeva ponovno pokretanje.", + "settings.general.row.wayland.tooltip": + "Na Linux s monitorima s miješanom brzinom osvježavanja izvorni Wayland može biti stabilniji.", + "settings.general.row.releaseNotes.title": "Bilješke o izdanju", + "settings.general.row.releaseNotes.description": "Prikaži skočne prozore Što je novo nakon ažuriranja", + "settings.updates.row.startup.title": "Provjerite ima li ažuriranja pri pokretanju", + "settings.updates.row.startup.description": "Automatski provjeri ima li ažuriranja kada se OpenCode pokrene", + "settings.updates.row.check.title": "Provjerite ima li ažuriranja", + "settings.updates.row.check.description": "Ručno provjerite postoje li ažuriranja i instalirajte ih ako su dostupna", + "settings.updates.action.checkNow": "Provjerite sada", + "settings.updates.action.checking": "Provjera...", + "settings.updates.action.downloading": "Preuzimanje...", + "settings.updates.action.installing": "Instaliranje...", + "settings.updates.toast.latest.title": "U toku ste", + "settings.updates.toast.latest.description": "Imate najnoviju verziju OpenCode.", + "sound.option.none": "Nijedan", + "sound.option.alert01": "Upozorenje 01", + "sound.option.alert02": "Upozorenje 02", + "sound.option.alert03": "Upozorenje 03", + "sound.option.alert04": "Upozorenje 04", + "sound.option.alert05": "Upozorenje 05", + "sound.option.alert06": "Upozorenje 06", + "sound.option.alert07": "Upozorenje 07", + "sound.option.alert08": "Upozorenje 08", + "sound.option.alert09": "Upozorenje 09", + "sound.option.alert10": "Upozorenje 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Ne 01", + "sound.option.nope02": "Ne 02", + "sound.option.nope03": "Ne 03", + "sound.option.nope04": "Ne 04", + "sound.option.nope05": "Ne 05", + "sound.option.nope06": "Ne 06", + "sound.option.nope07": "Ne 07", + "sound.option.nope08": "Ne 08", + "sound.option.nope09": "Ne 09", + "sound.option.nope10": "Ne 10", + "sound.option.nope11": "Ne 11", + "sound.option.nope12": "Ne 12", + "sound.option.yup01": "Da 01", + "sound.option.yup02": "Da 02", + "sound.option.yup03": "Da 03", + "sound.option.yup04": "Da 04", + "sound.option.yup05": "Da 05", + "sound.option.yup06": "Da 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Prikaži obavijest sustava kada je agent dovršen ili treba pozornost", + "settings.general.notifications.permissions.title": "Dozvole", + "settings.general.notifications.permissions.description": "Prikaži obavijest sustava kada je potrebna dozvola", + "settings.general.notifications.errors.title": "Greške", + "settings.general.notifications.errors.description": "Prikaži obavijest sustava kada se pojavi greška", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Pusti zvuk kada je agent gotov ili mu treba pozornost", + "settings.general.sounds.permissions.title": "Dozvole", + "settings.general.sounds.permissions.description": "Pusti zvuk kada je potrebno dopuštenje", + "settings.general.sounds.errors.title": "Greške", + "settings.general.sounds.errors.description": "Pusti zvuk kada se pojavi pogreška", + "settings.shortcuts.title": "Tipkovnički prečaci", + "settings.shortcuts.reset.button": "Vrati na zadane postavke", + "settings.shortcuts.reset.toast.title": "Ponovno postavljanje prečaca", + "settings.shortcuts.reset.toast.description": "Tipkovnički prečaci vraćeni su na zadane postavke.", + "settings.shortcuts.conflict.title": "Prečac se već koristi", + "settings.shortcuts.conflict.description": "{{keybind}} je već dodijeljen {{titles}}.", + "settings.shortcuts.unassigned": "Nedodijeljeno", + "settings.shortcuts.pressKeys": "Pritisnite tipke", + "settings.shortcuts.search.placeholder": "Prečaci za pretraživanje", + "settings.shortcuts.search.empty": "Nema pronađenih prečaca", + "settings.shortcuts.group.general": "Općenito", + "settings.shortcuts.group.session": "Sesija", + "settings.shortcuts.group.navigation": "Navigacija", + "settings.shortcuts.group.modelAndAgent": "Model i agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Upit", + "settings.providers.title": "Pružatelji usluga", + "settings.providers.description": "Ovdje će se moći konfigurirati postavke pružatelja usluga.", + "settings.providers.section.connected": "Povezani pružatelji usluga", + "settings.providers.connected.empty": "Nema povezanih pružatelja usluga", + "settings.providers.connected.environmentDescription": "Povezano iz varijabli vaše okoline", + "settings.providers.section.popular": "Popularni pružatelji usluga", + "settings.providers.custom.description": "Dodajte pružatelja kompatibilnog sa OpenAI prema bazi URL.", + "settings.providers.tag.environment": "okoliš", + "settings.providers.tag.config": "Konfiguracija", + "settings.providers.tag.custom": "Običaj", + "settings.providers.tag.other": "ostalo", + "settings.models.title": "Modeli", + "settings.models.description": "Ovdje će se moći konfigurirati postavke modela.", + "settings.agents.title": "Agenti", + "settings.agents.description": "Ovdje će se moći konfigurirati postavke agenta.", + "settings.commands.title": "Naredbe", + "settings.commands.description": "Ovdje će se moći konfigurirati postavke naredbi.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Ovdje će se moći konfigurirati postavke MCP.", + "settings.permissions.title": "Dozvole", + "settings.permissions.description": "Kontrolirajte koje alate poslužitelj može koristiti prema zadanim postavkama.", + "settings.permissions.section.tools": "Alati", + "settings.permissions.toast.updateFailed.title": "Ažuriranje dopuštenja nije uspjelo", + "settings.permissions.action.allow": "Dopusti", + "settings.permissions.action.ask": "Pitajte", + "settings.permissions.action.deny": "poreći", + "settings.permissions.tool.read.title": "čitati", + "settings.permissions.tool.read.description": "Čitanje datoteke (odgovara putanji datoteke)", + "settings.permissions.tool.edit.title": "Uredi", + "settings.permissions.tool.edit.description": "Mijenjajte datoteke, uključujući uređivanja, pisanje i zakrpe", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Usporedite datoteke koristeći glob uzorke", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Pretražujte sadržaj datoteke pomoću regularnih izraza", + "settings.permissions.tool.list.title": "Popis", + "settings.permissions.tool.list.description": "Popis datoteka unutar direktorija", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Pokretanje naredbi ljuske", + "settings.permissions.tool.task.title": "Zadatak", + "settings.permissions.tool.task.description": "Pokretanje pod-agenta", + "settings.permissions.tool.skill.title": "Vještina", + "settings.permissions.tool.skill.description": "Učitajte vještinu po imenu", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Izvršite upite jezičnog poslužitelja", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Ažurirajte popis obveza", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Dohvaćanje sadržaja sa URL", + "settings.permissions.tool.websearch.title": "Web pretraživanje", + "settings.permissions.tool.websearch.description": "Pretraži web", + "settings.permissions.tool.external_directory.title": "Vanjski imenik", + "settings.permissions.tool.external_directory.description": "Pristup datotekama izvan direktorija projekta", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Otkrijte ponovljene pozive alata s identičnim unosom", + "session.delete.failed.title": "Brisanje sesije nije uspjelo", + "session.delete.title": "Izbriši sesiju", + "session.delete.confirm": 'Izbrisati sesiju "{{name}}"?', + "session.delete.button": "Izbriši sesiju", + "workspace.new": "Novi radni prostor", + "workspace.type.local": "lokalni", + "workspace.type.sandbox": "pješčanik", + "workspace.create.failed.title": "Izrada radnog prostora nije uspjela", + "workspace.delete.failed.title": "Brisanje radnog prostora nije uspjelo", + "workspace.resetting.title": "Ponovno postavljanje radnog prostora", + "workspace.resetting.description": "Ovo može potrajati minutu.", + "workspace.reset.failed.title": "Poništavanje radnog prostora nije uspjelo", + "workspace.reset.success.title": "Resetiranje radnog prostora", + "workspace.reset.success.description": "Radni prostor sada odgovara zadanoj grani.", + "workspace.error.stillPreparing": "Radni prostor je još u pripremi", + "workspace.status.checking": "Provjera nespojenih promjena...", + "workspace.status.error": "Nije moguće potvrditi git status.", + "workspace.status.clean": "Nisu otkrivene nespojene promjene.", + "workspace.status.dirty": "Nespojene promjene otkrivene u ovom radnom prostoru.", + "workspace.delete.title": "Delete radni prostor", + "workspace.delete.confirm": 'Delete radni prostor "{{name}}"?', + "workspace.delete.button": "Delete radni prostor", + "workspace.reset.title": "Resetiraj radni prostor", + "workspace.reset.confirm": 'Resetirati radni prostor "{{name}}"?', + "workspace.reset.button": "Resetiraj radni prostor", + "workspace.reset.archived.none": "Nijedna aktivna sesija neće biti arhivirana.", + "workspace.reset.archived.one": "1 sesija će biti arhivirana.", + "workspace.reset.archived.many": "{{count}} sesije će biti arhivirane.", + "workspace.reset.note": "Ovo će resetirati radni prostor kako bi odgovarao zadanoj grani.", + "dialog.usageExceeded.dontShowAgain": "Ne prikazuj ponovno", +} diff --git a/packages/app/src/i18n/hu.ts b/packages/app/src/i18n/hu.ts new file mode 100644 index 0000000000000000000000000000000000000000..664abf8ee4c01ccffaf324fb0432274e84563269 --- /dev/null +++ b/packages/app/src/i18n/hu.ts @@ -0,0 +1,1170 @@ +import { DESKTOP_NATIVE_KEYS } from "./desktop-native" + +const desktop = [ + "OpenCode", + "Fájl", + "Szerkesztés", + "Nézet", + "Ugrás", + "Ablak", + "Súgó", + "Frissítések keresése...", + "Beállítások", + "Webview újratöltése", + "Újraindítás", + "Naplók exportálása...", + "Új munkamenet", + "Projekt megnyitása...", + "Új ablak", + "Ablak bezárása", + "Visszavonás", + "Újra", + "Kivágás", + "Másolás", + "Beillesztés", + "Törlés", + "Összes kijelölése", + "Oldalsáv ki- és bekapcsolása", + "Terminál ki- és bekapcsolása", + "Fájlfastruktúra ki- és bekapcsolása", + "Újratöltés", + "Fejlesztői eszközök ki- és bekapcsolása", + "Valódi méret", + "Nagyítás", + "Kicsinyítés", + "Teljes képernyő ki- és bekapcsolása", + "Vissza", + "Előre", + "Előző munkamenet", + "Következő munkamenet", + "Előző projekt", + "Következő projekt", + "Kis méret", + "Teljes méret", + "OpenCode dokumentáció", + "Támogatási fórum", + "Visszajelzés küldése", + "Hiba jelentése", + "OpenCode menü", + "A frissítések keresése sikertelen.", + "Frissítési hiba", + "A legfrissebb verziót használja.", + "Nincs frissítés", + "A(z) {{version}} frissítés letöltődött. Újraindítja most?", + "A frissítés készen áll", + "Újraindítás", + "Később", + "Újraindítás", + "Naplók exportálása", + "Várakozás folytatása", + "Kilépés", + "Az OpenCode betöltése sikertelen", + "Az OpenCode ablaka váratlanul leállt", + "Az OpenCode nem válaszol", + "Újraindíthatja az alkalmazást, megnyithatja a naplókat, vagy folytathatja a várakozást.", + "Ablak: {{window}}\nURL: {{url}}\nHiba: {{code}} {{description}}", + "Ablak: {{window}}\nOk: {{reason}}\nKód: {{code}}", + "", + "Mappa kiválasztása", + "Fájl kiválasztása", + "Fájl mentése", + "Fájlok", + "Helyi kiszolgáló", + "A WSL csak Windows rendszeren érhető el", + "A WSL nem érhető el", + "A telepített WSL-disztribúciók felsorolása sikertelen", + "Az online WSL-disztribúciók felsorolása sikertelen", + "Nem hajthatók végre parancsok a disztribúcióban", + "A WSL telepítése sikertelen", + "A disztribúció telepítése sikertelen: {{distro}}", + "Az OpenCode telepítése sikertelen", + "A(z) {{distro}} már hozzá van adva", + "Az opencode nincs telepítve ebben a disztribúcióban", + "Az opencode telepítve van, de nem futtatható", + "Az OpenCode nincs telepítve ebben: {{distro}}", + "Az OpenCode frissítése befejeződött, de a(z) {{distro}} továbbra is ezt jelenti: {{installed}}; a várt érték: {{expected}}", + "nincs verzió", + "A WSL-kiszolgáló indítás után kilépett (code={{code}} signal={{signal}})", + "A WSL-kiszolgáló azelőtt kilépett, hogy üzemkésszé vált volna (code={{code}} signal={{signal}}){{output}}", + "A(z) {{distro}} segédfolyamatának állapotellenőrzése {{timeout}} ms után időtúllépés miatt megszakadt", + "A(z) {{command}} {{args}} parancs {{timeout}} ms után időtúllépés miatt megszakadt", + "A port lekérése sikertelen", + "A fájl nem lett kiválasztva a választóban", + "A kiválasztott mellékletek meghaladják a(z) {{limit}} MB-os korlátot", +] + +export const dict = { + ...Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key, index) => [key, desktop[index]])), + "command.category.suggested": "Javasolt", + "command.category.view": "Nézet", + "command.category.project": "Projekt", + "command.category.provider": "Szolgáltató", + "command.category.server": "Kiszolgáló", + "command.category.session": "Munkamenet", + "command.category.theme": "Téma", + "command.category.language": "Nyelv", + "command.category.file": "Fájl", + "command.category.context": "Kontextus", + "command.category.terminal": "Terminál", + "command.category.model": "Modell", + "command.category.mcp": "MCP", + "command.category.agent": "Ügynök", + "command.category.permissions": "Engedélyek", + "command.category.workspace": "Munkaterület", + "command.category.settings": "Beállítások", + "command.logs.export": "Naplók exportálása", + "theme.scheme.system": "Rendszer", + "theme.scheme.light": "Világos", + "theme.scheme.dark": "Sötét", + "command.sidebar.toggle": "Oldalsáv váltása", + "command.project.open": "Projekt megnyitása", + "command.project.previous": "Előző projekt", + "command.project.next": "Következő projekt", + "command.project.index": "Váltson a {{index}} projektre", + "command.provider.connect": "Szolgáltató csatlakoztatása", + "command.server.switch": "Kiszolgáló váltása", + "command.settings.open": "Beállítások megnyitása", + "command.session.previous": "Előző munkamenet", + "command.session.next": "Következő munkamenet", + "command.session.previous.unseen": "Előző olvasatlan munkamenet", + "command.session.next.unseen": "Következő olvasatlan munkamenet", + "command.session.archive": "Munkamenet archiválása", + "command.palette": "Parancs paletta", + "command.theme.cycle": "Téma váltása", + "command.theme.set": "Használd a témát: {{theme}}", + "command.theme.scheme.cycle": "Színséma váltása", + "command.theme.scheme.set": "Használja a színsémát: {{scheme}}", + "command.language.cycle": "Nyelv váltása", + "command.language.set": "Nyelv használata: {{language}}", + "command.session.new": "Új munkamenet", + "command.file.open": "Nyissa meg a fájlt", + "command.tab.close": "Lap bezárása", + "command.tab.reopenClosed": "Nyissa meg újra a bezárt lapot", + "command.context.addSelection": "Kijelölés hozzáadása a kontextushoz", + "command.context.addSelection.description": "Kijelölt sorok hozzáadása az aktuális fájlból", + "command.input.focus": "Beviteli mező fókuszálása", + "command.terminal.toggle": "Kapcsolja be a terminált", + "command.fileTree.toggle": "Fájlfa váltása", + "command.review.toggle": "Áttekintés átváltása", + "command.terminal.new": "Új terminál", + "command.terminal.new.description": "Hozzon létre egy új terminállapot", + "command.steps.toggle": "Lépések váltása", + "command.steps.toggle.description": "Az aktuális üzenet lépéseinek megjelenítése vagy elrejtése", + "command.message.previous": "Előző üzenet", + "command.message.previous.description": "Ugrás az előző felhasználói üzenetre", + "command.message.next": "Következő üzenet", + "command.message.next.description": "Ugrás a következő felhasználói üzenetre", + "command.model.choose": "Válasszon modellt", + "command.model.choose.description": "Válasszon másik modellt", + "command.mcp.toggle": "MCPs váltás", + "command.mcp.toggle.description": "MCPs váltás", + "command.agent.cycle": "Ügynök váltása", + "command.agent.cycle.description": "Váltson a következő ügynökre", + "command.agent.cycle.reverse": "Váltás az előző ügynökre", + "command.agent.cycle.reverse.description": "Váltás az előző ügynökre", + "command.model.variant.cycle": "Ciklikus gondolkodási erőfeszítés", + "command.model.variant.cycle.description": "Váltson a következő erőfeszítési szintre", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Utasítás", + "command.permissions.autoaccept.enable": "Engedélyek automatikus elfogadása", + "command.permissions.autoaccept.disable": "Állítsa le az engedélyek automatikus elfogadását", + "command.workspace.toggle": "Munkaterületek váltása", + "command.workspace.toggle.description": "Több munkaterület engedélyezése vagy letiltása az oldalsávon", + "command.session.undo": "Visszavonás", + "command.session.undo.description": "Az utolsó üzenet visszavonása", + "command.session.redo": "Újra", + "command.session.redo.description": "Ismételje meg az utolsó visszavont üzenetet", + "command.session.compact": "Munkamenet tömörítése", + "command.session.compact.description": "A kontextus méretének csökkentése érdekében foglalja össze a munkamenetet", + "command.session.fork": "Elágazás az üzenettől", + "command.session.fork.description": "Hozzon létre új munkamenetet egy korábbi üzenetből", + "command.session.share": "Munkamenet megosztása", + "command.session.share.description": "Ossza meg ezt a munkamenetet, és másolja a URL fájlt a vágólapra", + "command.session.unshare": "Munkamenet megosztásának megszüntetése", + "command.session.unshare.description": "Állítsa le a munkamenet megosztását", + "command.session.export": "Exportálási munkamenet", + "command.session.export.description": "Exportálja a teljes munkamenet-átiratot JSON formátumban", + "palette.search.placeholder": "Fájlok, parancsok és munkamenetek keresése", + "palette.search.placeholder.home": "Parancsok és munkamenetek keresése", + "palette.empty": "Nincs találat", + "palette.group.commands": "Parancsok", + "palette.group.files": "Fájlok", + "dialog.provider.search.placeholder": "Szolgáltatók keresése", + "dialog.provider.empty": "Nem található szolgáltató", + "dialog.provider.group.popular": "Népszerű", + "dialog.provider.group.other": "Más", + "dialog.provider.custom.label": "Egyedi OpenAI-kompatibilis szolgáltató", + "dialog.provider.tag.recommended": "Ajánlott", + "dialog.provider.opencode.note": "Összeválogatott modellek, köztük Claude, GPT, Gemini és még sok más", + "dialog.provider.opencode.tagline": "Megbízható optimalizált modellek", + "dialog.provider.opencodeGo.tagline": "Olcsó előfizetés mindenki számára", + "dialog.provider.anthropic.note": "Közvetlen hozzáférés a Claude modellekhez, beleértve a Pro és Max modelleket", + "dialog.provider.copilot.note": "AI modellek a kódoláshoz a GitHub Copilot segítségével", + "dialog.provider.openai.note": "GPT modellek a gyors, általános AI-feladatokhoz", + "dialog.provider.google.note": "Gemini modellek a gyors, strukturált válaszokhoz", + "dialog.provider.openrouter.note": "Hozzáférés az összes támogatott modellhez egy szolgáltatótól", + "dialog.provider.vercel.note": "Egységes hozzáférés az AI modellekhez intelligens útválasztással", + "dialog.model.select.title": "Válasszon modellt", + "dialog.model.search.placeholder": "Modellek keresése", + "dialog.model.empty": "Nincsenek modelleredmények", + "dialog.model.manage": "Modellek kezelése", + "dialog.model.manage.description": "Testreszabhatja, hogy mely modellek jelenjenek meg a modellválasztóban.", + "dialog.model.manage.provider.toggle": "Kapcsolja be az összes {{provider}} modellt", + "dialog.model.unpaid.freeModels.title": "A OpenCode ingyenes modelljei", + "dialog.model.unpaid.addMore.title": "Adjon hozzá további modelleket a népszerű szolgáltatóktól", + "dialog.model.unpaid.viewMoreProviders": "További 70+ szolgáltató megtekintése", + "dialog.provider.viewAll": "További szolgáltatók megjelenítése", + "provider.connect.title": "Csatlakoztassa a {{provider}}-t", + "provider.connect.title.anthropicProMax": "Jelentkezzen be a Claude Pro/Max", + "provider.connect.selectMethod": "Válassza ki a {{provider}} bejelentkezési módját.", + "provider.connect.method.apiKey": "API kulcs", + "provider.connect.method.browser": "Böngésző", + "provider.connect.method.headless": "Fejetlen", + "provider.connect.status.inProgress": "Az engedélyezés folyamatban...", + "provider.connect.status.waiting": "Várakozás az engedélyezésre...", + "provider.connect.status.failed": "Az engedélyezés sikertelen: {{error}}", + "provider.connect.apiKey.description": + "Enter az Ön {{provider}} API kulcsa fiókja összekapcsolásához és a {{provider}} modellek használatához a OpenCode-ben.", + "provider.connect.apiKey.label": "{{provider}} API kulcs", + "provider.connect.apiKey.placeholder": "API kulcs", + "provider.connect.apiKey.required": "API kulcs szükséges", + "provider.connect.opencodeZen.line1": + "A OpenCode Zen hozzáférést biztosít a kódoló ügynökök számára megbízható, optimalizált modellek válogatott készletéhez.", + "provider.connect.opencodeZen.line2": + "Egyetlen API kulccsal hozzáférhet olyan modellekhez, mint a Claude, GPT, Gemini, GLM és még sok más.", + "provider.connect.opencodeZen.visit.prefix": "Látogatás", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "hogy átvegye a API kulcsát.", + "provider.connect.oauth.code.visit.prefix": "Látogatás", + "provider.connect.oauth.code.visit.link": "ezt a linket", + "provider.connect.oauth.code.visit.suffix": + "az engedélyezési kód összegyűjtéséhez fiókja összekapcsolásához és a {{provider}} modellek használatához a OpenCode-ben.", + "provider.connect.oauth.code.label": "{{method}} engedélyezési kód", + "provider.connect.oauth.code.placeholder": "Engedélyezési kód", + "provider.connect.oauth.code.required": "Engedélyezési kód szükséges", + "provider.connect.oauth.code.invalid": "Érvénytelen engedélyezési kód", + "provider.connect.oauth.auto.visit.prefix": "Látogatás", + "provider.connect.oauth.auto.visit.link": "ezt a linket", + "provider.connect.oauth.auto.visit.suffix": + "és írja be az alábbi kódot fiókja összekapcsolásához, és használja a {{provider}} modelleket a OpenCode-ben.", + "provider.connect.oauth.auto.confirmationCode": "Megerősítő kód", + "provider.connect.toast.connected.title": "{{provider}} csatlakoztatva", + "provider.connect.toast.connected.description": "A {{provider}} modellek már használhatók.", + "provider.custom.title": "Egyedi szolgáltató", + "provider.custom.unavailable": "Egyéni szolgáltatók nem érhetők el ezen a szerveren", + "provider.custom.description.prefix": "Konfiguráljon egy OpenAI-kompatibilis szolgáltatót. Lásd a", + "provider.custom.description.link": "szolgáltató konfigurációs dokumentumai", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Szolgáltató azonosítója", + "provider.custom.field.providerID.placeholder": "a szolgáltatóm", + "provider.custom.field.providerID.description": "Kisbetűk, számok, kötőjelek vagy aláhúzásjelek", + "provider.custom.field.name.label": "Megjelenítési név", + "provider.custom.field.name.placeholder": "AI szolgáltatóm", + "provider.custom.field.baseURL.label": "Alap URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API kulcs", + "provider.custom.field.apiKey.placeholder": "API kulcs", + "provider.custom.field.apiKey.description": + "Választható. Hagyja üresen, ha a hitelesítést fejléceken keresztül kezeli.", + "provider.custom.models.label": "Modellek", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "modell-azonosító", + "provider.custom.models.name.label": "Név", + "provider.custom.models.name.placeholder": "Megjelenítési név", + "provider.custom.models.remove": "Modell eltávolítása", + "provider.custom.models.add": "Modell hozzáadása", + "provider.custom.headers.label": "Fejlécek (opcionális)", + "provider.custom.headers.key.label": "Fejléc", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Érték", + "provider.custom.headers.value.placeholder": "érték", + "provider.custom.headers.remove": "Fejléc eltávolítása", + "provider.custom.headers.add": "Fejléc hozzáadása", + "provider.custom.error.providerID.required": "Szolgáltatói azonosító szükséges", + "provider.custom.error.providerID.format": "Használjon kisbetűket, számokat, kötőjeleket vagy aláhúzásjeleket", + "provider.custom.error.providerID.exists": "Ez a szolgáltatói azonosító már létezik", + "provider.custom.error.name.required": "A megjelenített név kötelező", + "provider.custom.error.baseURL.required": "Alap URL szükséges", + "provider.custom.error.baseURL.format": "http:// vagy https:// karakterekkel kell kezdődnie", + "provider.custom.error.required": "Kívánt", + "provider.custom.error.duplicate": "Másolat", + "provider.disconnect.toast.disconnected.title": "{{provider}} leválasztva", + "provider.disconnect.toast.disconnected.description": "A {{provider}} modellek már nem kaphatók.", + "model.tag.free": "Ingyenes", + "model.tag.latest": "Legújabb", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "szöveg", + "model.input.image": "kép", + "model.input.audio": "hang-", + "model.input.video": "videó", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Lehetővé teszi: {{inputs}}", + "model.tooltip.context.label": "Kontextus", + "model.tooltip.inputs": "Bemenetek", + "model.tooltip.model": "Modell", + "model.tooltip.provider": "Szolgáltató", + "model.tooltip.reasoning": "Érvelés", + "model.tooltip.reasoning.allowed": "Érvelést tesz lehetővé", + "model.tooltip.reasoning.none": "Nincs érvelés", + "model.tooltip.context": "Kontextuskorlát {{limit}}", + "common.search.placeholder": "Keresés", + "common.goBack": "Navigáljon vissza", + "common.goForward": "Navigáljon előre", + "common.loading": "Betöltés", + "common.loading.ellipsis": "...", + "common.cancel": "Mégsem", + "common.clear": "Törlés", + "common.open": "Nyitott", + "common.connect": "Csatlakozás", + "common.disconnect": "Leválasztás", + "common.continue": "Folytatás", + "common.submit": "Beküld", + "common.save": "Megtakarítás", + "common.saving": "Megtakarítás...", + "common.default": "Alapértelmezett", + "common.attachment": "mellékletet", + "prompt.placeholder.shell": "Enter shell parancs... {{example}}", + "prompt.placeholder.normal": 'Kérdezz bármit... "{{example}}"', + "prompt.placeholder.simple": "Kérdezz bármit...", + "prompt.placeholder.summarizeComments": "A megjegyzések összegzése…", + "prompt.placeholder.summarizeComment": "Komment összefoglalása…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Utasítás", + "prompt.mode.shell.exit": "esc a kilépéshez", + "session.child.promptDisabled": "Az alügynöki munkamenetek nem kérhetők fel.", + "session.child.backToParent": "Vissza a fő munkamenethez.", + "prompt.example.1": "Javítson ki egy TODO-t a kódbázisban", + "prompt.example.2": "Mi ennek a projektnek a technológiai halmaza?", + "prompt.example.3": "Javítsa ki a hibás teszteket", + "prompt.example.4": "Ismertesse a hitelesítés működését", + "prompt.example.5": "Keresse meg és javítsa ki a biztonsági réseket", + "prompt.example.6": "Adjon hozzá egységteszteket a felhasználói szolgáltatáshoz", + "prompt.example.7": "Refaktorálja ezt a függvényt, hogy jobban olvasható legyen", + "prompt.example.8": "Mit jelent ez a hiba?", + "prompt.example.9": "Segítsen a probléma hibakeresésében", + "prompt.example.10": "API dokumentáció létrehozása", + "prompt.example.11": "Az adatbázis-lekérdezések optimalizálása", + "prompt.example.12": "Bemeneti ellenőrzés hozzáadása", + "prompt.example.13": "Hozzon létre egy új összetevőt a...", + "prompt.example.14": "Hogyan telepíthetem ezt a projektet?", + "prompt.example.15": "Tekintse át a kódomat a bevált módszerekért", + "prompt.example.16": "Adja hozzá a hibakezelést ehhez a funkcióhoz", + "prompt.example.17": "Magyarázza el ezt a reguláris kifejezést", + "prompt.example.18": "Konvertálja ezt TypeScript-re", + "prompt.example.19": "Naplózás hozzáadása a kódbázishoz", + "prompt.example.20": "Milyen függőségek elavultak?", + "prompt.example.21": "Segíts nekem migrációs szkriptet írni", + "prompt.example.22": "Gyorsítótárazás megvalósítása ehhez a végponthoz", + "prompt.example.23": "Lapozás hozzáadása ehhez a listához", + "prompt.example.24": "Hozzon létre egy CLI parancsot...", + "prompt.example.25": "Hogyan működnek itt a környezeti változók?", + "prompt.popover.emptyResults": "Nincsenek megfelelő eredmények", + "prompt.popover.emptyCommands": "Nincsenek megfelelő parancsok", + "prompt.dropzone.label": "Húzza ide a képeket, PDF-eket vagy szöveges fájlokat", + "prompt.dropzone.file.label": "Vidd a @említési fájlba", + "prompt.slash.badge.custom": "szokás", + "prompt.slash.badge.skill": "készség", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktív", + "prompt.context.includeActiveFile": "Aktív fájl hozzáadása", + "prompt.context.removeActiveFile": "Távolítsa el az aktív fájlt a kontextusból", + "prompt.context.removeFile": "Távolítsa el a fájlt a kontextusból", + "prompt.action.attachFile": "Fájlok hozzáadása", + "prompt.menu.addImagesAndFiles": "Fájlok hozzáadása és egyebek", + "prompt.menu.imagesAndFiles": "Képek és fájlok", + "prompt.menu.commands": "Parancsok", + "prompt.menu.context": "Kontextus", + "prompt.menu.shellCommand": "Shell parancs", + "prompt.attachment.remove": "Távolítsa el a mellékletet", + "prompt.action.send": "Elküld", + "prompt.action.stop": "Leállítás", + "prompt.toast.pasteUnsupported.title": "Nem támogatott melléklet", + "prompt.toast.pasteUnsupported.description": "Ide csak képeket, PDF-eket vagy szöveges fájlokat lehet csatolni.", + "prompt.toast.attachmentDuplicate.title": "Ezt a fájlt már feltöltötték", + "prompt.toast.modelAgentRequired.title": "Válasszon egy ügynököt és modellt", + "prompt.toast.modelAgentRequired.description": "A felszólítás elküldése előtt válasszon ügynököt és modellt.", + "prompt.toast.worktreeCreateFailed.title": "Nem sikerült létrehozni a munkafát", + "prompt.toast.sessionCreateFailed.title": "Nem sikerült létrehozni a munkamenetet", + "prompt.toast.shellSendFailed.title": "Nem sikerült elküldeni a shell parancsot", + "prompt.toast.commandSendFailed.title": "Nem sikerült elküldeni a parancsot", + "prompt.toast.promptSendFailed.title": "Nem sikerült elküldeni a felszólítást", + "prompt.toast.promptSendFailed.description": "Nem sikerült lekérni a munkamenetet", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}}/{{total}} engedélyezve", + "dialog.mcp.empty": "Nincs konfigurálva MCP", + "dialog.lsp.empty": "A LSP automatikusan felismerhető a fájltípusokból", + "dialog.plugins.empty": "Az opencode.json fájlban konfigurált beépülő modulok", + "mcp.status.connected": "csatlakoztatva", + "mcp.status.failed": "sikertelen", + "mcp.status.needs_auth": "hitelesítést igényel", + "mcp.status.disabled": "letiltva", + "mcp.auth.clickToAuthenticate": "Kattintson a hitelesítéshez", + "dialog.fork.empty": "Nincsenek üzenetek, ahonnan elágazhatna", + "dialog.directory.search.placeholder": "Mappák keresése", + "dialog.directory.empty": "Nem találhatók mappák", + "dialog.directory.action.selectFile": "Válassza ki a fájlt", + "dialog.directory.action.selectFolder": "Válasszon mappát", + "dialog.directory.root": "Gyökér", + "dialog.directory.parent": "Szülő", + "dialog.directory.readError": "Ez a mappa nem olvasható", + "app.server.unreachable": "A {{server}} nem érhető el", + "app.server.retrying": "Automatikus újrapróbálkozás...", + "app.server.otherServers": "Egyéb szerverek", + "dialog.server.title": "Szerverek", + "dialog.server.description": "Állítsa be, hogy ez az alkalmazás melyik OpenCode szerverhez csatlakozzon.", + "dialog.server.search.placeholder": "Szerverek keresése", + "dialog.server.empty": "Még nincsenek szerverek", + "dialog.server.add.title": "Szerver hozzáadása", + "dialog.server.add.url": "Szerver címe", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Nem sikerült csatlakozni a szerverhez", + "dialog.server.add.checking": "Ellenőrzés...", + "dialog.server.add.button": "Szerver hozzáadása", + "dialog.server.add.name": "Szerver neve (nem kötelező)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Felhasználónév (nem kötelező)", + "dialog.server.add.usernamePlaceholder": "felhasználónév", + "dialog.server.add.password": "Jelszó (nem kötelező)", + "dialog.server.add.passwordPlaceholder": "jelszó", + "dialog.server.edit.title": "Szerver szerkesztése", + "dialog.server.default.title": "Alapértelmezett szerver", + "dialog.server.default.description": + "Helyi szerver indítása helyett csatlakozzon ehhez a szerverhez az alkalmazás indításakor. Újraindítást igényel.", + "dialog.server.default.none": "Nincs kiválasztva szerver", + "dialog.server.default.set": "Az aktuális szerver beállítása alapértelmezettként", + "dialog.server.default.clear": "Törlés", + "dialog.server.action.remove": "Szerver eltávolítása", + "dialog.server.menu.edit": "Szerkesztés", + "dialog.server.menu.default": "Beállítás alapértelmezettként", + "dialog.server.menu.defaultRemove": "Alapértelmezés eltávolítása", + "dialog.server.menu.delete": "Törlés", + "dialog.server.current": "Jelenlegi szerver", + "dialog.server.status.default": "Alapértelmezett", + "wsl.server.add": "WSL szerver hozzáadása", + "wsl.server.addShort": "Adja hozzá a WSL-t", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL szerver", + "wsl.server.retryStart": "Próbálja újra az indítást", + "wsl.server.updating": "Frissítés...", + "wsl.onboarding.step.distro": "Válasszon disztrót", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL ellenőrzése...", + "wsl.onboarding.restartRequired": "A Windows készüléket újra kell indítani a WSL telepítésének befejezéséhez.", + "wsl.onboarding.ready": "A WSL készen áll.", + "wsl.onboarding.required": "A folytatáshoz WSL szükséges.", + "wsl.onboarding.checkingDistros": "A disztribúciók ellenőrzése...", + "wsl.onboarding.installingDistro": "{{distro}} telepítése...", + "wsl.onboarding.checkingDistro": "{{distro}} ellenőrzése...", + "wsl.onboarding.listingDistros": "A disztribúciók listázása...", + "wsl.onboarding.distroReady": "A {{distro}} készen áll.", + "wsl.onboarding.distroNotInstalled": "A {{distro}} még nincs telepítve.", + "wsl.onboarding.openDistroOnce": "Nyissa meg egyszer a {{distro}}-t a beállítás befejezéséhez.", + "wsl.onboarding.finishingDistro": "A {{distro}} befejező beállítása.", + "wsl.onboarding.pickDistro": "Válasszon disztrót vagy telepítsen egyet alább.", + "wsl.onboarding.checkingOpencode": "OpenCode ellenőrzése...", + "wsl.onboarding.checkingOpencodeIn": "OpenCode ellenőrzése a {{distro}}-ben...", + "wsl.onboarding.updatingOpencode": "OpenCode frissítése...", + "wsl.onboarding.updatingOpencodeIn": "OpenCode frissítése a {{distro}}-ben...", + "wsl.onboarding.updateOpencodeIn": "Frissítse a OpenCode-t a {{distro}}-ben.", + "wsl.onboarding.updateOpencode": "Frissítse a OpenCode-t", + "wsl.onboarding.opencodeReadyIn": "A OpenCode készen áll a {{distro}}-ben.", + "wsl.onboarding.opencodeReady": "A OpenCode készen áll.", + "wsl.onboarding.installOpencodeIn": "Telepítse a OpenCode-t a {{distro}}-be.", + "wsl.onboarding.installOpencode": "Telepítse a OpenCode-t", + "wsl.onboarding.chooseDistroFirst": "Először válasszon disztrót.", + "wsl.onboarding.loadFailed": "Nem sikerült betölteni a WSL állapotot.", + "wsl.onboarding.loading": "Betöltés...", + "wsl.onboarding.installedDistros": "Telepített disztribúciók", + "wsl.onboarding.checkAgain": "Ellenőrizze újra", + "wsl.onboarding.distroStatus.ready": "Kész", + "wsl.onboarding.distroStatus.checking": "Ellenőrzés...", + "wsl.onboarding.distroStatus.opencodeMissing": "A OpenCode nincs telepítve", + "wsl.onboarding.distroStatus.missingTools": "Hiányzik a bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Nem támogatott · Használja a WSL 2-t", + "wsl.onboarding.needAnotherDistro": "Kell még egy disztró?", + "wsl.onboarding.needAnotherDistroHint": "Telepítsen egy Linux disztribúciót a WSL katalógusból", + "wsl.onboarding.wslNotInstalled.title": "A WSL nincs telepítve", + "wsl.onboarding.wslNotInstalled.description": + "A WSL (Windows alrendszer a Linux számára) szükséges ahhoz, hogy a OpenCode hozzáadhasson egy WSL szervert", + "wsl.onboarding.wslUnavailable.title": "A WSL nem elérhető", + "wsl.onboarding.wslUnavailable.description": "A OpenCode nem tudta ellenőrizni a WSL jelet ezen a gépen.", + "wsl.onboarding.installWsl": "Telepítse a WSL-t", + "wsl.onboarding.windowsRestartRequired": + "Indítsa újra a Windows programot a WSL telepítésének befejezéséhez, majd nyissa meg újra a OpenCode programot.", + "wsl.onboarding.next": "Következő", + "wsl.onboarding.refresh": "Frissítés", + "wsl.onboarding.allDistrosAdded": "Az összes telepített disztribúció már hozzáadva van.", + "wsl.onboarding.noDistros": "Még nem észleltek disztribúciót.", + "wsl.onboarding.install": "Telepítés", + "wsl.onboarding.installing": "Telepítés...", + "wsl.onboarding.installDistro": "Telepítse a disztribúciót", + "wsl.onboarding.searchDistros": "Keresés disztribúciókban", + "wsl.onboarding.wsl2Required": "WSL 2 szükséges.", + "wsl.onboarding.toolsRequired": "Ehhez a disztribúcióhoz bash és curl kell.", + "wsl.onboarding.openTerminal": "Nyissa meg a terminált", + "wsl.onboarding.path": "Útvonal: {{path}}", + "wsl.onboarding.notFound": "nem található", + "wsl.onboarding.version": "Kivitel: {{version}}", + "wsl.onboarding.unknown": "ismeretlen", + "wsl.onboarding.desktopVersion": "asztali {{version}}", + "wsl.onboarding.versionMismatch": "A telepített verzió nem egyezik az asztali alkalmazás verziójával.", + "wsl.onboarding.adding": "Hozzáadás...", + "help.tabs.toast.ariaLabel": "Bemutatjuk a Tab-ket. A lapokkal megszervezheti munkáját és aktív munkameneteit", + "help.tabs.toast.dismiss": "Tabs információ elvetése", + "help.tabs.title": "Bemutatjuk a Tab-ket", + "help.tabs.description": "A lapokkal megszervezheti munkáját és aktív munkameneteit", + "help.tabs.date": "július 14", + "help.tabs.introduction": "A OpenCode Desktop mostantól lapokra épül.", + "help.tabs.sessions": + "Indítson új munkamenetet egy lapon, vagy nyisson meg egy meglévő munkamenetet bármelyik projektből. Nyisson meg egy új lapot, amikor valami újat kezd, és zárja be, ha végzett.", + "help.tabs.organize": + "Néhány lap nyitva tartása megkönnyíti az aktív munkamenetek megszervezését. Nevezze át a lapokat valami emlékezetesre, ha azt tervezi, hogy megtartja őket.", + "help.tabs.home": + "Minden munkamenetét és projektjét megtalálja az új Home képernyőn. Ha kiválaszt egy munkamenetet, az egy lapon nyílik meg.", + "help.tabs.persistence": "Amikor újra megnyitja az alkalmazást, a lapok továbbra is nyitva maradnak.", + "help.tabs.worktrees": + "Az új kialakítás még nem támogatja a Git Worktrees-t, hamarosan megjelenik. Ha tehát szeretné folytatni az előző elrendezést, a Beállításokban válthat az elrendezések között. Ne feledje, hogy az új elrendezés néhány héten belül állandóvá válik.", + "server.row.noUsername": "nincs felhasználónév", + "dialog.project.edit.title": "Projekt szerkesztése", + "dialog.project.edit.name": "Név", + "dialog.project.edit.icon": "Ikon", + "dialog.project.edit.icon.alt": "Projekt ikonra", + "dialog.project.edit.icon.hint": "Kattintson vagy húzza a képet", + "dialog.project.edit.icon.recommended": "Javasolt: 128x128px", + "dialog.project.edit.color": "Szín", + "dialog.project.edit.color.select": "Válassza ki a {{color}} színt", + "dialog.project.edit.worktree.startup": "Munkaterület indító szkriptje", + "dialog.project.edit.worktree.startup.description": "Új munkaterület (munkafa) létrehozása után fut.", + "dialog.project.edit.worktree.startup.placeholder": "például zsemle telepítés", + "dialog.releaseNotes.action.getStarted": "Kezdje el", + "dialog.releaseNotes.action.next": "Következő", + "dialog.releaseNotes.action.hideFuture": "Ne mutasd ezeket a jövőben", + "dialog.releaseNotes.media.alt": "Kiadás előnézete", + "context.breakdown.title": "Kontextus bontása", + "context.breakdown.note": + "A beviteli tokenek hozzávetőleges bontása. Az „Egyéb” a szerszámdefiníciókat és az általános költségeket tartalmazza.", + "context.breakdown.system": "Rendszer", + "context.breakdown.user": "Felhasználó", + "context.breakdown.assistant": "Helyettes", + "context.breakdown.tool": "Szerszámhívások", + "context.breakdown.other": "Más", + "context.systemPrompt.title": "Rendszer prompt", + "context.rawMessages.title": "Nyers üzenetek", + "context.export.session": "Exportálási munkamenet", + "context.stats.session": "Munkamenet", + "context.stats.messages": "Üzenetek", + "context.stats.provider": "Szolgáltató", + "context.stats.model": "Modell", + "context.stats.limit": "Kontextuskorlát", + "context.stats.totalTokens": "Összes token", + "context.stats.usage": "Használat", + "context.stats.inputTokens": "Beviteli tokenek", + "context.stats.outputTokens": "Kimeneti tokenek", + "context.stats.reasoningTokens": "Érvelési tokenek", + "context.stats.cacheTokens": "Gyorsítótár tokenek (írás/olvasás)", + "context.stats.userMessages": "Felhasználói üzenetek", + "context.stats.assistantMessages": "Segéd üzenetek", + "context.stats.totalCost": "Teljes költség", + "context.stats.sessionCreated": "Munkamenet létrehozva", + "context.stats.lastActivity": "Utolsó tevékenység", + "context.usage.tokens": "Tokenek", + "context.usage.usage": "Használat", + "context.usage.cost": "Költség", + "context.usage.clickToView": "Kattintson a kontextus megtekintéséhez", + "context.usage.view": "Tekintse meg a környezethasználatot", + "language.en": "angol", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "portugál (Brazília)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Nyelv", + "toast.language.description": "{{language}}-re váltott", + "toast.theme.title": "Téma váltva", + "toast.scheme.title": "Színséma", + "toast.workspace.enabled.title": "Munkaterületek engedélyezve", + "toast.workspace.enabled.description": "Most több munkafa is megjelenik az oldalsávban", + "toast.workspace.disabled.title": "Munkaterületek letiltva", + "toast.workspace.disabled.description": "Csak a fő munkafa jelenik meg az oldalsávban", + "toast.permissions.autoaccept.on.title": "Engedélyek automatikus elfogadása", + "toast.permissions.autoaccept.on.description": "Az engedélykérések automatikusan jóváhagyásra kerülnek", + "toast.permissions.autoaccept.off.title": "Az engedélyek automatikus elfogadása leállítva", + "toast.permissions.autoaccept.off.description": "Az engedélykérésekhez jóváhagyás szükséges", + "toast.model.none.title": "Nincs kiválasztott modell", + "toast.model.none.description": "Csatlakozzon egy szolgáltatóhoz a munkamenet összefoglalásához", + "toast.file.loadFailed.title": "Nem sikerült betölteni a fájlt", + "toast.file.listFailed.title": "Nem sikerült listázni a fájlokat", + "toast.context.noLineSelection.title": "Nincs vonalválasztás", + "toast.context.noLineSelection.description": "Először válasszon ki egy sortartományt egy fájl lapon.", + "toast.session.share.copyFailed.title": "Nem sikerült a URL vágólapra másolása", + "toast.session.share.success.title": "Munkamenet megosztva", + "toast.session.share.success.description": "Megosztás URL vágólapra másolva!", + "toast.session.share.failed.title": "Nem sikerült megosztani a munkamenetet", + "toast.session.share.failed.description": "Hiba történt a munkamenet megosztása közben", + "toast.session.unshare.success.title": "Munkamenet megosztása visszavonva", + "toast.session.unshare.success.description": "A munkamenet megosztása sikeresen visszavonva!", + "toast.session.unshare.failed.title": "Nem sikerült megszüntetni a munkamenet megosztását", + "toast.session.unshare.failed.description": "Hiba történt a munkamenet megosztásának megszüntetése közben", + "toast.session.export.success.title": "Munkamenet exportálva", + "toast.session.export.success.description": "A munkamenet elmentve ide: {{filename}}", + "toast.session.export.failed.title": "Nem sikerült exportálni a munkamenetet", + "toast.session.export.failed.description": "Hiba történt a munkamenet exportálása közben", + "toast.session.listFailed.title": "Nem sikerült betölteni a {{project}} munkameneteit", + "toast.project.reloadFailed.title": "A {{project}} újratöltése nem sikerült", + "toast.update.title": "Frissítés elérhető", + "toast.update.description": "A OpenCode ({{version}}) új verziója már telepíthető.", + "toast.update.action.installRestart": "Telepítse és indítsa újra", + "toast.update.action.notYet": "Még nem", + "error.page.title": "Valami elromlott", + "error.page.description": "Hiba történt az alkalmazás betöltése közben.", + "error.page.description.localServerStartup": "Hiba történt a helyi szerver indításakor.", + "error.page.details.label": "Hiba részletei", + "error.page.action.restart": "Indítsa újra", + "error.page.action.report": "Jelentés hiba", + "error.page.action.reported": "Hiba jelentve", + "error.page.action.exportLogs": "Naplók exportálása", + "error.page.action.checking": "Ellenőrzés...", + "error.page.action.checkUpdates": "Ellenőrizze a frissítéseket", + "error.page.action.updateTo": "Frissítés a {{version}}-re", + "error.page.circular": "[Kör alakú]", + "error.page.report.prefix": "Kérjük, jelentse ezt a hibát a OpenCode csapatának", + "error.page.report.discord": "a Discord-n", + "error.page.version": "Kivitel: {{version}}", + "error.dev.rootNotFound": + "A gyökérelem nem található. Elfelejtette hozzáadni az index.html-hez? Vagy lehet, hogy az id attribútumot rosszul írták?", + "error.serverSync.connectFailed": "Nem sikerült csatlakozni a szerverhez. A `{{url}}`-n fut szerver?", + "error.serverSDK.noServerAvailable": "Nincs elérhető szerver", + "error.serverSDK.serverNotAvailable": "A szerver nem elérhető", + "error.childStore.persistedCacheCreateFailed": "Nem sikerült létrehozni a tartós gyorsítótárat", + "error.childStore.persistedProjectMetadataCreateFailed": "Nem sikerült létrehozni a megmaradt projekt metaadatait", + "error.childStore.persistedProjectIconCreateFailed": "Nem sikerült létrehozni a megmaradt projekt ikont", + "error.childStore.storeCreateFailed": "Nem sikerült létrehozni az üzletet", + "directory.error.invalidUrl": "Érvénytelen könyvtár a URL-ben.", + "error.chain.unknown": "Ismeretlen hiba", + "error.server.invalidConfiguration": "Érvénytelen konfiguráció", + "error.chain.causedBy": "Oka:", + "error.chain.apiError": "API hiba", + "error.chain.status": "Állapot: {{status}}", + "error.chain.retryable": "Újrapróbálható: {{retryable}}", + "error.chain.responseBody": "Válasz törzse:\n{{body}}", + "error.chain.didYouMean": "Erre gondolt: {{suggestions}}?", + "error.chain.modelNotFound": "A modell nem található: {{provider}}/{{model}}", + "error.chain.checkConfig": "Ellenőrizze a konfigurációs (opencode.json) szolgáltató/modell nevét", + "error.chain.mcpFailed": + 'A "{{name}}" MCP szerver meghibásodott. Megjegyzés: a OpenCode még nem támogatja a MCP hitelesítést.', + "error.chain.providerAuthFailed": "A szolgáltató hitelesítése nem sikerült ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + "Nem sikerült inicializálni a „{{provider}}” szolgáltatót. Ellenőrizze a hitelesítő adatokat és a konfigurációt.", + "error.chain.configJsonInvalid": "A {{path}} konfigurációs fájlja nem érvényes JSON(C)", + "error.chain.configJsonInvalidWithMessage": "A {{path}} konfigurációs fájlja nem érvényes JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'A "{{dir}}" könyvtár a {{path}}-ben nem érvényes. Nevezze át a könyvtárat "{{suggestion}}" névre, vagy távolítsa el. Ez egy gyakori elírás.', + "error.chain.configFrontmatterError": "A frontanyag elemzése nem sikerült a {{path}}-ben:\n{{message}}", + "error.chain.configInvalid": "A {{path}} konfigurációs fájl érvénytelen", + "error.chain.configInvalidWithMessage": "A {{path}} konfigurációs fájlja érvénytelen: {{message}}", + "notification.permission.title": "Engedély szükséges", + "notification.permission.description": "A {{sessionTitle}} {{projectName}}-ben engedélyre van szüksége", + "notification.question.title": "Kérdés", + "notification.question.description": "A {{sessionTitle}} {{projectName}}-ben van egy kérdése", + "notification.action.goToSession": "Ugrás a munkamenetre", + "notification.session.responseReady.title": "Válasz kész", + "notification.session.error.title": "Munkamenet hiba", + "notification.session.error.fallbackDescription": "Hiba történt", + "home.recentProjects": "Legutóbbi projektek", + "home.empty.title": "Nincsenek újabb projektek", + "home.empty.description": "Kezdje el egy helyi projekt megnyitásával", + "home.title": "Kezdőlap", + "home.projects": "Projektek", + "home.project.add": "Projekt hozzáadása", + "home.recentlyClosed": "Nemrég zárva", + "home.server.collapse": "Szerverprojektek összecsukása", + "home.server.expand": "Bővítse ki a szerverprojekteket", + "home.sessions.search.placeholder": "Munkamenetek keresése", + "home.sessions.search.placeholder.scoped": "Keresés a munkamenetekben a {{scope}} nyelven", + "home.sessions.search.sessions": "Munkamenetek", + "home.sessions.search.noResults": "Nem található munkamenet a {{query}} számára", + "home.sessions.empty": "Itt még semmi", + "home.sessions.empty.description": "A kezdéshez hozzon létre egy munkamenetet", + "home.sessions.group.today": "Ma", + "home.sessions.group.yesterday": "Tegnap", + "home.sessions.group.older": "Régebbi", + "home.providerTip": + "Csatlakozzon 75+ szolgáltatóhoz más modellek használatához, beleértve a Claude, GPT, Gemini stb.", + "session.tab.session": "Munkamenet", + "session.tab.review": "Tekintse át", + "session.tab.context": "Kontextus", + "session.tab.unknown": "Ismeretlen munkamenet", + "session.panel.reviewAndFiles": "Áttekintés és fájlok", + "session.error.notFound": "Ez a munkamenet nem található", + "session.error.notFound.description": "Ez a lap olyan munkamenetre mutat, amely már nem létezik ezen a kiszolgálón.", + "session.error.notFound.closeTab": "Zárja be a Tab-t", + "session.error.serverConnection": "Nem lehet csatlakozni ehhez a szerverhez", + "session.review.filesChanged": "A fájlok megváltoztak {{count}}", + "session.review.change.one": "Változás", + "session.review.change.other": "Változások", + "session.review.loadingChanges": "Módosítások betöltése...", + "session.review.empty": "Ebben a munkamenetben még nincs változás", + "session.review.noVcs": "Nem észlelt Git verzióvezérlő rendszert, a változások nem jelennek meg", + "session.review.noVcs.createGit.title": "Hozzon létre egy Git tárolót", + "session.review.noVcs.createGit.description": + "Kövesse nyomon, tekintse át és vonja vissza a változtatásokat ebben a projektben", + "session.review.noVcs.createGit.actionLoading": "Git adattár létrehozása...", + "session.review.noVcs.createGit.action": "Git tároló létrehozása", + "session.review.noSnapshot": + "A pillanatkép-követés le van tiltva a konfigurációban, így a munkamenet-módosítások nem érhetők el", + "session.review.noChanges": "Nincs változás", + "session.review.noUncommittedChanges": "Még nincsenek végrehajtatlan változtatások", + "session.review.noBranchChanges": "A fióktelep még nem változott", + "session.files.selectToOpen": "Válassza ki a megnyitandó fájlt", + "session.files.all": "Minden fájl", + "session.files.empty": "Nincsenek fájlok", + "session.files.binaryContent": "Bináris fájl (a tartalom nem jeleníthető meg)", + "session.messages.renderEarlier": "A korábbi üzenetek megjelenítése", + "session.messages.loadingEarlier": "Korábbi üzenetek betöltése...", + "session.messages.loadEarlier": "Korábbi üzenetek betöltése", + "session.messages.loading": "Üzenetek betöltése...", + "session.messages.jumpToLatest": "Ugrás a legújabbra", + "session.context.addToContext": "Adja hozzá a {{selection}}-t a kontextushoz", + "session.todo.title": "Feladatok", + "session.todo.collapse": "Összeomlás", + "session.todo.expand": "Bontsa ki", + "session.todo.progress": "{{done}}/{{total}} todos elkészült", + "session.question.progress": "{{current}}/{{total}} kérdések", + "session.question.minimize": "Minimalizálja a kérdést", + "session.question.restore": "Kérdés visszaállítása", + "session.question.pending.one": "{{count}} függőben lévő kérdés", + "session.question.pending.other": "{{count}} függőben lévő kérdések", + "session.followupDock.summary.one": "{{count}} sorba állított üzenet", + "session.followupDock.summary.other": "{{count}} sorba állított üzenetek", + "session.followupDock.sendNow": "Küldje el most", + "session.followupDock.edit": "Szerkesztés", + "session.followupDock.collapse": "A sorba állított üzenetek összecsukása", + "session.followupDock.expand": "A sorban álló üzenetek kibontása", + "session.revertDock.summary.one": "{{count}} visszavont üzenet", + "session.revertDock.summary.other": "{{count}} visszavont üzenetek", + "session.revertDock.collapse": "A visszagörgetett üzenetek összecsukása", + "session.revertDock.expand": "A visszagörgetett üzenetek kibontása", + "session.revertDock.restore": "Üzenet visszaállítása", + "session.new.title": "Építsen bármit", + "session.new.project.new": "Új projekt", + "session.new.project.search": "Projektek keresése", + "session.new.project.add": "Projekt hozzáadása", + "session.new.worktree.main": "Fő ág", + "session.new.worktree.mainWithBranch": "Fő ág ({{branch}})", + "session.new.worktree.create": "Új munkafa létrehozása", + "session.new.workspace.runIn": "Futtassa a munkamenetet", + "session.new.workspace.triggerLocal": "Helyi", + "session.new.workspace.local": "Helyi adattár", + "session.new.workspace.existing": "Munkaterület…", + "session.new.git.none": "Nincs Git", + "session.new.lastModified": "Utoljára módosítva", + "session.header.search.placeholder": "Keresés {{project}}", + "session.header.searchFiles": "Fájlok keresése", + "session.header.openIn": "Nyissa meg", + "session.header.open.action": "Nyissa meg a {{app}}-t", + "session.header.open.ariaLabel": "Nyitás {{app}}-ben", + "session.header.open.menu": "Nyissa meg az opciókat", + "session.header.open.copyPath": "Útvonal másolása", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "Fájlkezelő", + "session.header.reveal.finder": "Mutasd be a Finder-ben", + "session.header.reveal.fileExplorer": "Mutasd be a File Explorer-ben", + "session.header.reveal.containingFolder": "Nyissa meg a tartalmazó mappát", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminál", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Állapot", + "status.popover.ariaLabel": "Szerver konfigurációk", + "status.popover.tab.servers": "Szerverek", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Beépülő modulok", + "status.popover.action.manageServers": "Szerverek kezelése", + "session.share.popover.title": "Közzététel a weben", + "session.share.popover.description.shared": "Ez a munkamenet nyilvános a weben. A link birtokában bárki hozzáférhet.", + "session.share.popover.description.unshared": + "Ossza meg a munkamenetet nyilvánosan az interneten. A link birtokában bárki számára elérhető lesz.", + "session.share.action.share": "Részesedés", + "session.share.action.publish": "Közzététel", + "session.share.action.publishing": "Kiadás...", + "session.share.action.unpublish": "Közzététel visszavonása", + "session.share.action.unpublishing": "Közzététel visszavonása...", + "session.share.action.view": "Nézet", + "session.share.copy.copied": "Másolva", + "session.share.copy.copyLink": "Link másolása", + "lsp.tooltip.none": "Nincsenek LSP szerverek", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Prompt betöltése...", + "terminal.loading": "Terminál betöltése...", + "terminal.title": "Terminál", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Zárja be a terminált", + "terminal.connectionLost.title": "Kapcsolat megszakadt", + "terminal.connectionLost.abnormalClose": "WebSocket rendellenesen zárva: {{code}}", + "terminal.connectionLost.description": + "A terminál kapcsolat megszakadt. Ez akkor fordulhat elő, amikor a szerver újraindul.", + "terminal.connectTicket.csrfError": + "A PTY csatlakozási jegyet az eredet vagy a CSRF ellenőrzések elutasították. Ellenőrizze a szerver CORS konfigurációját.", + "terminal.connectTicket.statusError": "A PTY csatlakozási jegy sikertelen a {{status}}-vel", + "titlebar.update": "Frissítés", + "titlebar.updateVersion": "Frissítse a {{version}}-t", + "common.closeTab": "Lap bezárása", + "common.dismiss": "Elvetés", + "common.moreCountSuffix": "(+{{count}} további)", + "common.requestFailed": "A kérés sikertelen", + "common.moreOptions": "További lehetőségek", + "common.learnMore": "További információ", + "common.rename": "Átnevezés", + "common.export": "Exportálás", + "common.reset": "Alaphelyzet", + "common.archive": "Archívum", + "common.delete": "Törlés", + "common.close": "Bezárás", + "common.edit": "Szerkesztés", + "common.loadMore": "Töltsön be többet", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Törlés", + "common.key.home": "Kezdőlap", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Beszúrás", + "common.unknown": "ismeretlen", + "common.time.justNow": "Éppen most", + "common.time.minutesAgo.short": "{{count}}m ezelőtt", + "common.time.hoursAgo.short": "{{count}}h ezelőtt", + "common.time.daysAgo.short": "{{count}}d ezelőtt", + "sidebar.menu.toggle": "Váltó menü", + "sidebar.nav.projectsAndSessions": "Projektek és foglalkozások", + "sidebar.settings": "Beállítások", + "sidebar.help": "Segítség", + "sidebar.workspaces.enable": "Munkaterületek engedélyezése", + "sidebar.workspaces.disable": "Munkaterületek letiltása", + "sidebar.gettingStarted.title": "Kezdő lépések", + "sidebar.gettingStarted.line1": "A OpenCode ingyenes modelleket tartalmaz, így azonnal elkezdheti.", + "sidebar.gettingStarted.line2": + "Csatlakozzon bármely szolgáltatóhoz a modellek használatához, pl. Claude, GPT, Gemini stb.", + "sidebar.project.recentSessions": "Legutóbbi munkamenetek", + "sidebar.project.viewAllSessions": "Az összes munkamenet megtekintése", + "sidebar.project.clearNotifications": "Értesítések törlése", + "sidebar.empty.title": "Nincs nyitott projekt", + "sidebar.empty.description": "A kezdéshez nyisson meg egy projektet", + "debugBar.ariaLabel": "Fejlesztési teljesítmény diagnosztika", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Utolsó befejezett útvonal-átmenet, amely egy munkamenet-oldalt érint, az útválasztó indulásától az első festésig mérve.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Gördülő képkockák másodpercenként az utolsó 5 másodpercben.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "A legrosszabb képkockaidő az elmúlt 5 másodpercben.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "32 ms-nál hosszabb képkockák az utolsó 5 másodpercben.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blokkolt idő és hosszú feladatok száma az utolsó 5 másodpercben. Max feladat: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "A legrosszabb megfigyelt bemeneti késleltetés az elmúlt 5 másodpercben.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Az interakció hozzávetőleges időtartama az elmúlt 5 másodpercben. Ez a INP-szerű, nem a hivatalos Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Összesített elrendezési eltolódás az alkalmazás jelenlegi élettartamára vonatkozóan.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Használt JS kupac a kupackorláthoz képest. Csak a Chromium.", + "debugBar.mem.tip": "Használt JS kupac a kupackorláthoz képest. {{used}} vagy {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "A fókuszstílusok kényszerítése az összes interaktív elemre", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": + "A teljes alkalmazáselrendezési irány kényszerítése a kiválasztott nyelv megváltoztatása nélkül", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode asztali számítógép", + "settings.section.desktop": "Asztali", + "settings.section.server": "Kiszolgáló", + "settings.tab.general": "Általános", + "settings.tab.shortcuts": "Parancsikonok", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integráció", + "settings.desktop.wsl.description": "Futtassa a OpenCode szervert a WSL belsejében a Windows rendszeren.", + "settings.general.section.appearance": "Megjelenés", + "settings.general.section.advanced": "Fejlett", + "settings.general.section.notifications": "Rendszerértesítések", + "settings.general.section.updates": "Frissítések", + "settings.general.section.sounds": "Hangeffektusok", + "settings.general.section.feed": "Takarmány", + "settings.general.section.display": "Kijelző", + "settings.general.row.language.title": "Nyelv", + "settings.general.row.language.description": "Módosítsa a OpenCode kijelző nyelvét", + "settings.general.row.shell.title": "Terminál shellje", + "settings.general.row.shell.description": "A terminál és az ügynökeszközök által használt shell", + "settings.general.row.shell.autoDefault": "Automatikus (alapértelmezett)", + "settings.general.row.shell.terminalOnly": "csak terminál", + "settings.general.row.appearance.title": "Megjelenés", + "settings.general.row.appearance.description": "Szabja személyre, hogyan néz ki a OpenCode az eszközén", + "settings.general.row.colorScheme.title": "Színséma", + "settings.general.row.colorScheme.description": + "Válassza ki, hogy a OpenCode a rendszer, a világos vagy a sötét témát követi-e", + "settings.general.row.theme.title": "Téma", + "settings.general.row.theme.description": "Szabja személyre a OpenCode témáját.", + "settings.general.row.font.title": "Kód betűtípus", + "settings.general.row.font.description": "Testreszabhatja a kódblokkban használt betűtípust", + "settings.general.row.terminalFont.title": "Terminal betűtípus", + "settings.general.row.terminalFont.description": "Testreszabhatja a terminálban használt betűtípust", + "settings.general.row.uiFont.title": "UI betűtípus", + "settings.general.row.uiFont.description": "Testreszabhatja a felületen használt betűtípust", + "settings.general.row.followup.title": "Nyomon követési viselkedés", + "settings.general.row.followup.description": + "Válassza ki, hogy a nyomon követési felszólítások azonnal irányítsák, vagy várakozzanak a sorban", + "settings.general.row.followup.option.queue": "Sor", + "settings.general.row.followup.option.steer": "Tinó", + "settings.general.row.showFileTree.title": "Fájlfa", + "settings.general.row.showFileTree.description": "A fájlfa panel megjelenítése a munkamenetekben", + "settings.general.row.showNavigation.title": "Navigációs vezérlők", + "settings.general.row.showNavigation.description": "A vissza és előre gombok megjelenítése az asztal címsorában", + "settings.general.row.showSearch.title": "Parancs paletta", + "settings.general.row.showSearch.description": "A kereső és a parancspaletta gomb megjelenítése a címsorban", + "settings.general.row.showTerminal.title": "Terminál", + "settings.general.row.showTerminal.description": "Jelenítse meg a terminál gombot az asztal címsorában", + "settings.general.row.showStatus.title": "Szerver állapota", + "settings.general.row.showStatus.description": "Jelenítse meg a szerver állapot gombját a címsorban", + "settings.general.row.mobileTitlebarBottom.title": "Alsó navigáció", + "settings.general.row.mobileTitlebarBottom.description": + "Helyezze el a címsort és a munkamenet füleket a képernyő aljára mobileszközön", + "settings.general.row.showCustomAgents.title": "Mutasd az ügynököt", + "settings.general.row.showCustomAgents.description": + "Váltás az ágensek között a zeneszerzőben. Ha rejtett, alapértelmezés szerint Build agent.", + "settings.general.row.reasoningSummaries.title": "Mutasson érvelési összefoglalókat", + "settings.general.row.reasoningSummaries.description": "Modell indoklási összefoglalók megjelenítése az idővonalon", + "settings.general.row.shellToolPartsExpanded.title": "Shelleszköz részeinek kibontása", + "settings.general.row.shellToolPartsExpanded.description": + "Alapértelmezés szerint kibontott shell eszközrészek megjelenítése az idővonalon", + "settings.general.row.editToolPartsExpanded.title": "A Szerkesztőeszköz részei kibontása", + "settings.general.row.editToolPartsExpanded.description": + "Az idővonalon alapértelmezés szerint kibontott szerkesztési, írási és javítási eszközrészek megjelenítése", + "settings.general.row.newInterface.title": "Új elrendezés", + "settings.general.row.newInterface.badge": "Új", + "settings.general.row.newInterface.description": + "Használja az új lapokat és a kezdőlap elrendezését. Váltás az elrendezések között korlátozott ideig.", + "settings.general.row.newInterfaceNotice.title": "Most új elrendezést használ", + "settings.general.row.newInterfaceNotice.description": "Az előző elrendezés már nem elérhető", + "settings.general.row.newInterfaceNotice.dismiss": "Elvetés", + "settings.general.row.pinchZoom.title": "Csípje össze a nagyításhoz", + "settings.general.row.pinchZoom.description": + "Engedélyezze az érintőpad összecsípését és a Ctrl-görgetést a nagyításhoz", + "settings.general.row.wayland.title": "Használja a natív Wayland-t", + "settings.general.row.wayland.description": "A X11 tartalék letiltása a Wayland-n. Újraindítást igényel.", + "settings.general.row.wayland.tooltip": + "A vegyes frissítési gyakoriságú monitorokkal rendelkező Linux esetében a natív Wayland stabilabb lehet.", + "settings.general.row.releaseNotes.title": "Kiadási megjegyzések", + "settings.general.row.releaseNotes.description": "Az Újdonságok előugró ablakok megjelenítése a frissítések után", + "settings.updates.row.startup.title": "Indításkor ellenőrizze a frissítéseket", + "settings.updates.row.startup.description": "A OpenCode indításakor automatikusan ellenőrizze a frissítéseket", + "settings.updates.row.check.title": "Ellenőrizze a frissítéseket", + "settings.updates.row.check.description": "Manuálisan ellenőrizze a frissítéseket, és telepítse, ha elérhető", + "settings.updates.action.checkNow": "Ellenőrizze most", + "settings.updates.action.checking": "Ellenőrzés...", + "settings.updates.action.downloading": "Letöltés...", + "settings.updates.action.installing": "Telepítés...", + "settings.updates.toast.latest.title": "Ön naprakész", + "settings.updates.toast.latest.description": "Ön a OpenCode legújabb verzióját futtatja.", + "sound.option.none": "Egyik sem", + "sound.option.alert01": "Figyelmeztetés 01", + "sound.option.alert02": "Figyelmeztetés 02", + "sound.option.alert03": "Figyelmeztetés 03", + "sound.option.alert04": "Figyelmeztetés 04", + "sound.option.alert05": "Figyelmeztetés 05", + "sound.option.alert06": "Figyelmeztetés 06", + "sound.option.alert07": "Figyelmeztetés 07", + "sound.option.alert08": "Figyelmeztetés 08", + "sound.option.alert09": "Figyelmeztetés 09", + "sound.option.alert10": "Figyelmeztetés 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nem 01", + "sound.option.nope02": "Nem 02", + "sound.option.nope03": "Nem 03", + "sound.option.nope04": "Nem 04", + "sound.option.nope05": "Nem 05", + "sound.option.nope06": "Nem 06", + "sound.option.nope07": "Nem 07", + "sound.option.nope08": "Nem 08", + "sound.option.nope09": "Nem 09", + "sound.option.nope10": "Nem 10", + "sound.option.nope11": "Nem 11", + "sound.option.nope12": "Nem 12", + "sound.option.yup01": "Igen 01", + "sound.option.yup02": "Igen 02", + "sound.option.yup03": "Igen 03", + "sound.option.yup04": "Igen 04", + "sound.option.yup05": "Igen 05", + "sound.option.yup06": "Igen 06", + "settings.general.notifications.agent.title": "Ügynök", + "settings.general.notifications.agent.description": + "Rendszerértesítés megjelenítése, ha az ügynök befejeződött vagy beavatkozásra van szüksége", + "settings.general.notifications.permissions.title": "Engedélyek", + "settings.general.notifications.permissions.description": + "Rendszerértesítés megjelenítése, ha engedélyre van szükség", + "settings.general.notifications.errors.title": "Hibák", + "settings.general.notifications.errors.description": "Rendszerértesítés megjelenítése hiba esetén", + "settings.general.sounds.agent.title": "Ügynök", + "settings.general.sounds.agent.description": "Hang lejátszása, ha az ügynök befejeződött vagy figyelmet igényel", + "settings.general.sounds.permissions.title": "Engedélyek", + "settings.general.sounds.permissions.description": "Hang lejátszása, ha engedélyre van szükség", + "settings.general.sounds.errors.title": "Hibák", + "settings.general.sounds.errors.description": "Hang lejátszása hiba esetén", + "settings.shortcuts.title": "Billentyűparancsok", + "settings.shortcuts.reset.button": "Állítsa vissza az alapértelmezett értékeket", + "settings.shortcuts.reset.toast.title": "Parancsikonok visszaállítása", + "settings.shortcuts.reset.toast.description": "A billentyűparancsok visszaálltak az alapértelmezett értékekre.", + "settings.shortcuts.conflict.title": "A parancsikon már használatban van", + "settings.shortcuts.conflict.description": "A {{keybind}} már hozzá van rendelve a {{titles}}-hez.", + "settings.shortcuts.unassigned": "Nincs hozzárendelve", + "settings.shortcuts.pressKeys": "Nyomja meg a gombokat", + "settings.shortcuts.search.placeholder": "Parancsikonok keresése", + "settings.shortcuts.search.empty": "Nem találhatók parancsikonok", + "settings.shortcuts.group.general": "Általános", + "settings.shortcuts.group.session": "Munkamenet", + "settings.shortcuts.group.navigation": "Navigáció", + "settings.shortcuts.group.modelAndAgent": "Modell és ügynök", + "settings.shortcuts.group.terminal": "Terminál", + "settings.shortcuts.group.prompt": "Utasítás", + "settings.providers.title": "Szolgáltatók", + "settings.providers.description": "A szolgáltató beállításai itt konfigurálhatók.", + "settings.providers.section.connected": "Csatlakozott szolgáltatók", + "settings.providers.connected.empty": "Nincsenek csatlakoztatott szolgáltatók", + "settings.providers.connected.environmentDescription": "A környezeti változókból csatlakozik", + "settings.providers.section.popular": "Népszerű szolgáltatók", + "settings.providers.custom.description": "Adjon hozzá egy OpenAI-kompatibilis szolgáltatót az alap URL segítségével.", + "settings.providers.tag.environment": "Környezet", + "settings.providers.tag.config": "Konfiguráció", + "settings.providers.tag.custom": "Szokás", + "settings.providers.tag.other": "Más", + "settings.models.title": "Modellek", + "settings.models.description": "A modellbeállítások itt konfigurálhatók.", + "settings.agents.title": "Ügynökök", + "settings.agents.description": "Az ügynök beállításai itt konfigurálhatók.", + "settings.commands.title": "Parancsok", + "settings.commands.description": "A parancsbeállítások itt konfigurálhatók.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "A MCP beállítások itt konfigurálhatók.", + "settings.permissions.title": "Engedélyek", + "settings.permissions.description": + "Szabályozza, hogy a szerver milyen eszközöket használhat alapértelmezés szerint.", + "settings.permissions.section.tools": "Eszközök", + "settings.permissions.toast.updateFailed.title": "Nem sikerült frissíteni az engedélyeket", + "settings.permissions.action.allow": "Engedélyezze", + "settings.permissions.action.ask": "Kérdez", + "settings.permissions.action.deny": "Megtagadás", + "settings.permissions.tool.read.title": "Olvas", + "settings.permissions.tool.read.description": "Fájl olvasása (egyezik a fájl elérési útjával)", + "settings.permissions.tool.edit.title": "Szerkesztés", + "settings.permissions.tool.edit.description": + "Fájlok módosítása, beleértve a szerkesztéseket, írásokat és javításokat", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Fájlok egyezése glob-mintákkal", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Keresés a fájl tartalmában reguláris kifejezések használatával", + "settings.permissions.tool.list.title": "Lista", + "settings.permissions.tool.list.description": "Fájlok listázása egy könyvtárban", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Futtassa a shell parancsokat", + "settings.permissions.tool.task.title": "Feladat", + "settings.permissions.tool.task.description": "Al-ügynökök indítása", + "settings.permissions.tool.skill.title": "Ügyesség", + "settings.permissions.tool.skill.description": "Töltsön be egy képességet név szerint", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Nyelvi szerver lekérdezések futtatása", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Frissítse a teendők listáját", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Tartalom lekérése URL-ről", + "settings.permissions.tool.websearch.title": "Webes keresés", + "settings.permissions.tool.websearch.description": "Keresés a weben", + "settings.permissions.tool.external_directory.title": "Külső címtár", + "settings.permissions.tool.external_directory.description": "Hozzáférés a projektkönyvtáron kívüli fájlokhoz", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Ismétlődő szerszámhívások észlelése azonos bemenettel", + "session.delete.failed.title": "Nem sikerült törölni a munkamenetet", + "session.delete.title": "Delete munkamenet", + "session.delete.confirm": "Delete „{{name}}” munkamenet?", + "session.delete.button": "Delete munkamenet", + "workspace.new": "Új munkaterület", + "workspace.type.local": "helyi", + "workspace.type.sandbox": "homokozó", + "workspace.create.failed.title": "Nem sikerült létrehozni a munkaterületet", + "workspace.delete.failed.title": "Nem sikerült törölni a munkaterületet", + "workspace.resetting.title": "Munkaterület visszaállítása", + "workspace.resetting.description": "Ez eltarthat egy percig.", + "workspace.reset.failed.title": "Nem sikerült visszaállítani a munkaterületet", + "workspace.reset.success.title": "Munkaterület visszaállítása", + "workspace.reset.success.description": "A munkaterület most megegyezik az alapértelmezett ággal.", + "workspace.error.stillPreparing": "A munkaterület még készül", + "workspace.status.checking": "Nem egyesített módosítások ellenőrzése...", + "workspace.status.error": "Nem lehet ellenőrizni a git állapotát.", + "workspace.status.clean": "Nem észleltek összevont változtatásokat.", + "workspace.status.dirty": "Nem egyesített változtatásokat észleltünk ezen a munkaterületen.", + "workspace.delete.title": "Delete munkaterület", + "workspace.delete.confirm": 'Delete munkaterület "{{name}}"?', + "workspace.delete.button": "Delete munkaterület", + "workspace.reset.title": "Állítsa vissza a munkaterületet", + "workspace.reset.confirm": "Visszaállítja a „{{name}}” munkaterületet?", + "workspace.reset.button": "Állítsa vissza a munkaterületet", + "workspace.reset.archived.none": "Az aktív munkamenetek nem lesznek archiválva.", + "workspace.reset.archived.one": "1 munkamenet archiválva lesz.", + "workspace.reset.archived.many": "A {{count}} munkamenetek archiválva lesznek.", + "workspace.reset.note": "Ezzel visszaállítja a munkaterületet, hogy megfeleljen az alapértelmezett ágnak.", + "dialog.usageExceeded.dontShowAgain": "Ne jelenjen meg újra", +} diff --git a/packages/app/src/i18n/hy.ts b/packages/app/src/i18n/hy.ts new file mode 100644 index 0000000000000000000000000000000000000000..6eac43b5e6dd55566f24df28c1af503e3a9aefac --- /dev/null +++ b/packages/app/src/i18n/hy.ts @@ -0,0 +1,1165 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Ֆայլ", + "desktop.menu.edit": "Խմբագրել", + "desktop.menu.view": "Դիտել", + "desktop.menu.go": "Գնալ", + "desktop.menu.window": "Պատուհան", + "desktop.menu.help": "Օգնություն", + "desktop.menu.checkForUpdates": "Ստուգեք թարմացումների համար...", + "desktop.menu.settings": "Կարգավորումներ", + "desktop.menu.reloadWebview": "Վերբեռնել Webview", + "desktop.menu.restart": "Վերագործարկեք", + "desktop.menu.exportLogs": "Արտահանման մատյաններ...", + "desktop.menu.newSession": "Նոր նիստ", + "desktop.menu.openProject": "Բաց նախագիծ...", + "desktop.menu.newWindow": "Նոր պատուհան", + "desktop.menu.closeWindow": "Փակել պատուհանը", + "desktop.menu.undo": "Հետարկել", + "desktop.menu.redo": "Կրկնել", + "desktop.menu.cut": "Կտրել", + "desktop.menu.copy": "Պատճեն", + "desktop.menu.paste": "Կպցնել", + "desktop.menu.delete": "Ջնջել", + "desktop.menu.selectAll": "Ընտրել բոլորը", + "desktop.menu.toggleSidebar": "Փոխարկել կողագոտին", + "desktop.menu.toggleTerminal": "Փոխարկել տերմինալը", + "desktop.menu.toggleFileTree": "Փոխարկել ֆայլերի ծառը", + "desktop.menu.reload": "Վերբեռնել", + "desktop.menu.toggleDeveloperTools": "Փոխարկել մշակողի գործիքները", + "desktop.menu.actualSize": "Փաստացի չափ", + "desktop.menu.zoomIn": "Խոշորացնել", + "desktop.menu.zoomOut": "Փոքրացնել", + "desktop.menu.toggleFullScreen": "Փոխարկել ամբողջ էկրանը", + "desktop.menu.back": "Հետ", + "desktop.menu.forward": "Առաջ", + "desktop.menu.previousSession": "Նախորդ նիստ", + "desktop.menu.nextSession": "Հաջորդ նիստ", + "desktop.menu.previousProject": "Նախորդ նախագիծ", + "desktop.menu.nextProject": "Հաջորդ նախագիծ", + "desktop.menu.minimize": "Նվազագույնի հասցնել", + "desktop.menu.maximize": "Առավելագույնի հասցնել", + "desktop.menu.documentation": "OpenCode Փաստաթղթեր", + "desktop.menu.supportForum": "Աջակցման ֆորում", + "desktop.menu.shareFeedback": "Կիսվել կարծիք", + "desktop.menu.reportBug": "Հաղորդել սխալի մասին", + "desktop.menu.ariaLabel": "OpenCode մենյու", + "desktop.updater.dialog.checkFailed.message": "Թարմացման ստուգումը ձախողվեց։", + "desktop.updater.dialog.checkFailed.title": "Թարմացման սխալ", + "desktop.updater.dialog.upToDate.message": "Դուք արդիական եք", + "desktop.updater.dialog.upToDate.title": "Թարմացումներ չկան", + "desktop.updater.dialog.ready.message": "Թարմացումը {{version}} ներբեռնվել է։ Վերագործարկե՞լ հիմա:", + "desktop.updater.dialog.ready.title": "Թարմացումը պատրաստ է", + "desktop.updater.dialog.restart": "Վերագործարկեք", + "desktop.updater.dialog.later": "Հետագայում", + "desktop.recovery.action.relaunch": "Վերագործարկում", + "desktop.recovery.action.exportLogs": "Արտահանման մատյաններ", + "desktop.recovery.action.keepWaiting": "Շարունակեք սպասել", + "desktop.recovery.action.quit": "Դուրս գալ", + "desktop.recovery.loadFailed": "OpenCode չհաջողվեց բեռնել", + "desktop.recovery.terminated": "OpenCode պատուհանն անսպասելիորեն ավարտվեց", + "desktop.recovery.unresponsive": "OpenCode-ը չի արձագանքում", + "desktop.recovery.unresponsive.detail": + "Դուք կարող եք վերագործարկել հավելվածը, բացել տեղեկամատյանները կամ շարունակել սպասել:", + "desktop.recovery.loadFailed.detail": "Պատուհան՝ {{window}}\nURL: {{url}}\nՍխալ՝ {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Պատուհան՝ {{window}}\nՊատճառը՝ {{reason}}\nԿոդ՝ {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Ընտրեք թղթապանակ", + "desktop.dialog.chooseFile": "Ընտրեք ֆայլ", + "desktop.dialog.saveFile": "Պահպանել ֆայլը", + "desktop.dialog.files": "Ֆայլեր", + "desktop.server.local": "Տեղական սերվեր", + "desktop.wsl.error.windowsOnly": "WSL հասանելի է միայն Windows", + "desktop.wsl.error.unavailable": "WSL անհասանելի է", + "desktop.wsl.error.listInstalled": "Չհաջողվեց ցուցակել տեղադրված WSL բաշխումները", + "desktop.wsl.error.listOnline": "Չհաջողվեց թվարկել առցանց WSL բաշխումները", + "desktop.wsl.error.executeDistro": "Հնարավոր չէ կատարել հրամաններ բաշխման մեջ", + "desktop.wsl.error.installWsl": "WSL տեղադրումը ձախողվեց", + "desktop.wsl.error.installDistro": "Չհաջողվեց տեղադրել բաշխում՝ {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode տեղադրումը ձախողվեց", + "desktop.wsl.error.alreadyAdded": "{{distro}} արդեն ավելացված է", + "desktop.wsl.error.opencodeMissing": "opencode-ը տեղադրված չէ այս բաշխում", + "desktop.wsl.error.opencodeCannotRun": "opencode-ը տեղադրված է, բայց չի կարող գործարկվել", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode տեղադրված չէ {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode թարմացումն ավարտված է, բայց {{distro}}-ը դեռ հայտնում է {{installed}}; սպասվում է {{expected}}", + "desktop.wsl.error.noVersion": "առանց տարբերակ", + "desktop.wsl.error.serverExited": "WSL սերվերը դուրս է եկել գործարկումից հետո (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL սերվերը դուրս է եկել առողջանալուց առաջ (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "{{distro}} առողջության ստուգման կողային մեքենայի ժամանակը սպառվել է {{timeout}} ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} ժամանակը սպառվել է {{timeout}}ms", + "desktop.wsl.error.failedPort": "Չհաջողվեց ստանալ միացքը", + "desktop.picker.error.notSelected": "Ֆայլը ընտրողի կողմից ընտրված չէ", + "desktop.picker.error.sizeLimit": "Ընտրված հավելվածները գերազանցում են {{limit}} ՄԲ սահմանաչափը", + "command.category.suggested": "Առաջարկված", + "command.category.view": "Դիտել", + "command.category.project": "Նախագիծ", + "command.category.provider": "Մատակարար", + "command.category.server": "Սերվեր", + "command.category.session": "Նիստ", + "command.category.theme": "Թեմա", + "command.category.language": "Լեզու", + "command.category.file": "Ֆայլ", + "command.category.context": "Համատեքստ", + "command.category.terminal": "Տերմինալ", + "command.category.model": "Մոդել", + "command.category.mcp": "MCP", + "command.category.agent": "Գործակալ", + "command.category.permissions": "Թույլտվություններ", + "command.category.workspace": "Աշխատանքային տարածք", + "command.category.settings": "Կարգավորումներ", + "command.logs.export": "Արտահանել տեղեկամատյանները", + "theme.scheme.system": "Համակարգ", + "theme.scheme.light": "Բաց", + "theme.scheme.dark": "Մուգ", + "command.sidebar.toggle": "Փոխարկել կողագոտին", + "command.project.open": "Բացել նախագիծ", + "command.project.previous": "Նախորդ նախագիծ", + "command.project.next": "Հաջորդ նախագիծ", + "command.project.index": "Անցնել նախագծին {{index}}", + "command.provider.connect": "Միացեք մատակարարին", + "command.server.switch": "Փոխարկել սերվեր", + "command.settings.open": "Բացել կարգավորումները", + "command.session.previous": "Նախորդ նիստ", + "command.session.next": "Հաջորդ նիստ", + "command.session.previous.unseen": "Նախորդ չկարդացված նիստը", + "command.session.next.unseen": "Հաջորդ չկարդացված նիստը", + "command.session.archive": "Արխիվացնել նիստը", + "command.palette": "Հրամանների գունապնակ", + "command.theme.cycle": "Ցիկլի թեմա", + "command.theme.set": "Օգտագործել թեման՝ {{theme}}", + "command.theme.scheme.cycle": "Ցիկլի գունային սխեման", + "command.theme.scheme.set": "Օգտագործել գունային սխեման՝ {{scheme}}", + "command.language.cycle": "Ցիկլի լեզու", + "command.language.set": "Օգտագործել լեզուն՝ {{language}}", + "command.session.new": "Նոր նիստ", + "command.file.open": "Բացել ֆայլ", + "command.tab.close": "Փակել ներդիրը", + "command.tab.reopenClosed": "Վերաբացել փակ ներդիրը", + "command.context.addSelection": "Ավելացնել ընտրությունը համատեքստում", + "command.context.addSelection.description": "Ավելացնել ընտրված տողերը ընթացիկ ֆայլից", + "command.input.focus": "Կիզակետային մուտքագրում", + "command.terminal.toggle": "Փոխարկել տերմինալը", + "command.fileTree.toggle": "Փոխարկել ֆայլի ծառը", + "command.review.toggle": "Փոխարկել ակնարկ", + "command.terminal.new": "Նոր տերմինալ", + "command.terminal.new.description": "Ստեղծել նոր տերմինալի ներդիր", + "command.steps.toggle": "Փոխարկել քայլերը", + "command.steps.toggle.description": "Ցույց տալ կամ թաքցնել ընթացիկ հաղորդագրության քայլերը", + "command.message.previous": "Նախորդ հաղորդագրություն", + "command.message.previous.description": "Անցնել օգտվողի նախորդ հաղորդագրությունը", + "command.message.next": "Հաջորդ հաղորդագրություն", + "command.message.next.description": "Անցնել օգտվողի հաջորդ հաղորդագրությունը", + "command.model.choose": "Ընտրեք մոդել", + "command.model.choose.description": "Ընտրեք այլ մոդել", + "command.mcp.toggle": "Փոխարկել MCPs", + "command.mcp.toggle.description": "Փոխարկել MCPs", + "command.agent.cycle": "Փոխարկել գործակալը", + "command.agent.cycle.description": "Անցնել հաջորդ գործակալին", + "command.agent.cycle.reverse": "Անցնել նախորդ գործակալին", + "command.agent.cycle.reverse.description": "Անցնել նախորդ գործակալին", + "command.model.variant.cycle": "Փոխել մտածողության ջանքի մակարդակը", + "command.model.variant.cycle.description": "Անցնել հաջորդ ջանքերի մակարդակին", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Հուշում", + "command.permissions.autoaccept.enable": "Ավտոմատ ընդունել թույլտվությունները", + "command.permissions.autoaccept.disable": "Դադարեցնել թույլտվությունների ավտոմատ ընդունումը", + "command.workspace.toggle": "Փոխարկել աշխատանքային տարածքները", + "command.workspace.toggle.description": "Միացնել կամ անջատել բազմաթիվ աշխատանքային տարածքներ կողագոտում", + "command.session.undo": "Հետարկել", + "command.session.undo.description": "Հետարկել վերջին հաղորդագրությունը", + "command.session.redo": "Կրկնել", + "command.session.redo.description": "Կրկնել վերջին չեղարկված հաղորդագրությունը", + "command.session.compact": "Կոմպակտ նիստ", + "command.session.compact.description": "Ամփոփեք նիստը՝ համատեքստի չափը նվազեցնելու համար", + "command.session.fork": "Ստեղծել ճյուղ հաղորդագրությունից", + "command.session.fork.description": "Ստեղծել նոր նիստ նախորդ հաղորդագրությունից", + "command.session.share": "Կիսվել նիստ", + "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": "Արդյունքներ չեն գտնվել", + "palette.group.commands": "Հրամաններ", + "palette.group.files": "Ֆայլեր", + "dialog.provider.search.placeholder": "Որոնել մատակարարների", + "dialog.provider.empty": "Ոչ մի մատակարար չի գտնվել", + "dialog.provider.group.popular": "Հանրաճանաչ", + "dialog.provider.group.other": "Այլ", + "dialog.provider.custom.label": "Պատվիրված OpenAI-համատեղելի մատակարար", + "dialog.provider.tag.recommended": "Խորհուրդ է տրվում", + "dialog.provider.opencode.note": "Ընտրված մոդելներ, ներառյալ՝ Claude, GPT, Gemini և ավելին", + "dialog.provider.opencode.tagline": "Վստահելի օպտիմիզացված մոդելներ", + "dialog.provider.opencodeGo.tagline": "Ցածր գնով բաժանորդագրություն բոլորի համար", + "dialog.provider.anthropic.note": "Ուղիղ մուտք դեպի Claude մոդելներ, ներառյալ Pro և Max", + "dialog.provider.copilot.note": "AI մոդելներ կոդավորման օգնության համար՝ GitHub Copilot", + "dialog.provider.openai.note": "GPT մոդելներ արագ, ունակ ընդհանուր AI առաջադրանքների համար", + "dialog.provider.google.note": "Gemini մոդելներ արագ, կառուցվածքային պատասխանների համար", + "dialog.provider.openrouter.note": "Մուտք գործեք բոլոր աջակցվող մոդելները մեկ մատակարարից", + "dialog.provider.vercel.note": "Միասնական մուտք դեպի AI մոդելներ՝ խելացի երթուղիներով", + "dialog.model.select.title": "Ընտրել մոդել", + "dialog.model.search.placeholder": "Որոնել մոդելներ", + "dialog.model.empty": "Մոդելի արդյունքներ չկա", + "dialog.model.manage": "Կառավարել մոդելները", + "dialog.model.manage.description": "Անհատականացրեք, թե որ մոդելները կհայտնվեն մոդելի ընտրիչում:", + "dialog.model.manage.provider.toggle": "Փոխարկել բոլոր {{provider}} մոդելները", + "dialog.model.unpaid.freeModels.title": "Անվճար մոդելները տրամադրվում են OpenCode", + "dialog.model.unpaid.addMore.title": "Ավելացնել ավելի շատ մոդելներ հանրաճանաչ մատակարարներից", + "dialog.model.unpaid.viewMoreProviders": "Տես 70+ այլ մատակարարներ", + "dialog.provider.viewAll": "Ցույց տալ ավելի շատ մատակարարներ", + "provider.connect.title": "Միացեք {{provider}}", + "provider.connect.title.anthropicProMax": "Մուտք գործեք Claude Pro/Max-ով", + "provider.connect.selectMethod": "Ընտրեք մուտքի եղանակը {{provider}}-ի համար։", + "provider.connect.method.apiKey": "API բանալի", + "provider.connect.method.browser": "Դիտարկիչ", + "provider.connect.method.headless": "Առանց գրաֆիկական միջերեսի", + "provider.connect.status.inProgress": "Լիազորումն ընթացքի մեջ է...", + "provider.connect.status.waiting": "Սպասում է թույլտվության...", + "provider.connect.status.failed": "Լիազորումը ձախողվեց՝ {{error}}", + "provider.connect.apiKey.description": + "Մուտքագրեք ձեր {{provider}} API բանալին՝ ձեր հաշիվը միացնելու և OpenCode-ում {{provider}} մոդելներ օգտագործելու համար։", + "provider.connect.apiKey.label": "{{provider}} API բանալի", + "provider.connect.apiKey.placeholder": "API բանալի", + "provider.connect.apiKey.required": "API բանալի է պահանջվում", + "provider.connect.opencodeZen.line1": + "OpenCode Zen-ը թույլ է տալիս մուտք գործել կոդավորման գործակալների համար հուսալի օպտիմիզացված մոդելների հավաքածու։", + "provider.connect.opencodeZen.line2": + "Մեկ API բանալիով դուք մուտք կունենաք այնպիսի մոդելների, ինչպիսիք են՝ Claude, GPT, Gemini, GLM և ավելին:", + "provider.connect.opencodeZen.visit.prefix": "Այցելություն ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " ձեր API բանալին հավաքելու համար", + "provider.connect.oauth.code.visit.prefix": "Այցելություն ", + "provider.connect.oauth.code.visit.link": "այս հղումը", + "provider.connect.oauth.code.visit.suffix": + " հավաքել ձեր թույլտվության կոդը՝ ձեր հաշիվը միացնելու և OpenCode-ում {{provider}} մոդելներ օգտագործելու համար:", + "provider.connect.oauth.code.label": "{{method}} թույլտվության կոդ", + "provider.connect.oauth.code.placeholder": "Լիցենզավորման կոդ", + "provider.connect.oauth.code.required": "Պահանջվում է լիազորման կոդը", + "provider.connect.oauth.code.invalid": "Սխալ թույլտվության կոդը", + "provider.connect.oauth.auto.visit.prefix": "Այցելություն ", + "provider.connect.oauth.auto.visit.link": "այս հղումը", + "provider.connect.oauth.auto.visit.suffix": + " և մուտքագրեք ստորև ծածկագիրը՝ ձեր հաշիվը միացնելու և OpenCode-ում {{provider}} մոդելներ օգտագործելու համար:", + "provider.connect.oauth.auto.confirmationCode": "Հաստատման կոդը", + "provider.connect.toast.connected.title": "{{provider}} կապված", + "provider.connect.toast.connected.description": "{{provider}} մոդելներ այժմ հասանելի են օգտագործման համար։", + "provider.custom.title": "Պատվերով մատակարար", + "provider.custom.unavailable": "Պատվերով մատակարարներն անհասանելի են այս սերվերում", + "provider.custom.description.prefix": "Կարգավորել OpenAI-ի հետ համատեղելի մատակարար: Տեսեք ", + "provider.custom.description.link": "մատակարարի կազմաձևման փաստաթղթեր", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Մատակարարի ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Փոքրատառ, թվեր, գծիկներ կամ ընդգծում", + "provider.custom.field.name.label": "Ցուցադրման անուն", + "provider.custom.field.name.placeholder": "Իմ AI մատակարարը", + "provider.custom.field.baseURL.label": "Հիմնական URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API բանալի", + "provider.custom.field.apiKey.placeholder": "API բանալի", + "provider.custom.field.apiKey.description": + "Ըստ ցանկության։ Դատարկ թողեք, եթե դուք կառավարում եք հեղինակությունը վերնագրերի միջոցով:", + "provider.custom.models.label": "Մոդելներ", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Անուն", + "provider.custom.models.name.placeholder": "Ցուցադրման անուն", + "provider.custom.models.remove": "Հեռացնել մոդելը", + "provider.custom.models.add": "Ավելացնել մոդել", + "provider.custom.headers.label": "Վերնագրեր (ըստ ցանկության)", + "provider.custom.headers.key.label": "Վերնագիր", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Արժեք", + "provider.custom.headers.value.placeholder": "արժեք", + "provider.custom.headers.remove": "Հեռացնել վերնագիրը", + "provider.custom.headers.add": "Ավելացնել վերնագիր", + "provider.custom.error.providerID.required": "Պահանջվում է մատակարարի ID", + "provider.custom.error.providerID.format": "Օգտագործեք փոքրատառեր, թվեր, գծիկներ կամ ընդգծում", + "provider.custom.error.providerID.exists": "Այդ մատակարարի ID-ն արդեն գոյություն ունի", + "provider.custom.error.name.required": "Ցուցադրել անունը պարտադիր է", + "provider.custom.error.baseURL.required": "Պահանջվում է բազային URL", + "provider.custom.error.baseURL.format": "Պետք է սկսել http:// կամ https://", + "provider.custom.error.required": "Պահանջվում", + "provider.custom.error.duplicate": "Կրկնօրինակ", + "provider.disconnect.toast.disconnected.title": "{{provider}} անջատված է", + "provider.disconnect.toast.disconnected.description": "{{provider}} մոդելներն այլևս հասանելի չեն:", + "model.tag.free": "Անվճար", + "model.tag.latest": "Վերջին", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "տեքստ", + "model.input.image": "պատկեր", + "model.input.audio": "ձայն", + "model.input.video": "տեսանյութ", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Թույլ է տալիս՝ {{inputs}}", + "model.tooltip.context.label": "Համատեքստ", + "model.tooltip.inputs": "Մուտքագրումներ", + "model.tooltip.model": "Մոդել", + "model.tooltip.provider": "Մատակարար", + "model.tooltip.reasoning": "պատճառաբանում", + "model.tooltip.reasoning.allowed": "Թույլ է տալիս պատճառաբանել", + "model.tooltip.reasoning.none": "Ոչ հիմնավորում", + "model.tooltip.context": "Համատեքստային սահմանաչափ՝ {{limit}}", + "common.search.placeholder": "Որոնում", + "common.goBack": "Նավարկեք հետ", + "common.goForward": "Նավարկել առաջ", + "common.loading": "Բեռնվում", + "common.loading.ellipsis": "...", + "common.cancel": "Չեղարկել", + "common.clear": "Մաքրել", + "common.open": "Բացել", + "common.connect": "Միացեք", + "common.disconnect": "Անջատել կապը", + "common.continue": "Շարունակել", + "common.submit": "Ներկայացնել", + "common.save": "Պահել", + "common.saving": "Պահպանում...", + "common.default": "Լռելյայն", + "common.attachment": "կցված", + "prompt.placeholder.shell": "Մուտքագրեք shell հրամանը... {{example}}", + "prompt.placeholder.normal": "Հարցրեք որևէ բան... «{{example}}»", + "prompt.placeholder.simple": "Հարցրեք որեւէ բան...", + "prompt.placeholder.summarizeComments": "Ամփոփել մեկնաբանությունները…", + "prompt.placeholder.summarizeComment": "Ամփոփել մեկնաբանությունը…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Հուշում", + "prompt.mode.shell.exit": "դուրս գալու համար՝ esc", + "session.child.promptDisabled": "Ենթագենտի նիստերը չեն կարող հուշվել:", + "session.child.backToParent": "Վերադառնալ հիմնական նիստին։", + "prompt.example.1": "Ստուգել TODO կոդերի բազայում", + "prompt.example.2": "Ինչպիսի՞ն է այս նախագծի տեխնիկական փաթեթը։", + "prompt.example.3": "Ստուգել կոտրված թեստերը", + "prompt.example.4": "Բացատրեք, թե ինչպես է աշխատում նույնականացումը", + "prompt.example.5": "Գտեք և շտկեք անվտանգության խոցելիությունները", + "prompt.example.6": "Ավելացնել միավորի թեստեր օգտվողի ծառայության համար", + "prompt.example.7": "Վերափոխեք այս ֆունկցիան ավելի ընթեռնելի", + "prompt.example.8": "Ինչ է նշանակում այս սխալը", + "prompt.example.9": "Օգնիր ինձ կարգաբերել այս խնդիրը", + "prompt.example.10": "Ստեղծել API փաստաթղթեր", + "prompt.example.11": "Օպտիմալացնել տվյալների բազայի հարցումները", + "prompt.example.12": "Ավելացնել մուտքագրման վավերացում", + "prompt.example.13": "Ստեղծել նոր բաղադրիչ...", + "prompt.example.14": "Ինչպե՞ս կարող եմ տեղակայել այս նախագիծը", + "prompt.example.15": "Վերանայել իմ կոդը լավագույն փորձի համար", + "prompt.example.16": "Ավելացնել սխալների մշակումն այս ֆունկցիային", + "prompt.example.17": "Բացատրեք այս ռեգեքսի օրինակը", + "prompt.example.18": "Փոխարկեք սա TypeScript", + "prompt.example.19": "Ավելացնել գրանցում կոդերի բազայում", + "prompt.example.20": "Ի՞նչ կախվածություններն են հնացած:", + "prompt.example.21": "Օգնիր ինձ գրել միգրացիոն սցենար", + "prompt.example.22": "Իրականացնել քեշավորում այս վերջնակետի համար", + "prompt.example.23": "Ավելացնել էջադրում այս ցանկին", + "prompt.example.24": "Ստեղծել CLI հրաման...", + "prompt.example.25": "Ինչպե՞ս են այստեղ աշխատում շրջակա միջավայրի փոփոխականները", + "prompt.popover.emptyResults": "Համապատասխան արդյունքներ չկա", + "prompt.popover.emptyCommands": "Համապատասխան հրամաններ", + "prompt.dropzone.label": "Թողեք պատկերներ, PDF կամ տեքստային ֆայլեր այստեղ", + "prompt.dropzone.file.label": "Թողնել @mention ֆայլին", + "prompt.slash.badge.custom": "պատվերով", + "prompt.slash.badge.skill": "հմտություն", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "ակտիվ", + "prompt.context.includeActiveFile": "Ներառել ակտիվ ֆայլ", + "prompt.context.removeActiveFile": "Հեռացնել ակտիվ ֆայլը համատեքստից", + "prompt.context.removeFile": "Հեռացնել ֆայլը համատեքստից", + "prompt.action.attachFile": "Ավելացնել ֆայլեր", + "prompt.menu.addImagesAndFiles": "Ավելացնել ֆայլեր և ավելին", + "prompt.menu.imagesAndFiles": "Պատկերներ և ֆայլեր", + "prompt.menu.commands": "Հրամաններ", + "prompt.menu.context": "Համատեքստ", + "prompt.menu.shellCommand": "Shell հրաման", + "prompt.attachment.remove": "Հեռացնել հավելվածը", + "prompt.action.send": "Ուղարկել", + "prompt.action.stop": "Կանգնեցնել", + "prompt.toast.pasteUnsupported.title": "Չաջակցվող հավելված", + "prompt.toast.pasteUnsupported.description": "Այստեղ կարող են կցվել միայն պատկերներ, PDF կամ տեքստային ֆայլեր։", + "prompt.toast.attachmentDuplicate.title": "Այս ֆայլն արդեն վերբեռնվել է", + "prompt.toast.modelAgentRequired.title": "Ընտրեք գործակալ և մոդել", + "prompt.toast.modelAgentRequired.description": "Ընտրեք գործակալ և մոդել՝ նախքան հուշում ուղարկելը։", + "prompt.toast.worktreeCreateFailed.title": "Չհաջողվեց ստեղծել աշխատանքային ծառը", + "prompt.toast.sessionCreateFailed.title": "Չհաջողվեց ստեղծել նիստ", + "prompt.toast.shellSendFailed.title": "Չհաջողվեց ուղարկել shell հրամանը", + "prompt.toast.commandSendFailed.title": "Չհաջողվեց ուղարկել հրամանը", + "prompt.toast.promptSendFailed.title": "Չհաջողվեց ուղարկել հուշումը", + "prompt.toast.promptSendFailed.description": "Անհնար է առբերել նիստը", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}}-ը {{total}}-ից միացված է", + "dialog.mcp.empty": "Կազմաձևված չէ MCPs", + "dialog.lsp.empty": "LSPs ավտոմատ հայտնաբերվում է ֆայլերի տեսակներից", + "dialog.plugins.empty": "Ընդլայնումները կազմաձևված են opencode.json-ում", + "mcp.status.connected": "միացված", + "mcp.status.failed": "ձախողվեց", + "mcp.status.needs_auth": "անհրաժեշտ է վավերացում", + "mcp.status.disabled": "անջատված", + "mcp.auth.clickToAuthenticate": "Սեղմեք՝ նույնականացնելու համար", + "dialog.fork.empty": "Չկան պատառաքաղված հաղորդագրություններ", + "dialog.directory.search.placeholder": "Որոնել թղթապանակներ", + "dialog.directory.empty": "Պանակներ չեն գտնվել", + "dialog.directory.action.selectFile": "Ընտրել ֆայլը", + "dialog.directory.action.selectFolder": "Ընտրել թղթապանակ", + "dialog.directory.root": "Արմատ", + "dialog.directory.parent": "Ծնող", + "dialog.directory.readError": "Անհնար է կարդալ այս թղթապանակը", + "app.server.unreachable": "Չհաջողվեց հասնել {{server}}", + "app.server.retrying": "Նորից ինքնաբերաբար...", + "app.server.otherServers": "Այլ սերվերներ", + "dialog.server.title": "Սերվերներ", + "dialog.server.description": "Փոխարկեք, թե որ OpenCode սերվերին է միանում այս հավելվածը։", + "dialog.server.search.placeholder": "Որոնման սերվերներ", + "dialog.server.empty": "Սերվերներ դեռ չկան", + "dialog.server.add.title": "Ավելացնել սերվեր", + "dialog.server.add.url": "Սերվերի հասցեն", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Չհաջողվեց միանալ սերվերին", + "dialog.server.add.checking": "Ստուգում...", + "dialog.server.add.button": "Ավելացնել սերվեր", + "dialog.server.add.name": "Սերվերի անունը (ըստ ցանկության)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Օգտագործողի անուն (ըստ ցանկության)", + "dialog.server.add.usernamePlaceholder": "օգտվողի անուն", + "dialog.server.add.password": "Գաղտնաբառ (ըստ ցանկության)", + "dialog.server.add.passwordPlaceholder": "գաղտնաբառ", + "dialog.server.edit.title": "Խմբագրել սերվերը", + "dialog.server.default.title": "Լռելյայն սերվեր", + "dialog.server.default.description": + "Միացեք այս սերվերին հավելվածի գործարկման ժամանակ՝ տեղական սերվեր գործարկելու փոխարեն։ Պահանջվում է վերագործարկում:", + "dialog.server.default.none": "ընտրված չէ սերվեր", + "dialog.server.default.set": "Սահմանել ընթացիկ սերվերը որպես լռելյայն", + "dialog.server.default.clear": "Մաքրել", + "dialog.server.action.remove": "Հեռացնել սերվերը", + "dialog.server.menu.edit": "Խմբագրել", + "dialog.server.menu.default": "Սահմանել որպես լռելյայն", + "dialog.server.menu.defaultRemove": "Հեռացնել լռելյայն", + "dialog.server.menu.delete": "Ջնջել", + "dialog.server.current": "Ընթացիկ սերվեր", + "dialog.server.status.default": "Լռելյայն", + "wsl.server.add": "Ավելացնել WSL սերվեր", + "wsl.server.addShort": "Ավելացնել WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL սերվեր", + "wsl.server.retryStart": "Փորձեք նորից սկսել", + "wsl.server.updating": "Թարմացվում է...", + "wsl.onboarding.step.distro": "Ընտրեք բաշխում", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Ստուգում է WSL...", + "wsl.onboarding.restartRequired": "Windows-ին անհրաժեշտ է վերագործարկում՝ WSL-ի տեղադրումն ավարտելու համար։", + "wsl.onboarding.ready": "WSL պատրաստ է։", + "wsl.onboarding.required": "WSL անհրաժեշտ է շարունակելու համար։", + "wsl.onboarding.checkingDistros": "Ստուգում է բաշխումները...", + "wsl.onboarding.installingDistro": "Տեղադրվում է {{distro}}...", + "wsl.onboarding.checkingDistro": "Ստուգում է {{distro}}...", + "wsl.onboarding.listingDistros": "Ցուցակման բաշխումներ...", + "wsl.onboarding.distroReady": "{{distro}} պատրաստ է։", + "wsl.onboarding.distroNotInstalled": "{{distro}} դեռ տեղադրված չէ։", + "wsl.onboarding.openDistroOnce": "Բացեք {{distro}}-ը մեկ անգամ՝ կարգավորումն ավարտելու համար:", + "wsl.onboarding.finishingDistro": "{{distro}}-ի կարգավորումն ավարտվում է։", + "wsl.onboarding.pickDistro": "Ընտրեք բաշխումը կամ տեղադրեք ստորև։", + "wsl.onboarding.checkingOpencode": "Ստուգում է OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Ստուգում է OpenCode-ը {{distro}}...", + "wsl.onboarding.updatingOpencode": "Թարմացվում է OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Թարմացվում է OpenCode-ը {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Թարմացնել OpenCode-ը {{distro}}-ում։", + "wsl.onboarding.updateOpencode": "Թարմացնել OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode պատրաստ է {{distro}}-ում։", + "wsl.onboarding.opencodeReady": "OpenCode պատրաստ է։", + "wsl.onboarding.installOpencodeIn": "Տեղադրեք OpenCode-ը {{distro}}-ում։", + "wsl.onboarding.installOpencode": "Տեղադրեք OpenCode", + "wsl.onboarding.chooseDistroFirst": "Ընտրեք նախաբաշխումը։", + "wsl.onboarding.loadFailed": "Չհաջողվեց բեռնել WSL վիճակը։", + "wsl.onboarding.loading": "Բեռնվում է...", + "wsl.onboarding.installedDistros": "Տեղադրված բաշխումներ", + "wsl.onboarding.checkAgain": "Ստուգել նորից", + "wsl.onboarding.distroStatus.ready": "Պատրաստ է", + "wsl.onboarding.distroStatus.checking": "Ստուգում...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode տեղադրված չէ", + "wsl.onboarding.distroStatus.missingTools": "Բացակայում են bash-ը և curl-ը", + "wsl.onboarding.distroStatus.unsupported": "Չի աջակցվում · Օգտագործել WSL 2", + "wsl.onboarding.needAnotherDistro": "Պետք է մեկ այլ բաշխում", + "wsl.onboarding.needAnotherDistroHint": "Տեղադրեք Linux բաշխումը WSL կատալոգից", + "wsl.onboarding.wslNotInstalled.title": "WSL տեղադրված չէ", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Ենթահամակարգը Linux-ի համար) անհրաժեշտ է, որպեսզի OpenCode-ը կարողանա ավելացնել WSL սերվեր", + "wsl.onboarding.wslUnavailable.title": "WSL անհասանելի է", + "wsl.onboarding.wslUnavailable.description": "OpenCode-ը չկարողացավ հաստատել WSL-ն այս մեքենայի վրա։", + "wsl.onboarding.installWsl": "Տեղադրեք WSL", + "wsl.onboarding.windowsRestartRequired": + "Վերագործարկեք Windows՝ WSL-ի տեղադրումն ավարտելու համար, այնուհետև նորից բացեք OpenCode։", + "wsl.onboarding.next": "Հաջորդ", + "wsl.onboarding.refresh": "Թարմացնել", + "wsl.onboarding.allDistrosAdded": "Բոլոր տեղադրված բաշխումները արդեն ավելացված են։", + "wsl.onboarding.noDistros": "Դեռևս տարածումներ չեն հայտնաբերվել։", + "wsl.onboarding.install": "Տեղադրել", + "wsl.onboarding.installing": "Տեղադրվում է...", + "wsl.onboarding.installDistro": "Տեղադրել դիստրիբուտիվը", + "wsl.onboarding.searchDistros": "Որոնման բաշխումներ", + "wsl.onboarding.wsl2Required": "WSL 2 պարտադիր է։", + "wsl.onboarding.toolsRequired": "Այս դիստրիբուտիվին անհրաժեշտ են bash և curl։", + "wsl.onboarding.openTerminal": "Բացել տերմինալը", + "wsl.onboarding.path": "Ուղին՝ {{path}}", + "wsl.onboarding.notFound": "չի գտնվել", + "wsl.onboarding.version": "Տարբերակ՝ {{version}}", + "wsl.onboarding.unknown": "անհայտ", + "wsl.onboarding.desktopVersion": "աշխատասեղանի {{version}}", + "wsl.onboarding.versionMismatch": "Տեղադրված տարբերակը չի համապատասխանում աշխատասեղանի հավելվածի տարբերակին։", + "wsl.onboarding.adding": "Ավելացվում է...", + "help.tabs.toast.ariaLabel": "Ներկայացնում ենք ներդիրները։ Կազմակերպեք ձեր աշխատանքը և ակտիվ նիստերը ներդիրներով", + "help.tabs.toast.dismiss": "Փակել ներդիրների տեղեկությունները", + "help.tabs.title": "Ներկայացնում ենք ներդիրները", + "help.tabs.description": "Կազմակերպեք ձեր աշխատանքը և ակտիվ նիստերը ներդիրներով", + "help.tabs.date": "հուլիսի 14", + "help.tabs.introduction": "OpenCode Desktop-ն այժմ կառուցված է ներդիրների շուրջ։", + "help.tabs.sessions": + "Սկսեք նոր նիստ ներդիրում կամ բացեք գոյություն ունեցող նիստը ձեր ցանկացած նախագծից: Բացեք նոր ներդիր, երբ ինչ-որ նոր բան եք սկսում, և փակեք այն, երբ ավարտեք:", + "help.tabs.organize": + "Մի քանի ներդիր բաց պահելը հեշտացնում է ձեր ակտիվ նիստերի կազմակերպումը։ Վերանվանեք ներդիրները հիշարժան որևէ բանի, եթե նախատեսում եք դրանք պահել:", + "help.tabs.home": + "Դուք կգտնեք ձեր բոլոր նիստերն ու նախագծերը նոր Հիմնական էկրանին։ Ընտրելով նիստը՝ այն բացվում է ներդիրում:", + "help.tabs.persistence": "Երբ վերաբացեք հավելվածը, ձեր ներդիրները դեռ բաց են։", + "help.tabs.worktrees": + "Նոր դիզայնը դեռ չի աջակցում Git Worktrees-ին, այն շուտով կլինի: Այսպիսով, եթե նախընտրում եք շարունակել օգտագործել նախորդ դասավորությունը, կարող եք անցնել դասավորությունների միջև Կարգավորումներում: Պարզապես հիշեք, որ նոր դասավորությունը կդառնա մշտական ​​մի քանի շաբաթից:", + "server.row.noUsername": "առանց օգտվողի անուն", + "dialog.project.edit.title": "Խմբագրել նախագիծը", + "dialog.project.edit.name": "Անուն", + "dialog.project.edit.icon": "Պատկերակ", + "dialog.project.edit.icon.alt": "Նախագծի պատկերակ", + "dialog.project.edit.icon.hint": "Սեղմեք կամ քաշեք պատկերը", + "dialog.project.edit.icon.recommended": "Խորհուրդ է տրվում՝ 128x128px", + "dialog.project.edit.color": "Գունավոր", + "dialog.project.edit.color.select": "Ընտրել {{color}} գույնը", + "dialog.project.edit.worktree.startup": "Աշխատատարածքի գործարկման սկրիպտ", + "dialog.project.edit.worktree.startup.description": + "Աշխատում է նոր աշխատանքային տարածք (աշխատանքային ծառ) ստեղծելուց հետո", + "dialog.project.edit.worktree.startup.placeholder": "օր. bun տեղադրում", + "dialog.releaseNotes.action.getStarted": "Սկսել", + "dialog.releaseNotes.action.next": "Հաջորդ", + "dialog.releaseNotes.action.hideFuture": "Չցուցադրել դրանք ապագայում", + "dialog.releaseNotes.media.alt": "Թողարկման նախադիտում", + "context.breakdown.title": "Համատեքստային բաշխում", + "context.breakdown.note": + "Մուտքային նշանների մոտավոր բաժանում։ «Այլ»-ը ներառում է գործիքի սահմանումները և վերադիր ծախսերը:", + "context.breakdown.system": "Համակարգ", + "context.breakdown.user": "Օգտվող", + "context.breakdown.assistant": "Օգնական", + "context.breakdown.tool": "Գործիքների զանգեր", + "context.breakdown.other": "Այլ", + "context.systemPrompt.title": "Համակարգի հուշում", + "context.rawMessages.title": "Հում հաղորդագրություններ", + "context.export.session": "Արտահանման նիստ", + "context.stats.session": "Նիստ", + "context.stats.messages": "Հաղորդագրություններ", + "context.stats.provider": "Մատակարար", + "context.stats.model": "Մոդել", + "context.stats.limit": "Համատեքստային սահմանափակում", + "context.stats.totalTokens": "Ընդհանուր նշաններ", + "context.stats.usage": "Օգտագործում", + "context.stats.inputTokens": "Մուտքային նշաններ", + "context.stats.outputTokens": "Ելքի նշաններ", + "context.stats.reasoningTokens": "Դատողության թոքեններ", + "context.stats.cacheTokens": "Քեշի թոքեններ (կարդալ/գրել)", + "context.stats.userMessages": "Օգտվողի հաղորդագրություններ", + "context.stats.assistantMessages": "Օգնականի հաղորդագրություններ", + "context.stats.totalCost": "Ընդհանուր արժեքը", + "context.stats.sessionCreated": "Նիստը ստեղծվել է", + "context.stats.lastActivity": "Վերջին գործունեությունը", + "context.usage.tokens": "Թոքեններ", + "context.usage.usage": "Օգտագործում", + "context.usage.cost": "Արժեքը", + "context.usage.clickToView": "Սեղմեք՝ համատեքստը դիտելու համար", + "context.usage.view": "Դիտել համատեքստի օգտագործումը", + "language.en": "Անգլերեն", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Դանսկ", + "language.ja": "日本語", + "language.pl": "Պոլսկի", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Նորսկ", + "language.br": "Պորտուգալիա (Բրազիլիա)", + "language.bs": "Բոսանսկի", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Լեզու", + "toast.language.description": "Անցվել է {{language}}", + "toast.theme.title": "Թեմա է փոխվել", + "toast.scheme.title": "Գունային սխեման", + "toast.workspace.enabled.title": "Աշխատանքային տարածքները միացված են", + "toast.workspace.enabled.description": "Բազմաթիվ աշխատանքային ծառեր այժմ ցուցադրված են կողագոտում", + "toast.workspace.disabled.title": "Աշխատանքային տարածքներն անջատված են", + "toast.workspace.disabled.description": "Միայն հիմնական աշխատանքային ծառը ցուցադրված է կողագոտում", + "toast.permissions.autoaccept.on.title": "Ավտոմատների ընդունում", + "toast.permissions.autoaccept.on.description": "Թույլտվության հարցումներն ավտոմատ կերպով կհաստատվեն", + "toast.permissions.autoaccept.off.title": "Դադարեցվել է ավտոմատ ընդունելության թույլտվությունները", + "toast.permissions.autoaccept.off.description": "Թույլտվության հարցումները կպահանջեն հաստատում", + "toast.model.none.title": "Ընտրված չէ մոդել", + "toast.model.none.description": "Միացրեք մատակարարին՝ այս նիստն ամփոփելու համար", + "toast.file.loadFailed.title": "Չհաջողվեց բեռնել ֆայլը", + "toast.file.listFailed.title": "Չհաջողվեց ցուցակագրել ֆայլերը", + "toast.context.noLineSelection.title": "Տող չկա", + "toast.context.noLineSelection.description": "Ընտրեք տողերի տիրույթը ֆայլի ներդիրում:", + "toast.session.share.copyFailed.title": "Չհաջողվեց պատճենել URL-ը սեղմատախտակում", + "toast.session.share.success.title": "Նիստը համօգտագործված", + "toast.session.share.success.description": "Կիսվել URL-ը պատճենված է սեղմատախտակում:", + "toast.session.share.failed.title": "Չհաջողվեց կիսել նիստը", + "toast.session.share.failed.description": "Սխալ է տեղի ունեցել նիստը կիսելիս", + "toast.session.unshare.success.title": "Նիստի համօգտագործումը դադարեցված է", + "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": "Թարմացումը հասանելի է", + "toast.update.description": "OpenCode-ի ({{version}}) նոր տարբերակը այժմ հասանելի է տեղադրման համար:", + "toast.update.action.installRestart": "Տեղադրել և վերագործարկել", + "toast.update.action.notYet": "դեռ ոչ", + "error.page.title": "Սխալ առաջացավ", + "error.page.description": "Սխալ է տեղի ունեցել հավելվածը բեռնելիս։", + "error.page.description.localServerStartup": "Սխալ է տեղի ունեցել տեղական սերվերը գործարկելիս:", + "error.page.details.label": "Սխալի մանրամասներ", + "error.page.action.restart": "Վերագործարկեք", + "error.page.action.report": "Հաղորդել սխալ", + "error.page.action.reported": "Հաղորդվել է սխալ", + "error.page.action.exportLogs": "Արտահանման մատյաններ", + "error.page.action.checking": "Ստուգում...", + "error.page.action.checkUpdates": "Ստուգեք թարմացումների համար", + "error.page.action.updateTo": "Թարմացնել {{version}}", + "error.page.circular": "[Circular]", + "error.page.report.prefix": "Խնդրում ենք հայտնել այս սխալի մասին OpenCode թիմին", + "error.page.report.discord": "Discord", + "error.page.version": "Տարբերակ՝ {{version}}", + "error.dev.rootNotFound": + "Արմատային տարրը չի գտնվել։ Մոռացե՞լ եք այն ավելացնել ձեր index.html-ում: Կամ գուցե id հատկանիշը սխալ է գրվել:", + "error.serverSync.connectFailed": "Չհաջողվեց միանալ սերվերին։ Կա՞ սերվեր, որն աշխատում է `{{url}}`-ում:", + "error.serverSDK.noServerAvailable": "Սերվերը հասանելի չէ", + "error.serverSDK.serverNotAvailable": "Սերվերը հասանելի չէ", + "error.childStore.persistedCacheCreateFailed": "Չհաջողվեց ստեղծել մշտական ​​քեշ", + "error.childStore.persistedProjectMetadataCreateFailed": "Չհաջողվեց ստեղծել ծրագրի մշտական ​​մետատվյալներ", + "error.childStore.persistedProjectIconCreateFailed": "Չհաջողվեց ստեղծել շարունակական նախագծի պատկերակը", + "error.childStore.storeCreateFailed": "Չհաջողվեց ստեղծել խանութ", + "directory.error.invalidUrl": "Անվավեր գրացուցակ URL-ում։", + "error.chain.unknown": "Անհայտ սխալ", + "error.server.invalidConfiguration": "Սխալ կազմաձևում", + "error.chain.causedBy": "Պատճառված է՝", + "error.chain.apiError": "API սխալ", + "error.chain.status": "Կարգավիճակը՝ {{status}}", + "error.chain.retryable": "Կրկին փորձարկելի՝ {{retryable}}", + "error.chain.responseBody": "Պատասխանող մարմին՝\n{{body}}", + "error.chain.didYouMean": "Դուք նկատի ունեք՝ {{suggestions}}", + "error.chain.modelNotFound": "Մոդելը չի ​​գտնվել՝ {{provider}}/{{model}}", + "error.chain.checkConfig": "Ստուգեք ձեր կազմաձևը (opencode.json) մատակարարի/մոդելի անունները", + "error.chain.mcpFailed": + "MCP «{{name}}» սերվերը ձախողվեց: Նկատի ունեցեք, որ OpenCode-ը դեռ չի աջակցում MCP նույնականացումը:", + "error.chain.providerAuthFailed": "Մատակարարի նույնականացումը ձախողվեց ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + "Չհաջողվեց նախաստորագրել «{{provider}}» մատակարարը։ Ստուգեք հավատարմագրերը և կազմաձևումը:", + "error.chain.configJsonInvalid": "Կարգավորման ֆայլը {{path}}-ում վավեր չէ JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Կարգավորման ֆայլը {{path}}-ում վավեր չէ JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + "«{{dir}}» գրացուցակը {{path}}-ում վավեր չէ: Վերանվանել գրացուցակը «{{suggestion}}» կամ հեռացնել այն: Սա սովորական տառասխալ է:", + "error.chain.configFrontmatterError": "Չհաջողվեց վերլուծել ճակատային նյութը {{path}}-ում՝\n{{message}}", + "error.chain.configInvalid": "Կարգավորման ֆայլը {{path}}-ում անվավեր է", + "error.chain.configInvalidWithMessage": "Կարգավորման ֆայլը {{path}}-ում անվավեր է՝ {{message}}", + "notification.permission.title": "Պահանջվում է թույլտվություն", + "notification.permission.description": "{{sessionTitle}}-ը {{projectName}}-ում թույլտվություն է պահանջում", + "notification.question.title": "Հարց", + "notification.question.description": "{{sessionTitle}} {{projectName}}-ում հարց կա", + "notification.action.goToSession": "Անցնել նիստին", + "notification.session.responseReady.title": "Պատասխանը պատրաստ է", + "notification.session.error.title": "Նիստի սխալ", + "notification.session.error.fallbackDescription": "Սխալ է տեղի ունեցել", + "home.recentProjects": "Վերջին նախագծեր", + "home.empty.title": "Վերջին նախագծեր չկան", + "home.empty.description": "Սկսեք՝ բացելով տեղական նախագիծ", + "home.title": "Տուն", + "home.projects": "Նախագծեր", + "home.project.add": "Ավելացնել նախագիծ", + "home.recentlyClosed": "Վերջերս փակված", + "home.server.collapse": "Փլուզել սերվերի նախագծերը", + "home.server.expand": "Ընդլայնել սերվերի նախագծերը", + "home.sessions.search.placeholder": "Որոնման նիստեր", + "home.sessions.search.placeholder.scoped": "Որոնել նիստեր {{scope}}-ում", + "home.sessions.search.sessions": "Նիստեր", + "home.sessions.search.noResults": "{{query}}", + "home.sessions.empty": "Այստեղ դեռ ոչինչ", + "home.sessions.empty.description": "Ստեղծեք նիստ՝ սկսելու համար", + "home.sessions.group.today": "Այսօր", + "home.sessions.group.yesterday": "Երեկ", + "home.sessions.group.older": "Հին", + "home.providerTip": + "Միացեք 75+ մատակարարների՝ այլ մոդելներ օգտագործելու համար, այդ թվում՝ Claude, GPT, Gemini և այլն", + "session.tab.session": "Նիստ", + "session.tab.review": "Վերանայել", + "session.tab.context": "Համատեքստ", + "session.tab.unknown": "Անհայտ նիստ", + "session.panel.reviewAndFiles": "Վերանայել և ֆայլեր", + "session.error.notFound": "Այս նիստը հնարավոր չէ գտնել", + "session.error.notFound.description": "Այս ներդիրը ցույց է տալիս նիստ, որն այլևս գոյություն չունի այս սերվերում։", + "session.error.notFound.closeTab": "Փակել ներդիրը", + "session.error.serverConnection": "Հնարավոր չէ միանալ այս սերվերին", + "session.review.filesChanged": "Ֆայլերը փոխվել են {{count}}", + "session.review.change.one": "Փոխել", + "session.review.change.other": "Փոփոխություններ", + "session.review.loadingChanges": "Բեռնվում են փոփոխություններ...", + "session.review.empty": "Այս նիստում դեռ փոփոխություններ չկան", + "session.review.noVcs": "Չի հայտնաբերվել Git Տարբերակի կառավարման համակարգ, փոփոխությունները չեն ցուցադրվում", + "session.review.noVcs.createGit.title": "Ստեղծել Git պահոց", + "session.review.noVcs.createGit.description": "Հետեւել, վերանայել և հետարկել այս նախագծի փոփոխությունները", + "session.review.noVcs.createGit.actionLoading": "Ստեղծվում է Git պահոց...", + "session.review.noVcs.createGit.action": "Ստեղծել Git պահոց", + "session.review.noSnapshot": + "Snapshot-ի հետագծումն անջատված է կազմաձևում, ուստի նիստի փոփոխություններն անհասանելի են", + "session.review.noChanges": "Ոչ փոփոխություններ", + "session.review.noUncommittedChanges": "Դեռեւս չկատարված փոփոխություններ", + "session.review.noBranchChanges": "Ճյուղի փոփոխություններ դեռ չկան", + "session.files.selectToOpen": "Ընտրեք ֆայլ բացելու համար", + "session.files.all": "Բոլոր ֆայլերը", + "session.files.empty": "Ֆայլեր չկան", + "session.files.binaryContent": "Երկուական ֆայլ (բովանդակությունը չի կարող ցուցադրվել)", + "session.messages.renderEarlier": "Ներկայացնել ավելի վաղ հաղորդագրությունները", + "session.messages.loadingEarlier": "Նախկին հաղորդագրությունների բեռնում...", + "session.messages.loadEarlier": "Բեռնել ավելի վաղ հաղորդագրությունները", + "session.messages.loading": "Բեռնվում է հաղորդագրություններ...", + "session.messages.jumpToLatest": "Անցնել վերջին", + "session.context.addToContext": "Ավելացնել {{selection}} համատեքստում", + "session.todo.title": "Անելիքներ", + "session.todo.collapse": "Ծալել", + "session.todo.expand": "Ընդարձակել", + "session.todo.progress": "{{done}}՝ {{total}} գործերից ավարտված", + "session.question.progress": "{{current}}՝ {{total}} հարցից", + "session.question.minimize": "Նվազագույնի հասցնել հարցը", + "session.question.restore": "Վերականգնել հարցը", + "session.question.pending.one": "{{count}} առկախ հարց", + "session.question.pending.other": "{{count}} առկախ հարցեր", + "session.followupDock.summary.one": "{{count}} հերթագրված հաղորդագրություն", + "session.followupDock.summary.other": "{{count}} հերթագրված հաղորդագրություններ", + "session.followupDock.sendNow": "Ուղարկել հիմա", + "session.followupDock.edit": "Խմբագրել", + "session.followupDock.collapse": "Ծալել հերթագրված հաղորդագրությունները", + "session.followupDock.expand": "Ընդարձակել հերթագրված հաղորդագրությունները", + "session.revertDock.summary.one": "{{count}} վերադարձված հաղորդագրություն", + "session.revertDock.summary.other": "{{count}} վերադարձված հաղորդագրություններ", + "session.revertDock.collapse": "Ծալել վերադարձված հաղորդագրությունները", + "session.revertDock.expand": "Ընդլայնել վերադարձված հաղորդագրությունները", + "session.revertDock.restore": "Վերականգնել հաղորդագրությունը", + "session.new.title": "Կառուցել որեւէ բան", + "session.new.project.new": "Նոր նախագիծ", + "session.new.project.search": "Որոնել նախագծեր", + "session.new.project.add": "Ավելացնել նախագիծ", + "session.new.worktree.main": "Հիմնական մասնաճյուղ", + "session.new.worktree.mainWithBranch": "Հիմնական մասնաճյուղ ({{branch}})", + "session.new.worktree.create": "Ստեղծել նոր աշխատանքային ծառ", + "session.new.workspace.runIn": "Գործարկել նիստը", + "session.new.workspace.triggerLocal": "Տեղական", + "session.new.workspace.local": "Տեղական պահոց", + "session.new.workspace.existing": "Աշխատանքային տարածք…", + "session.new.git.none": "Առանց Git-ի", + "session.new.lastModified": "Վերջին փոփոխությունը", + "session.header.search.placeholder": "Որոնում {{project}}", + "session.header.searchFiles": "Որոնել ֆայլեր", + "session.header.openIn": "Բացել", + "session.header.open.action": "Բացել {{app}}", + "session.header.open.ariaLabel": "Բացել {{app}}", + "session.header.open.menu": "Բացել ընտրանքները", + "session.header.open.copyPath": "Պատճենել ուղին", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Ֆայլերի դիտարկիչ", + "session.header.open.fileManager": "Ֆայլերի կառավարիչ", + "session.header.reveal.finder": "Բացահայտեք Finder", + "session.header.reveal.fileExplorer": "Հայտնաբերել File Explorer-ում", + "session.header.reveal.containingFolder": "Բացել պարունակող թղթապանակ", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Տերմինալ", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Կարգավիճակ", + "status.popover.ariaLabel": "Սերվերի կոնֆիգուրացիաներ", + "status.popover.tab.servers": "Սերվերներ", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Ընդլայնումներ", + "status.popover.action.manageServers": "Կառավարել սերվերները", + "session.share.popover.title": "Հրապարակել համացանցում", + "session.share.popover.description.shared": + "Այս նիստը հրապարակային է համացանցում։ Այն հասանելի է բոլորին, ովքեր ունեն հղումը:", + "session.share.popover.description.unshared": + "Հանրայնորեն համօգտագործեք նիստը համացանցում։ Այն հասանելի կլինի բոլորին, ովքեր ունեն հղումը:", + "session.share.action.share": "Կիսվել", + "session.share.action.publish": "Հրապարակել", + "session.share.action.publishing": "Հրատարակում...", + "session.share.action.unpublish": "Չհրապարակել", + "session.share.action.unpublishing": "Չհրապարակվում է...", + "session.share.action.view": "Դիտել", + "session.share.copy.copied": "Պատճենված", + "session.share.copy.copyLink": "Պատճենել հղումը", + "lsp.tooltip.none": "Չկան LSP սերվեր", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Բեռնվում է հուշում...", + "terminal.loading": "Բեռնվում է տերմինալը...", + "terminal.title": "Տերմինալ", + "terminal.title.numbered": "Տերմինալ {{number}}", + "terminal.close": "Փակել տերմինալը", + "terminal.connectionLost.title": "Կապը կորցրած", + "terminal.connectionLost.abnormalClose": "WebSocket փակվել է աննորմալ՝ {{code}}", + "terminal.connectionLost.description": + "տերմինալի կապն ընդհատվել է։ Դա կարող է տեղի ունենալ, երբ սերվերը վերագործարկվի:", + "terminal.connectTicket.csrfError": + "PTY կապի տոմսը մերժված է ծագման կամ CSRF չեկերի կողմից։ Ստուգեք սերվերի CORS կազմաձևը:", + "terminal.connectTicket.statusError": "PTY միացման տոմսը ձախողվեց {{status}}", + "titlebar.update": "Թարմացնել", + "titlebar.updateVersion": "Թարմացնել {{version}}", + "common.closeTab": "Փակել ներդիրը", + "common.dismiss": "Փակել", + "common.moreCountSuffix": " (+{{count}} ավելին)", + "common.requestFailed": "Հարցումը ձախողվեց", + "common.moreOptions": "Լրացուցիչ ընտրանքներ", + "common.learnMore": "Իմանալ ավելին", + "common.rename": "Վերանվանել", + "common.export": "Արտահանում", + "common.reset": "Վերականգնել", + "common.archive": "Արխիվ", + "common.delete": "Ջնջել", + "common.close": "Փակել", + "common.edit": "Խմբագրել", + "common.loadMore": "Բեռնել ավելին", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "անհայտ", + "common.time.justNow": "Հենց հիմա", + "common.time.minutesAgo.short": "{{count}}ր առաջ", + "common.time.hoursAgo.short": "{{count}}ժ առաջ", + "common.time.daysAgo.short": "{{count}}օր առաջ", + "sidebar.menu.toggle": "Փոխարկել ընտրացանկ", + "sidebar.nav.projectsAndSessions": "Նախագծեր և նիստեր", + "sidebar.settings": "Կարգավորումներ", + "sidebar.help": "Օգնություն", + "sidebar.workspaces.enable": "Միացնել աշխատանքային տարածքները", + "sidebar.workspaces.disable": "Անջատել աշխատանքային տարածքները", + "sidebar.gettingStarted.title": "Սկսում", + "sidebar.gettingStarted.line1": "OpenCode ներառում է անվճար մոդելներ, որպեսզի կարողանաք անմիջապես սկսել:", + "sidebar.gettingStarted.line2": + "Միացրեք ցանկացած մատակարարի՝ մոդելներ օգտագործելու համար, inc. Claude, GPT, Gemini և այլն", + "sidebar.project.recentSessions": "Վերջին նիստերը", + "sidebar.project.viewAllSessions": "Դիտել բոլոր նիստերը", + "sidebar.project.clearNotifications": "Մաքրել ծանուցումները", + "sidebar.empty.title": "Ոչ մի նախագիծ բաց", + "sidebar.empty.description": "Բացեք նախագիծ՝ սկսելու համար", + "debugBar.ariaLabel": "Զարգացման կատարողականի ախտորոշում", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Վերջին ավարտված երթուղու անցումը հպվելով աշխատաշրջանի էջին, որը չափվում է երթուղիչի սկզբից մինչև այն նստելուց հետո առաջին ներկը:", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Վայրկյանում գլորվող շրջանակներ վերջին 5 վայրկյանում", + "debugBar.frame.label": "ԿԱԴՐ", + "debugBar.frame.tip": "Վերջին 5 վայրկյանի ընթացքում կադրերի վատթարագույն ժամանակը։", + "debugBar.jank.label": "ԸՆԴՀԱՏՈՒՄ", + "debugBar.jank.tip": "32 մվ-ից ավելի շրջանակներ վերջին 5 վայրկյանում", + "debugBar.long.label": "ԵՐԿԱՐ", + "debugBar.long.tip": + "Արգելափակված ժամանակի և երկար առաջադրանքների քանակը վերջին 5 վայրկյանում։ Առավելագույն առաջադրանքը՝ {{max}}։", + "debugBar.delay.label": "ՀԱՊԱՂՈՒՄ", + "debugBar.delay.tip": "Վերջին 5 վայրկյանում գրանցված մուտքի վատթարագույն ուշացումը:", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Փոխգործակցության մոտավոր տևողությունը վերջին 5 վայրկյանում։ Սա INPնման է, ոչ թե պաշտոնական Web Vitals INP:", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Հավելվածի ընթացիկ դասավորության փոփոխություն։", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Օգտագործված JS կույտ ընդդեմ կույտի սահմանաչափի։ Chromium միայն։", + "debugBar.mem.tip": "Օգտագործված JS կույտ ընդդեմ կույտի սահմանաչափի։ {{used}} {{limit}}-ից։", + "debugBar.focus.label": "ԿԵՆՏՐՈՆԱՑՈՒՄ", + "debugBar.focus.tip": "Ստիպել կենտրոնանալ ոճերը բոլոր ինտերակտիվ տարրերի վրա", + "debugBar.focus.on": "ՄԻԱՑ", + "debugBar.focus.off": "ԱՋԱՏ", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Ստիպել հավելվածի ամբողջական դասավորության ուղղությունը՝ առանց ընտրված լեզուն փոխելու", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Սեղանաշար", + "settings.section.server": "Սերվեր", + "settings.tab.general": "Ընդհանուր", + "settings.tab.shortcuts": "Դյուրանցումներ", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL ինտեգրում", + "settings.desktop.wsl.description": "Գործարկեք OpenCode սերվերը WSL-ի ներսում՝ Windows-ում։", + "settings.general.section.appearance": "Արտաքին տեսք", + "settings.general.section.advanced": "Ընդլայնված", + "settings.general.section.notifications": "Համակարգի ծանուցումներ", + "settings.general.section.updates": "Թարմացումներ", + "settings.general.section.sounds": "Ձայնային էֆեկտներ", + "settings.general.section.feed": "Fed", + "settings.general.section.display": "Ցուցադրել", + "settings.general.row.language.title": "Լեզու", + "settings.general.row.language.description": "Փոխել ցուցադրման լեզուն OpenCode", + "settings.general.row.shell.title": "Տերմինալի Shell", + "settings.general.row.shell.description": "Shell-ն օգտագործվում է տերմինալի և գործակալի գործիքների կողմից", + "settings.general.row.shell.autoDefault": "Ավտոմատ (կանխադրված)", + "settings.general.row.shell.terminalOnly": "միայն տերմինալ", + "settings.general.row.appearance.title": "Արտաքին տեսք", + "settings.general.row.appearance.description": "Անհատականացրեք, թե ինչպես է OpenCode տեսքը ձեր սարքում", + "settings.general.row.colorScheme.title": "Գունային սխեման", + "settings.general.row.colorScheme.description": "Ընտրեք՝ OpenCode-ը հետևում է համակարգին, թեթև թե մուգ թեմային", + "settings.general.row.theme.title": "Թեմա", + "settings.general.row.theme.description": "Անհատականացրեք, թե ինչպես է OpenCode-ի թեմատիկությունը։", + "settings.general.row.font.title": "Կոդի տառատեսակ", + "settings.general.row.font.description": "Անհատականացրեք կոդերի բլոկներում օգտագործվող տառատեսակը", + "settings.general.row.terminalFont.title": "Տերմինալ տառատեսակ", + "settings.general.row.terminalFont.description": "Անհատականացրեք տերմինալում օգտագործվող տառատեսակը", + "settings.general.row.uiFont.title": "UI տառատեսակը", + "settings.general.row.uiFont.description": "Անհատականացրեք ինտերֆեյսի ընթացքում օգտագործվող տառատեսակը", + "settings.general.row.followup.title": "Հետագա պահվածք", + "settings.general.row.followup.description": "Ընտրեք՝ հետևելու հուշումներն անմիջապես կուղղվեն, թե սպասեք հերթում", + "settings.general.row.followup.option.queue": "Հերթ", + "settings.general.row.followup.option.steer": "Ղեկավար", + "settings.general.row.showFileTree.title": "Ֆայլի ծառ", + "settings.general.row.showFileTree.description": "Ցույց տալ ֆայլի ծառի վահանակը նիստերում", + "settings.general.row.showNavigation.title": "Նավարկության կառավարում", + "settings.general.row.showNavigation.description": "Ցույց տալ հետ և առաջ կոճակները աշխատասեղանի վերնագրի տողում", + "settings.general.row.showSearch.title": "Հրամանների գունապնակ", + "settings.general.row.showSearch.description": "Ցույց տալ որոնման և հրամանների պալիտրա կոճակը վերնագրի տողում", + "settings.general.row.showTerminal.title": "Տերմինալ", + "settings.general.row.showTerminal.description": "Ցույց տալ տերմինալի կոճակը աշխատասեղանի վերնագրի տողում", + "settings.general.row.showStatus.title": "Սերվերի կարգավիճակ", + "settings.general.row.showStatus.description": "Ցույց տալ սերվերի կարգավիճակի կոճակը վերնագրի տողում", + "settings.general.row.mobileTitlebarBottom.title": "Նավարկություն ներքևում", + "settings.general.row.mobileTitlebarBottom.description": + "Տեղադրեք վերնագրի տողն ու նիստի ներդիրները էկրանի ներքևի մասում բջջային հեռախոսի վրա", + "settings.general.row.showCustomAgents.title": "Ցույց տալ գործակալին", + "settings.general.row.showCustomAgents.description": + "Փոխարկեք կոմպոզիտորի գործակալների միջև։ Երբ թաքնված է, կանխադրված է Build agent:", + "settings.general.row.reasoningSummaries.title": "Ցույց տալ հիմնավորման ամփոփագրերը", + "settings.general.row.reasoningSummaries.description": "Ցուցադրել մոդելի հիմնավորման ամփոփագրերը ժամանակացույցում", + "settings.general.row.shellToolPartsExpanded.title": "Ընդարձակել Shell գործիքի մասերը", + "settings.general.row.shellToolPartsExpanded.description": + "Ցուցադրել ժամանակացույցում լռելյայնորեն ընդլայնված shell գործիքի մասերը", + "settings.general.row.editToolPartsExpanded.title": "Ընդարձակել խմբագրման գործիքի մասերը", + "settings.general.row.editToolPartsExpanded.description": + "Ցուցադրել խմբագրման, գրելու և կարկատելու գործիքի մասերը, որոնք լռելյայն ընդլայնվել են ժամանակացույցում", + "settings.general.row.newInterface.title": "Նոր դասավորություն", + "settings.general.row.newInterface.badge": "Նոր", + "settings.general.row.newInterface.description": + "Օգտագործեք նոր ներդիրները և տան դասավորությունը։ Անցեք դասավորությունների միջև սահմանափակ ժամանակով:", + "settings.general.row.newInterfaceNotice.title": "Դուք այժմ օգտագործում եք նոր դասավորություն", + "settings.general.row.newInterfaceNotice.description": "Նախորդ դասավորությունն այլևս հասանելի չէ", + "settings.general.row.newInterfaceNotice.dismiss": "Փակել", + "settings.general.row.pinchZoom.title": "Սեղմեք՝ խոշորացնելու համար", + "settings.general.row.pinchZoom.description": "Թույլատրել մատնաչափի սեղմումը և Ctrl-scroll ժեստերը մեծացնելու համար", + "settings.general.row.wayland.title": "Օգտագործել բնիկ Wayland", + "settings.general.row.wayland.description": "Անջատել X11-ի հետադարձ կապը Wayland-ում: Պահանջվում է վերագործարկում:", + "settings.general.row.wayland.tooltip": + "Linux-ի վրա՝ թարմացման արագության խառը մոնիտորներով, բնիկ Wayland-ը կարող է ավելի կայուն լինել:", + "settings.general.row.releaseNotes.title": "Թողարկման նշումներ", + "settings.general.row.releaseNotes.description": "Ցույց տալ, թե ինչ է նոր պատուհանները թարմացումներից հետո", + "settings.updates.row.startup.title": "Ստուգեք գործարկման ժամանակ թարմացումների համար", + "settings.updates.row.startup.description": "Ավտոմատ կերպով ստուգեք թարմացումների համար, երբ OpenCode-ը գործարկվի", + "settings.updates.row.check.title": "Ստուգեք թարմացումների համար", + "settings.updates.row.check.description": "Ձեռքով ստուգեք թարմացումների համար և տեղադրեք, եթե առկա է", + "settings.updates.action.checkNow": "Ստուգեք հիմա", + "settings.updates.action.checking": "Ստուգում...", + "settings.updates.action.downloading": "Ներբեռնվում է...", + "settings.updates.action.installing": "Տեղադրվում է...", + "settings.updates.toast.latest.title": "Դուք արդի եք", + "settings.updates.toast.latest.description": "Դուք օգտագործում եք OpenCode-ի վերջին տարբերակը։", + "sound.option.none": "Ոչ մեկը", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "Գործակալ", + "settings.general.notifications.agent.description": + "Ցույց տալ համակարգի ծանուցումը, երբ գործակալն ավարտված է կամ ուշադրության կարիք ունի", + "settings.general.notifications.permissions.title": "Թույլտվություններ", + "settings.general.notifications.permissions.description": + "Ցույց տալ համակարգի ծանուցումը, երբ անհրաժեշտ է թույլտվություն", + "settings.general.notifications.errors.title": "Սխալներ", + "settings.general.notifications.errors.description": "Ցույց տալ համակարգի ծանուցումը, երբ սխալ է տեղի ունենում", + "settings.general.sounds.agent.title": "Գործակալ", + "settings.general.sounds.agent.description": "Նվագարկեք ձայնը, երբ գործակալն ավարտված է կամ ուշադրության կարիք ունի", + "settings.general.sounds.permissions.title": "Թույլտվություններ", + "settings.general.sounds.permissions.description": "Նվագարկել ձայնը, երբ անհրաժեշտ է թույլտվություն", + "settings.general.sounds.errors.title": "Սխալներ", + "settings.general.sounds.errors.description": "Նվագարկել ձայնը, երբ սխալ է տեղի ունենում", + "settings.shortcuts.title": "Ստեղնաշարի դյուրանցումներ", + "settings.shortcuts.reset.button": "Վերականգնել լռելյայն", + "settings.shortcuts.reset.toast.title": "Դյուրանցումների վերակայում", + "settings.shortcuts.reset.toast.description": "Ստեղնաշարի դյուրանցումները վերակայվել են կանխադրվածին։", + "settings.shortcuts.conflict.title": "Դյուրանցումն արդեն օգտագործվում է", + "settings.shortcuts.conflict.description": "{{keybind}}-ն արդեն նշանակված է {{titles}}-ին։", + "settings.shortcuts.unassigned": "Չնշանակված", + "settings.shortcuts.pressKeys": "Սեղմել ստեղները", + "settings.shortcuts.search.placeholder": "Որոնման դյուրանցումներ", + "settings.shortcuts.search.empty": "Դյուրանցումներ չեն գտնվել", + "settings.shortcuts.group.general": "Ընդհանուր", + "settings.shortcuts.group.session": "Նիստ", + "settings.shortcuts.group.navigation": "Նավարկություն", + "settings.shortcuts.group.modelAndAgent": "Մոդել և գործակալ", + "settings.shortcuts.group.terminal": "Տերմինալ", + "settings.shortcuts.group.prompt": "Հուշում", + "settings.providers.title": "Մատակարարներ", + "settings.providers.description": "Մատակարարի կարգավորումները կկարգավորվեն այստեղ։", + "settings.providers.section.connected": "Կապված մատակարարներ", + "settings.providers.connected.empty": "Կապված մատակարարներ չկան", + "settings.providers.connected.environmentDescription": "Միացված է ձեր միջավայրի փոփոխականներից", + "settings.providers.section.popular": "Հանրաճանաչ մատակարարներ", + "settings.providers.custom.description": "Ավելացնել OpenAI-համատեղելի մատակարար ըստ բազային URL։", + "settings.providers.tag.environment": "Շրջակա միջավայր", + "settings.providers.tag.config": "Կազմաձևում", + "settings.providers.tag.custom": "Պատվիրված", + "settings.providers.tag.other": "Այլ", + "settings.models.title": "Մոդելներ", + "settings.models.description": "Մոդելի կարգավորումները կարգավորելի կլինեն այստեղ։", + "settings.agents.title": "Գործակալներ", + "settings.agents.description": "Գործակալի կարգավորումները կկարգավորվեն այստեղ։", + "settings.commands.title": "Հրամաններ", + "settings.commands.description": "Հրամանի կարգավորումները կկարգավորվեն այստեղ։", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP կարգավորումները կկարգավորվեն այստեղ։", + "settings.permissions.title": "Թույլտվություններ", + "settings.permissions.description": "Վերահսկեք, թե ինչ գործիքներ կարող է օգտագործել սերվերը լռելյայն։", + "settings.permissions.section.tools": "Գործիքներ", + "settings.permissions.toast.updateFailed.title": "Չհաջողվեց թարմացնել թույլտվությունները", + "settings.permissions.action.allow": "Թույլատրել", + "settings.permissions.action.ask": "Հարցրեք", + "settings.permissions.action.deny": "Մերժել", + "settings.permissions.tool.read.title": "Կարդալ", + "settings.permissions.tool.read.description": "Կարդում է ֆայլը (համընկնում է ֆայլի ուղու հետ)", + "settings.permissions.tool.edit.title": "Խմբագրել", + "settings.permissions.tool.edit.description": "Փոփոխեք ֆայլերը, ներառյալ խմբագրումները, գրությունները և պատչերը", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Համապատասխանեցրեք ֆայլերը՝ օգտագործելով գլոբի նախշերը", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": + "Որոնել ֆայլի բովանդակությունը՝ օգտագործելով կանոնավոր արտահայտություններ", + "settings.permissions.tool.list.title": "Ցուցակ", + "settings.permissions.tool.list.description": "Ցուցակել ֆայլերը գրացուցակում", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Գործարկել Shell հրամանները", + "settings.permissions.tool.task.title": "Առաջադրանք", + "settings.permissions.tool.task.description": "Ենթագործակալների գործարկում", + "settings.permissions.tool.skill.title": "Հմտություն", + "settings.permissions.tool.skill.description": "Բեռնել հմտությունը ըստ անունով", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Գործարկել լեզվի սերվերի հարցումները", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Թարմացնել անելիքների ցուցակը", + "settings.permissions.tool.webfetch.title": "Վեբ առբերում", + "settings.permissions.tool.webfetch.description": "Առբերել բովանդակությունը URL-ից", + "settings.permissions.tool.websearch.title": "Վեբ որոնում", + "settings.permissions.tool.websearch.description": "Որոնել համացանցում", + "settings.permissions.tool.external_directory.title": "Արտաքին գրացուցակ", + "settings.permissions.tool.external_directory.description": "Մուտք գործեք ֆայլեր նախագծի գրացուցակից դուրս", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Գտեք գործիքի կրկնվող զանգերը՝ նույն մուտքագրմամբ", + "session.delete.failed.title": "Չհաջողվեց ջնջել նիստը", + "session.delete.title": "Ջնջել նիստը", + "session.delete.confirm": "Ջնջե՞լ «{{name}}» նիստը", + "session.delete.button": "Ջնջել նիստը", + "workspace.new": "Նոր աշխատանքային տարածք", + "workspace.type.local": "տեղական", + "workspace.type.sandbox": "Sandbox", + "workspace.create.failed.title": "Չհաջողվեց ստեղծել աշխատանքային տարածք", + "workspace.delete.failed.title": "Չհաջողվեց ջնջել աշխատանքային տարածքը", + "workspace.resetting.title": "Աշխատանքային տարածքի վերակայում", + "workspace.resetting.description": "Սա կարող է տևել մեկ րոպե։", + "workspace.reset.failed.title": "Չհաջողվեց վերականգնել աշխատանքային տարածքը", + "workspace.reset.success.title": "Աշխատանքային տարածքի վերակայում", + "workspace.reset.success.description": "Աշխատանքային տարածքն այժմ համապատասխանում է լռելյայն ճյուղին։", + "workspace.error.stillPreparing": "Աշխատանքային տարածքը դեռ պատրաստվում է", + "workspace.status.checking": "Չմիաձուլված փոփոխությունների ստուգում...", + "workspace.status.error": "Հնարավոր չէ ստուգել git կարգավիճակը։", + "workspace.status.clean": "Չմիաձուլված փոփոխություններ չեն հայտնաբերվել։", + "workspace.status.dirty": "Չմիաձուլված փոփոխություններ են հայտնաբերվել այս աշխատանքային տարածքում։", + "workspace.delete.title": "Ջնջել աշխատանքային տարածքը", + "workspace.delete.confirm": "Ջնջե՞լ «{{name}}» աշխատանքային տարածքը", + "workspace.delete.button": "Ջնջել աշխատանքային տարածքը", + "workspace.reset.title": "Վերականգնել աշխատանքային տարածքը", + "workspace.reset.confirm": "Վերականգնե՞լ «{{name}}» աշխատանքային տարածքը", + "workspace.reset.button": "Վերականգնել աշխատանքային տարածքը", + "workspace.reset.archived.none": "Ակտիվ նիստեր չեն արխիվացվի։", + "workspace.reset.archived.one": "1 նիստ կարխիվացվի։", + "workspace.reset.archived.many": "{{count}} նիստերը կարխիվացվեն։", + "workspace.reset.note": "Սա կվերակայի աշխատանքային տարածքը, որպեսզի համապատասխանի լռելյայն ճյուղին:", + "dialog.usageExceeded.dontShowAgain": "Այլևս չցուցադրել", +} diff --git a/packages/app/src/i18n/id.ts b/packages/app/src/i18n/id.ts new file mode 100644 index 0000000000000000000000000000000000000000..96dc3c7592e4e55d9c0ca25d61315baf50c0f847 --- /dev/null +++ b/packages/app/src/i18n/id.ts @@ -0,0 +1,1250 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Berkas", + "desktop.menu.edit": "Sunting", + "desktop.menu.view": "Tampilan", + "desktop.menu.go": "Navigasi", + "desktop.menu.window": "Jendela", + "desktop.menu.help": "Bantuan", + "desktop.menu.checkForUpdates": "Periksa pembaruan...", + "desktop.menu.settings": "Pengaturan", + "desktop.menu.reloadWebview": "Muat ulang tampilan web", + "desktop.menu.restart": "Mulai ulang", + "desktop.menu.exportLogs": "Ekspor log...", + "desktop.menu.newSession": "Sesi baru", + "desktop.menu.openProject": "Buka proyek...", + "desktop.menu.newWindow": "Jendela baru", + "desktop.menu.closeWindow": "Tutup jendela", + "desktop.menu.undo": "Urungkan", + "desktop.menu.redo": "Ulangi", + "desktop.menu.cut": "Potong", + "desktop.menu.copy": "Salin", + "desktop.menu.paste": "Tempel", + "desktop.menu.delete": "Hapus", + "desktop.menu.selectAll": "Pilih semua", + "desktop.menu.toggleSidebar": "Alihkan panel samping", + "desktop.menu.toggleTerminal": "Alihkan terminal", + "desktop.menu.toggleFileTree": "Alihkan pohon berkas", + "desktop.menu.reload": "Muat ulang", + "desktop.menu.toggleDeveloperTools": "Alihkan alat pengembang", + "desktop.menu.actualSize": "Ukuran sebenarnya", + "desktop.menu.zoomIn": "Perbesar", + "desktop.menu.zoomOut": "Perkecil", + "desktop.menu.toggleFullScreen": "Alihkan layar penuh", + "desktop.menu.back": "Kembali", + "desktop.menu.forward": "Maju", + "desktop.menu.previousSession": "Sesi sebelumnya", + "desktop.menu.nextSession": "Sesi berikutnya", + "desktop.menu.previousProject": "Proyek sebelumnya", + "desktop.menu.nextProject": "Proyek berikutnya", + "desktop.menu.minimize": "Minimalkan", + "desktop.menu.maximize": "Maksimalkan", + "desktop.menu.documentation": "Dokumentasi OpenCode", + "desktop.menu.supportForum": "Forum dukungan", + "desktop.menu.shareFeedback": "Bagikan umpan balik", + "desktop.menu.reportBug": "Laporkan bug", + "desktop.menu.ariaLabel": "Menu OpenCode", + + "desktop.updater.dialog.checkFailed.message": "Pemeriksaan pembaruan gagal.", + "desktop.updater.dialog.checkFailed.title": "Kesalahan pembaruan", + "desktop.updater.dialog.upToDate.message": "Versi Anda sudah terbaru.", + "desktop.updater.dialog.upToDate.title": "Tidak ada pembaruan", + "desktop.updater.dialog.ready.message": "Pembaruan {{version}} telah diunduh. Mulai ulang sekarang?", + "desktop.updater.dialog.ready.title": "Pembaruan siap", + "desktop.updater.dialog.restart": "Mulai ulang", + "desktop.updater.dialog.later": "Nanti", + + "desktop.recovery.action.relaunch": "Luncurkan ulang", + "desktop.recovery.action.exportLogs": "Ekspor log", + "desktop.recovery.action.keepWaiting": "Tetap tunggu", + "desktop.recovery.action.quit": "Keluar", + "desktop.recovery.loadFailed": "OpenCode gagal dimuat", + "desktop.recovery.terminated": "Jendela OpenCode berhenti secara tidak terduga", + "desktop.recovery.unresponsive": "OpenCode tidak merespons", + "desktop.recovery.unresponsive.detail": "Anda dapat meluncurkan ulang aplikasi, membuka log, atau tetap menunggu.", + "desktop.recovery.loadFailed.detail": "Jendela: {{window}}\nURL: {{url}}\nKesalahan: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Jendela: {{window}}\nAlasan: {{reason}}\nKode: {{code}}", + "desktop.recovery.unknown": "", + + "desktop.dialog.chooseFolder": "Pilih folder", + "desktop.dialog.chooseFile": "Pilih berkas", + "desktop.dialog.saveFile": "Simpan berkas", + "desktop.dialog.files": "Berkas", + + "desktop.server.local": "Server lokal", + + "desktop.wsl.error.windowsOnly": "WSL hanya tersedia di Windows", + "desktop.wsl.error.unavailable": "WSL tidak tersedia", + "desktop.wsl.error.listInstalled": "Gagal menampilkan daftar distro WSL yang terinstal", + "desktop.wsl.error.listOnline": "Gagal menampilkan daftar distro WSL daring", + "desktop.wsl.error.executeDistro": "Tidak dapat menjalankan perintah di distro", + "desktop.wsl.error.installWsl": "Instalasi WSL gagal", + "desktop.wsl.error.installDistro": "Gagal menginstal distro: {{distro}}", + "desktop.wsl.error.installOpencode": "Instalasi OpenCode gagal", + "desktop.wsl.error.alreadyAdded": "{{distro}} sudah ditambahkan", + "desktop.wsl.error.opencodeMissing": "opencode belum terinstal di distro ini", + "desktop.wsl.error.opencodeCannotRun": "opencode telah terinstal, tetapi tidak dapat dijalankan", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode belum terinstal di {{distro}}", + "desktop.wsl.error.updateVersion": + "Pembaruan OpenCode selesai, tetapi {{distro}} masih melaporkan {{installed}}; seharusnya {{expected}}", + "desktop.wsl.error.noVersion": "tanpa versi", + "desktop.wsl.error.serverExited": "Server WSL berhenti setelah dimulai (kode={{code}} sinyal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Server WSL berhenti sebelum siap (kode={{code}} sinyal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Pemeriksaan kesehatan sidecar untuk {{distro}} kehabisan waktu setelah {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} kehabisan waktu setelah {{timeout}}ms", + "desktop.wsl.error.failedPort": "Gagal mendapatkan porta", + + "desktop.picker.error.notSelected": "Berkas tidak dipilih melalui pemilih", + "desktop.picker.error.sizeLimit": "Lampiran yang dipilih melebihi batas {{limit}} MB", + + "command.category.suggested": "Disarankan", + "command.category.view": "Tampilan", + "command.category.project": "Proyek", + "command.category.provider": "Penyedia", + "command.category.server": "Server", + "command.category.session": "Sesi", + "command.category.theme": "Tema", + "command.category.language": "Bahasa", + "command.category.file": "Berkas", + "command.category.context": "Konteks", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agen", + "command.category.permissions": "Izin", + "command.category.workspace": "Ruang kerja", + "command.category.settings": "Pengaturan", + "command.logs.export": "Ekspor log", + + "theme.scheme.system": "Sistem", + "theme.scheme.light": "Terang", + "theme.scheme.dark": "Gelap", + + "command.sidebar.toggle": "Alihkan panel samping", + "command.project.open": "Buka proyek", + "command.project.previous": "Proyek sebelumnya", + "command.project.next": "Proyek berikutnya", + "command.project.index": "Beralih ke proyek {{index}}", + "command.provider.connect": "Hubungkan penyedia", + "command.server.switch": "Ganti server", + "command.settings.open": "Buka pengaturan", + "command.session.previous": "Sesi sebelumnya", + "command.session.next": "Sesi berikutnya", + "command.session.previous.unseen": "Sesi sebelumnya yang belum dibaca", + "command.session.next.unseen": "Sesi berikutnya yang belum dibaca", + "command.session.archive": "Arsipkan sesi", + + "command.palette": "Palet perintah", + + "command.theme.cycle": "Ganti tema", + "command.theme.set": "Gunakan tema: {{theme}}", + "command.theme.scheme.cycle": "Ganti skema warna", + "command.theme.scheme.set": "Gunakan skema warna: {{scheme}}", + + "command.language.cycle": "Ganti bahasa", + "command.language.set": "Gunakan bahasa: {{language}}", + + "command.session.new": "Sesi baru", + "command.file.open": "Buka berkas", + "command.tab.close": "Tutup tab", + "command.tab.reopenClosed": "Buka kembali tab yang ditutup", + "command.context.addSelection": "Tambahkan pilihan ke konteks", + "command.context.addSelection.description": "Tambahkan baris yang dipilih dari berkas saat ini", + "command.input.focus": "Fokuskan masukan", + "command.terminal.toggle": "Alihkan terminal", + "command.fileTree.toggle": "Alihkan pohon berkas", + "command.review.toggle": "Alihkan tinjauan", + "command.terminal.new": "Terminal baru", + "command.terminal.new.description": "Buat tab terminal baru", + "command.steps.toggle": "Alihkan langkah", + "command.steps.toggle.description": "Tampilkan atau sembunyikan langkah untuk pesan saat ini", + "command.message.previous": "Pesan sebelumnya", + "command.message.previous.description": "Pergi ke pesan pengguna sebelumnya", + "command.message.next": "Pesan berikutnya", + "command.message.next.description": "Pergi ke pesan pengguna berikutnya", + "command.model.choose": "Pilih model", + "command.model.choose.description": "Pilih model yang berbeda", + "command.mcp.toggle": "Alihkan MCP", + "command.mcp.toggle.description": "Alihkan MCP", + "command.agent.cycle": "Ganti agen", + "command.agent.cycle.description": "Beralih ke agen berikutnya", + "command.agent.cycle.reverse": "Ganti agen mundur", + "command.agent.cycle.reverse.description": "Beralih ke agen sebelumnya", + "command.model.variant.cycle": "Ganti usaha berpikir", + "command.model.variant.cycle.description": "Beralih ke tingkat usaha berikutnya", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Terima izin otomatis", + "command.permissions.autoaccept.disable": "Hentikan penerimaan izin otomatis", + "command.workspace.toggle": "Alihkan ruang kerja", + "command.workspace.toggle.description": "Aktifkan atau nonaktifkan beberapa ruang kerja di panel samping", + "command.session.undo": "Urungkan", + "command.session.undo.description": "Urungkan pesan terakhir", + "command.session.redo": "Ulangi", + "command.session.redo.description": "Ulangi pesan yang diurungkan", + "command.session.compact": "Ringkas sesi", + "command.session.compact.description": "Ringkas sesi untuk mengurangi ukuran konteks", + "command.session.fork": "Fork dari pesan", + "command.session.fork.description": "Buat sesi baru dari pesan sebelumnya", + "command.session.share": "Bagikan sesi", + "command.session.share.description": "Bagikan sesi ini dan salin URL ke papan klip", + "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", + "palette.group.commands": "Perintah", + "palette.group.files": "Berkas", + + "dialog.provider.search.placeholder": "Cari penyedia", + "dialog.provider.empty": "Penyedia tidak ditemukan", + "dialog.provider.group.popular": "Populer", + "dialog.provider.group.other": "Lainnya", + "dialog.provider.custom.label": "Penyedia kustom yang kompatibel dengan OpenAI", + "dialog.provider.tag.recommended": "Direkomendasikan", + "dialog.provider.opencode.note": "Model pilihan, termasuk Claude, GPT, Gemini, dan lainnya", + "dialog.provider.opencode.tagline": "Model andal yang dioptimalkan", + "dialog.provider.opencodeGo.tagline": "Langganan terjangkau untuk semua orang", + "dialog.provider.anthropic.note": "Akses langsung ke model Claude, termasuk Pro dan Max", + "dialog.provider.copilot.note": "Model AI untuk bantuan pengodean melalui GitHub Copilot", + "dialog.provider.openai.note": "Model GPT untuk tugas AI umum yang cepat dan mumpuni", + "dialog.provider.google.note": "Model Gemini untuk respons cepat dan terstruktur", + "dialog.provider.openrouter.note": "Akses semua model yang didukung dari satu penyedia", + "dialog.provider.vercel.note": "Akses terpadu ke model AI dengan perutean cerdas", + + "dialog.model.select.title": "Pilih model", + "dialog.model.search.placeholder": "Cari model", + "dialog.model.empty": "Tidak ada hasil model", + "dialog.model.manage": "Kelola model", + "dialog.model.manage.description": "Sesuaikan model mana yang muncul di pemilih model.", + "dialog.model.manage.provider.toggle": "Alihkan semua model {{provider}}", + + "dialog.model.unpaid.freeModels.title": "Model gratis disediakan oleh OpenCode", + "dialog.model.unpaid.addMore.title": "Tambahkan lebih banyak model dari penyedia populer", + "dialog.model.unpaid.viewMoreProviders": "Lihat 70+ penyedia lainnya", + + "dialog.provider.viewAll": "Tampilkan lebih banyak penyedia", + + "provider.connect.title": "Hubungkan {{provider}}", + "provider.connect.title.anthropicProMax": "Masuk dengan Claude Pro/Max", + "provider.connect.selectMethod": "Pilih metode masuk untuk {{provider}}.", + "provider.connect.method.apiKey": "Kunci API", + "provider.connect.method.browser": "Peramban", + "provider.connect.method.headless": "Headless", + "provider.connect.status.inProgress": "Otorisasi sedang berlangsung...", + "provider.connect.status.waiting": "Menunggu otorisasi...", + "provider.connect.status.failed": "Otorisasi gagal: {{error}}", + "provider.connect.apiKey.description": + "Masukkan kunci API {{provider}} untuk menghubungkan akun Anda dan gunakan model {{provider}} di OpenCode.", + "provider.connect.apiKey.label": "Kunci API {{provider}}", + "provider.connect.apiKey.placeholder": "Kunci API", + "provider.connect.apiKey.required": "Kunci API diperlukan", + "provider.connect.opencodeZen.line1": + "OpenCode Zen memberi Anda akses ke serangkaian model andal yang dioptimalkan untuk agen pengodean.", + "provider.connect.opencodeZen.line2": + "Dengan satu kunci API, Anda akan mendapatkan akses ke model seperti Claude, GPT, Gemini, GLM dan lainnya.", + "provider.connect.opencodeZen.visit.prefix": "Kunjungi ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " untuk mendapatkan kunci API Anda.", + "provider.connect.oauth.code.visit.prefix": "Kunjungi ", + "provider.connect.oauth.code.visit.link": "tautan ini", + "provider.connect.oauth.code.visit.suffix": + " untuk mendapatkan kode otorisasi guna menghubungkan akun Anda dan menggunakan model {{provider}} di OpenCode.", + "provider.connect.oauth.code.label": "Kode otorisasi {{method}}", + "provider.connect.oauth.code.placeholder": "Kode otorisasi", + "provider.connect.oauth.code.required": "Kode otorisasi diperlukan", + "provider.connect.oauth.code.invalid": "Kode otorisasi tidak valid", + "provider.connect.oauth.auto.visit.prefix": "Kunjungi ", + "provider.connect.oauth.auto.visit.link": "tautan ini", + "provider.connect.oauth.auto.visit.suffix": + " dan masukkan kode di bawah untuk menghubungkan akun dan menggunakan model {{provider}} di OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Kode konfirmasi", + "provider.connect.toast.connected.title": "{{provider}} terhubung", + "provider.connect.toast.connected.description": "Model {{provider}} sekarang tersedia untuk digunakan.", + + "provider.custom.title": "Penyedia kustom", + "provider.custom.unavailable": "Penyedia kustom tidak tersedia di server ini", + "provider.custom.description.prefix": "Konfigurasikan penyedia yang kompatibel dengan OpenAI. Lihat ", + "provider.custom.description.link": "dokumen konfigurasi penyedia", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID Penyedia", + "provider.custom.field.providerID.placeholder": "penyediaku", + "provider.custom.field.providerID.description": "Huruf kecil, angka, tanda hubung, atau garis bawah", + "provider.custom.field.name.label": "Nama tampilan", + "provider.custom.field.name.placeholder": "Penyedia AI Saya", + "provider.custom.field.baseURL.label": "URL Dasar", + "provider.custom.field.baseURL.placeholder": "https://api.penyediaku.com/v1", + "provider.custom.field.apiKey.label": "Kunci API", + "provider.custom.field.apiKey.placeholder": "Kunci API", + "provider.custom.field.apiKey.description": "Opsional. Kosongkan jika Anda mengelola autentikasi melalui tajuk.", + "provider.custom.models.label": "Model", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Nama", + "provider.custom.models.name.placeholder": "Nama Tampilan", + "provider.custom.models.remove": "Hapus model", + "provider.custom.models.add": "Tambah model", + "provider.custom.headers.label": "Tajuk (opsional)", + "provider.custom.headers.key.label": "Tajuk", + "provider.custom.headers.key.placeholder": "Nama-Tajuk", + "provider.custom.headers.value.label": "Nilai", + "provider.custom.headers.value.placeholder": "nilai", + "provider.custom.headers.remove": "Hapus tajuk", + "provider.custom.headers.add": "Tambah tajuk", + "provider.custom.error.providerID.required": "ID Penyedia diperlukan", + "provider.custom.error.providerID.format": "Gunakan huruf kecil, angka, tanda hubung, atau garis bawah", + "provider.custom.error.providerID.exists": "ID penyedia tersebut sudah ada", + "provider.custom.error.name.required": "Nama tampilan diperlukan", + "provider.custom.error.baseURL.required": "URL Dasar diperlukan", + "provider.custom.error.baseURL.format": "Harus dimulai dengan http:// atau https://", + "provider.custom.error.required": "Diperlukan", + "provider.custom.error.duplicate": "Duplikat", + + "provider.disconnect.toast.disconnected.title": "{{provider}} terputus", + "provider.disconnect.toast.disconnected.description": "Model {{provider}} tidak lagi tersedia.", + + "model.tag.free": "Gratis", + "model.tag.latest": "Terbaru", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "teks", + "model.input.image": "gambar", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Mengizinkan: {{inputs}}", + "model.tooltip.context.label": "Konteks", + "model.tooltip.inputs": "Masukan", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Penyedia", + "model.tooltip.reasoning": "Penalaran", + "model.tooltip.reasoning.allowed": "Mengizinkan penalaran", + "model.tooltip.reasoning.none": "Tanpa penalaran", + "model.tooltip.context": "Batas konteks {{limit}}", + + "common.search.placeholder": "Cari", + "common.goBack": "Navigasi mundur", + "common.goForward": "Navigasi maju", + "common.loading": "Memuat", + "common.loading.ellipsis": "...", + "common.cancel": "Batal", + "common.clear": "Bersihkan", + "common.open": "Buka", + "common.connect": "Hubungkan", + "common.disconnect": "Putuskan", + "common.continue": "Lanjutkan", + "common.submit": "Kirim", + "common.save": "Simpan", + "common.saving": "Menyimpan...", + "common.default": "Bawaan", + "common.attachment": "lampiran", + + "prompt.placeholder.shell": "Masukkan perintah shell... {{example}}", + "prompt.placeholder.normal": 'Tanyakan apa saja... "{{example}}"', + "prompt.placeholder.simple": "Tanyakan apa saja...", + "prompt.placeholder.summarizeComments": "Ringkas komentar…", + "prompt.placeholder.summarizeComment": "Ringkas komentar…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "esc untuk keluar", + "session.child.promptDisabled": "Sesi subagen tidak dapat diberi prompt.", + "session.child.backToParent": "Kembali ke sesi utama.", + + "prompt.example.1": "Perbaiki TODO di basis kode", + "prompt.example.2": "Apa tumpukan teknologi proyek ini?", + "prompt.example.3": "Perbaiki tes yang rusak", + "prompt.example.4": "Jelaskan cara kerja autentikasi", + "prompt.example.5": "Temukan dan perbaiki kerentanan keamanan", + "prompt.example.6": "Tambahkan pengujian unit untuk layanan pengguna", + "prompt.example.7": "Refaktor fungsi ini agar lebih mudah dibaca", + "prompt.example.8": "Apa arti kesalahan ini?", + "prompt.example.9": "Bantu saya mengawakutu masalah ini", + "prompt.example.10": "Hasilkan dokumentasi API", + "prompt.example.11": "Optimalkan kueri basis data", + "prompt.example.12": "Tambahkan validasi masukan", + "prompt.example.13": "Buat komponen baru untuk...", + "prompt.example.14": "Bagaimana cara menerapkan proyek ini?", + "prompt.example.15": "Tinjau kode saya untuk praktik terbaik", + "prompt.example.16": "Tambahkan penanganan kesalahan ke fungsi ini", + "prompt.example.17": "Jelaskan pola regex ini", + "prompt.example.18": "Konversi ini ke TypeScript", + "prompt.example.19": "Tambahkan pencatatan log di seluruh basis kode", + "prompt.example.20": "Dependensi mana yang sudah kedaluwarsa?", + "prompt.example.21": "Bantu saya menulis skrip migrasi", + "prompt.example.22": "Terapkan penembolokan untuk titik akhir ini", + "prompt.example.23": "Tambahkan paginasi ke daftar ini", + "prompt.example.24": "Buat perintah CLI untuk...", + "prompt.example.25": "Bagaimana cara kerja variabel lingkungan di sini?", + + "prompt.popover.emptyResults": "Tidak ada hasil yang cocok", + "prompt.popover.emptyCommands": "Tidak ada perintah yang cocok", + "prompt.dropzone.label": "Letakkan gambar, PDF, atau berkas teks di sini", + "prompt.dropzone.file.label": "Letakkan untuk menyebut berkas dengan @", + "prompt.slash.badge.custom": "kustom", + "prompt.slash.badge.skill": "keahlian", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktif", + "prompt.context.includeActiveFile": "Sertakan berkas aktif", + "prompt.context.removeActiveFile": "Hapus berkas aktif dari konteks", + "prompt.context.removeFile": "Hapus berkas dari konteks", + "prompt.action.attachFile": "Tambah berkas", + "prompt.menu.addImagesAndFiles": "Tambah berkas dan lainnya", + "prompt.menu.imagesAndFiles": "Gambar dan berkas", + "prompt.menu.commands": "Perintah", + "prompt.menu.context": "Konteks", + "prompt.menu.shellCommand": "Perintah shell", + "prompt.attachment.remove": "Hapus lampiran", + "prompt.action.send": "Kirim", + "prompt.action.stop": "Hentikan", + + "prompt.toast.pasteUnsupported.title": "Lampiran tidak didukung", + "prompt.toast.pasteUnsupported.description": "Hanya gambar, PDF, atau berkas teks yang dapat dilampirkan di sini.", + "prompt.toast.attachmentDuplicate.title": "Berkas ini sudah diunggah", + "prompt.toast.modelAgentRequired.title": "Pilih agen dan model", + "prompt.toast.modelAgentRequired.description": "Pilih agen dan model sebelum mengirim prompt.", + "prompt.toast.worktreeCreateFailed.title": "Gagal membuat worktree", + "prompt.toast.sessionCreateFailed.title": "Gagal membuat sesi", + "prompt.toast.shellSendFailed.title": "Gagal mengirim perintah shell", + "prompt.toast.commandSendFailed.title": "Gagal mengirim perintah", + "prompt.toast.promptSendFailed.title": "Gagal mengirim prompt", + "prompt.toast.promptSendFailed.description": "Tidak dapat mengambil sesi", + + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{enabled}} dari {{total}} diaktifkan", + "dialog.mcp.empty": "Tidak ada MCP yang dikonfigurasi", + + "dialog.lsp.empty": "LSP terdeteksi otomatis dari jenis berkas", + "dialog.plugins.empty": "Plugin dikonfigurasi di opencode.json", + + "mcp.status.connected": "terhubung", + "mcp.status.failed": "gagal", + "mcp.status.needs_auth": "perlu autentikasi", + "mcp.status.disabled": "nonaktif", + "mcp.auth.clickToAuthenticate": "Klik untuk mengautentikasi", + + "dialog.fork.empty": "Tidak ada pesan yang dapat dicabangkan", + + "dialog.directory.search.placeholder": "Cari folder", + "dialog.directory.empty": "Folder tidak ditemukan", + "dialog.directory.action.selectFile": "Pilih berkas", + "dialog.directory.action.selectFolder": "Pilih folder", + "dialog.directory.root": "Akar", + "dialog.directory.parent": "Induk", + "dialog.directory.readError": "Tidak dapat membaca folder ini", + + "app.server.unreachable": "Tidak dapat menjangkau {{server}}", + "app.server.retrying": "Mencoba ulang secara otomatis...", + "app.server.otherServers": "Server lainnya", + + "dialog.server.title": "Server", + "dialog.server.description": "Ganti server OpenCode mana yang terhubung dengan aplikasi ini.", + "dialog.server.search.placeholder": "Cari server", + "dialog.server.empty": "Belum ada server", + "dialog.server.add.title": "Tambah server", + "dialog.server.add.url": "Alamat server", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Tidak dapat terhubung ke server", + "dialog.server.add.checking": "Memeriksa...", + "dialog.server.add.button": "Tambah server", + "dialog.server.add.name": "Nama server (opsional)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Nama pengguna (opsional)", + "dialog.server.add.usernamePlaceholder": "nama pengguna", + "dialog.server.add.password": "Kata sandi (opsional)", + "dialog.server.add.passwordPlaceholder": "kata sandi", + "dialog.server.edit.title": "Sunting server", + "dialog.server.default.title": "Server bawaan", + "dialog.server.default.description": + "Hubungkan ke server ini saat aplikasi diluncurkan, alih-alih memulai server lokal. Memerlukan mulai ulang.", + "dialog.server.default.none": "Tidak ada server dipilih", + "dialog.server.default.set": "Tetapkan server saat ini sebagai bawaan", + "dialog.server.default.clear": "Hapus", + "dialog.server.action.remove": "Hapus server", + + "dialog.server.menu.edit": "Sunting", + "dialog.server.menu.default": "Tetapkan sebagai bawaan", + "dialog.server.menu.defaultRemove": "Hapus bawaan", + "dialog.server.menu.delete": "Hapus", + "dialog.server.current": "Server Saat Ini", + "dialog.server.status.default": "Bawaan", + "wsl.server.add": "Tambah server WSL", + "wsl.server.addShort": "Tambah WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "Server WSL", + "wsl.server.retryStart": "Coba mulai ulang", + "wsl.server.updating": "Memperbarui...", + "wsl.onboarding.step.distro": "Pilih distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Memeriksa WSL...", + "wsl.onboarding.restartRequired": "Windows perlu dimulai ulang untuk menyelesaikan instalasi WSL.", + "wsl.onboarding.ready": "WSL siap.", + "wsl.onboarding.required": "WSL diperlukan untuk melanjutkan.", + "wsl.onboarding.checkingDistros": "Memeriksa distro...", + "wsl.onboarding.installingDistro": "Menginstal {{distro}}...", + "wsl.onboarding.checkingDistro": "Memeriksa {{distro}}...", + "wsl.onboarding.listingDistros": "Memuat daftar distro...", + "wsl.onboarding.distroReady": "{{distro}} siap.", + "wsl.onboarding.distroNotInstalled": "{{distro}} belum terinstal.", + "wsl.onboarding.openDistroOnce": "Buka {{distro}} sekali untuk menyelesaikan pengaturan.", + "wsl.onboarding.finishingDistro": "Menyelesaikan pengaturan untuk {{distro}}.", + "wsl.onboarding.pickDistro": "Pilih distro atau instal salah satu di bawah.", + "wsl.onboarding.checkingOpencode": "Memeriksa OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Memeriksa OpenCode di {{distro}}...", + "wsl.onboarding.updatingOpencode": "Memperbarui OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Memperbarui OpenCode di {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Perbarui OpenCode di {{distro}}.", + "wsl.onboarding.updateOpencode": "Perbarui OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode siap di {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode siap.", + "wsl.onboarding.installOpencodeIn": "Instal OpenCode di {{distro}}.", + "wsl.onboarding.installOpencode": "Instal OpenCode", + "wsl.onboarding.chooseDistroFirst": "Pilih distro terlebih dahulu.", + "wsl.onboarding.loadFailed": "Gagal memuat keadaan WSL.", + "wsl.onboarding.loading": "Memuat...", + "wsl.onboarding.installedDistros": "Distro terinstal", + "wsl.onboarding.checkAgain": "Periksa lagi", + "wsl.onboarding.distroStatus.ready": "Siap", + "wsl.onboarding.distroStatus.checking": "Memeriksa...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode belum terinstal", + "wsl.onboarding.distroStatus.missingTools": "bash dan curl tidak tersedia", + "wsl.onboarding.distroStatus.unsupported": "Tidak didukung · Gunakan WSL 2", + "wsl.onboarding.needAnotherDistro": "Perlu distro lain?", + "wsl.onboarding.needAnotherDistroHint": "Instal distribusi Linux dari katalog WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL belum terinstal", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) diperlukan sebelum OpenCode dapat menambahkan server WSL", + "wsl.onboarding.wslUnavailable.title": "WSL tidak tersedia", + "wsl.onboarding.wslUnavailable.description": "OpenCode tidak dapat memverifikasi WSL di komputer ini.", + "wsl.onboarding.installWsl": "Instal WSL", + "wsl.onboarding.windowsRestartRequired": + "Mulai ulang Windows untuk menyelesaikan instalasi WSL, lalu buka kembali OpenCode.", + "wsl.onboarding.next": "Berikutnya", + "wsl.onboarding.refresh": "Muat ulang", + "wsl.onboarding.allDistrosAdded": "Semua distro yang terinstal sudah ditambahkan.", + "wsl.onboarding.noDistros": "Belum ada distro terdeteksi.", + "wsl.onboarding.install": "Instal", + "wsl.onboarding.installing": "Menginstal...", + "wsl.onboarding.installDistro": "Instal distro", + "wsl.onboarding.searchDistros": "Cari distro", + "wsl.onboarding.wsl2Required": "WSL 2 diperlukan.", + "wsl.onboarding.toolsRequired": "Distro ini memerlukan bash dan curl.", + "wsl.onboarding.openTerminal": "Buka terminal", + "wsl.onboarding.path": "Jalur: {{path}}", + "wsl.onboarding.notFound": "tidak ditemukan", + "wsl.onboarding.version": "Versi: {{version}}", + "wsl.onboarding.unknown": "tidak diketahui", + "wsl.onboarding.desktopVersion": "desktop {{version}}", + "wsl.onboarding.versionMismatch": "Versi terinstal tidak cocok dengan versi aplikasi desktop.", + "wsl.onboarding.adding": "Menambahkan...", + + "help.tabs.toast.ariaLabel": "Memperkenalkan Tab. Atur pekerjaan dan sesi aktif Anda dengan tab", + "help.tabs.toast.dismiss": "Tutup informasi tentang Tab", + "help.tabs.title": "Memperkenalkan Tab", + "help.tabs.description": "Atur pekerjaan dan sesi aktif Anda dengan tab", + "help.tabs.date": "14 Juli", + "help.tabs.introduction": "OpenCode Desktop kini dirancang dengan tab sebagai pusatnya.", + "help.tabs.sessions": + "Mulai sesi baru di tab, atau buka sesi yang sudah ada dari proyek Anda. Buka tab baru saat Anda memulai sesuatu yang baru, lalu tutup setelah selesai.", + "help.tabs.organize": + "Membiarkan beberapa tab tetap terbuka memudahkan Anda mengatur sesi aktif. Ganti nama tab agar mudah diingat jika Anda berencana mempertahankannya.", + "help.tabs.home": "Semua sesi dan proyek Anda tersedia di layar Beranda baru. Memilih sesi akan membukanya di tab.", + "help.tabs.persistence": "Saat Anda membuka kembali aplikasi, tab Anda tetap terbuka.", + "help.tabs.worktrees": + "Desain baru belum mendukung Git Worktrees, tetapi dukungan tersebut akan segera hadir. Jika Anda ingin tetap menggunakan tata letak sebelumnya, Anda dapat beralih antartata letak di Pengaturan. Perlu diingat bahwa tata letak baru akan menjadi permanen dalam beberapa minggu.", + "server.row.noUsername": "tanpa nama pengguna", + + "dialog.project.edit.title": "Sunting proyek", + "dialog.project.edit.name": "Nama", + "dialog.project.edit.icon": "Ikon", + "dialog.project.edit.icon.alt": "Ikon proyek", + "dialog.project.edit.icon.hint": "Klik atau seret gambar", + "dialog.project.edit.icon.recommended": "Disarankan: 128x128px", + "dialog.project.edit.color": "Warna", + "dialog.project.edit.color.select": "Pilih warna {{color}}", + "dialog.project.edit.worktree.startup": "Skrip awal ruang kerja", + "dialog.project.edit.worktree.startup.description": "Berjalan setelah membuat ruang kerja (worktree) baru.", + "dialog.project.edit.worktree.startup.placeholder": "mis. bun install", + + "dialog.releaseNotes.action.getStarted": "Mulai", + "dialog.releaseNotes.action.next": "Berikutnya", + "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", + "context.breakdown.user": "Pengguna", + "context.breakdown.assistant": "Asisten", + "context.breakdown.tool": "Panggilan Alat", + "context.breakdown.other": "Lainnya", + + "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", + "context.stats.model": "Model", + "context.stats.limit": "Batas Konteks", + "context.stats.totalTokens": "Total Token", + "context.stats.usage": "Penggunaan", + "context.stats.inputTokens": "Token Masukan", + "context.stats.outputTokens": "Token Keluaran", + "context.stats.reasoningTokens": "Token Penalaran", + "context.stats.cacheTokens": "Token Tembolok (baca/tulis)", + "context.stats.userMessages": "Pesan Pengguna", + "context.stats.assistantMessages": "Pesan Asisten", + "context.stats.totalCost": "Total Biaya", + "context.stats.sessionCreated": "Sesi Dibuat", + "context.stats.lastActivity": "Aktivitas Terakhir", + + "context.usage.tokens": "Token", + "context.usage.usage": "Penggunaan", + "context.usage.cost": "Biaya", + "context.usage.clickToView": "Klik untuk melihat konteks", + "context.usage.view": "Lihat penggunaan konteks", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Bahasa", + "toast.language.description": "Beralih ke {{language}}", + + "toast.theme.title": "Tema diganti", + "toast.scheme.title": "Skema warna", + + "toast.workspace.enabled.title": "Ruang kerja diaktifkan", + "toast.workspace.enabled.description": "Beberapa worktree sekarang ditampilkan di panel samping", + "toast.workspace.disabled.title": "Ruang kerja dinonaktifkan", + "toast.workspace.disabled.description": "Hanya worktree utama yang ditampilkan di panel samping", + + "toast.permissions.autoaccept.on.title": "Menerima izin secara otomatis", + "toast.permissions.autoaccept.on.description": "Permintaan izin akan disetujui secara otomatis", + "toast.permissions.autoaccept.off.title": "Berhenti menerima izin otomatis", + "toast.permissions.autoaccept.off.description": "Permintaan izin akan memerlukan persetujuan", + + "toast.model.none.title": "Tidak ada model dipilih", + "toast.model.none.description": "Hubungkan penyedia untuk meringkas sesi ini", + + "toast.file.loadFailed.title": "Gagal memuat berkas", + "toast.file.listFailed.title": "Gagal mendaftar berkas", + + "toast.context.noLineSelection.title": "Tidak ada pilihan baris", + "toast.context.noLineSelection.description": "Pilih rentang baris di tab berkas terlebih dahulu.", + + "toast.session.share.copyFailed.title": "Gagal menyalin URL ke papan klip", + "toast.session.share.success.title": "Sesi dibagikan", + "toast.session.share.success.description": "URL berbagi disalin ke papan klip!", + "toast.session.share.failed.title": "Gagal membagikan sesi", + "toast.session.share.failed.description": "Terjadi kesalahan saat membagikan sesi", + + "toast.session.unshare.success.title": "Berbagi sesi dihentikan", + "toast.session.unshare.success.description": "Berbagi sesi berhasil dihentikan!", + "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}}", + + "toast.update.title": "Pembaruan tersedia", + "toast.update.description": "Versi baru OpenCode ({{version}}) sekarang tersedia untuk diinstal.", + "toast.update.action.installRestart": "Instal dan mulai ulang", + "toast.update.action.notYet": "Nanti saja", + + "error.page.title": "Terjadi kesalahan", + "error.page.description": "Terjadi kesalahan saat memuat aplikasi.", + "error.page.description.localServerStartup": "Terjadi kesalahan saat memulai server lokal.", + "error.page.details.label": "Detail Kesalahan", + "error.page.action.restart": "Mulai Ulang", + "error.page.action.report": "Laporkan Kesalahan", + "error.page.action.reported": "Kesalahan Dilaporkan", + "error.page.action.exportLogs": "Ekspor Log", + "error.page.action.checking": "Memeriksa...", + "error.page.action.checkUpdates": "Periksa pembaruan", + "error.page.action.updateTo": "Perbarui ke {{version}}", + "error.page.circular": "[Sirkular]", + "error.page.report.prefix": "Harap laporkan kesalahan ini ke tim OpenCode", + "error.page.report.discord": "di Discord", + "error.page.version": "Versi: {{version}}", + + "error.dev.rootNotFound": + "Elemen root tidak ditemukan. Apakah Anda lupa menambahkannya ke index.html? Atau mungkin atribut id salah eja?", + + "error.serverSync.connectFailed": "Tidak dapat terhubung ke server. Apakah ada server yang berjalan di `{{url}}`?", + "error.serverSDK.noServerAvailable": "Tidak ada server tersedia", + "error.serverSDK.serverNotAvailable": "Server tidak tersedia", + "error.childStore.persistedCacheCreateFailed": "Gagal membuat tembolok persisten", + "error.childStore.persistedProjectMetadataCreateFailed": "Gagal membuat metadata proyek persisten", + "error.childStore.persistedProjectIconCreateFailed": "Gagal membuat ikon proyek persisten", + "error.childStore.storeCreateFailed": "Gagal membuat penyimpanan", + "directory.error.invalidUrl": "Direktori tidak valid di URL.", + + "error.chain.unknown": "Kesalahan tidak diketahui", + "error.server.invalidConfiguration": "Konfigurasi tidak valid", + "error.chain.causedBy": "Disebabkan oleh:", + "error.chain.apiError": "Kesalahan API", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Dapat dicoba ulang: {{retryable}}", + "error.chain.responseBody": "Isi respons:\n{{body}}", + "error.chain.didYouMean": "Maksud Anda: {{suggestions}}", + "error.chain.modelNotFound": "Model tidak ditemukan: {{provider}}/{{model}}", + "error.chain.checkConfig": "Periksa nama penyedia/model di konfigurasi Anda (opencode.json)", + "error.chain.mcpFailed": 'Server MCP "{{name}}" gagal. Catatan: OpenCode belum mendukung autentikasi MCP.', + "error.chain.providerAuthFailed": "Autentikasi penyedia gagal ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Gagal menginisialisasi penyedia "{{provider}}". Periksa kredensial dan konfigurasi.', + "error.chain.configJsonInvalid": "Berkas konfigurasi di {{path}} bukan JSON(C) yang valid", + "error.chain.configJsonInvalidWithMessage": "Berkas konfigurasi di {{path}} bukan JSON(C) yang valid: {{message}}", + "error.chain.configDirectoryTypo": + 'Direktori "{{dir}}" di {{path}} tidak valid. Ganti nama direktori menjadi "{{suggestion}}" atau hapus. Ini adalah kesalahan ketik yang umum.', + "error.chain.configFrontmatterError": "Gagal mengurai frontmatter di {{path}}:\n{{message}}", + "error.chain.configInvalid": "Berkas konfigurasi di {{path}} tidak valid", + "error.chain.configInvalidWithMessage": "Berkas konfigurasi di {{path}} tidak valid: {{message}}", + + "notification.permission.title": "Izin diperlukan", + "notification.permission.description": "{{sessionTitle}} di {{projectName}} memerlukan izin", + "notification.question.title": "Pertanyaan", + "notification.question.description": "{{sessionTitle}} di {{projectName}} memiliki pertanyaan", + "notification.action.goToSession": "Pergi ke sesi", + + "notification.session.responseReady.title": "Respons siap", + "notification.session.error.title": "Kesalahan sesi", + "notification.session.error.fallbackDescription": "Terjadi kesalahan", + + "home.recentProjects": "Proyek terbaru", + "home.empty.title": "Tidak ada proyek terbaru", + "home.empty.description": "Mulai dengan membuka proyek lokal", + "home.title": "Beranda", + "home.projects": "Proyek", + "home.project.add": "Tambah proyek", + "home.recentlyClosed": "Baru saja ditutup", + "home.server.collapse": "Ciutkan proyek server", + "home.server.expand": "Bentangkan proyek server", + "home.sessions.search.placeholder": "Cari sesi", + "home.sessions.search.placeholder.scoped": "Cari sesi di {{scope}}", + "home.sessions.search.sessions": "Sesi", + "home.sessions.search.noResults": "Tidak ada sesi ditemukan untuk {{query}}", + "home.sessions.empty": "Belum ada apa pun di sini", + "home.sessions.empty.description": "Buat sesi untuk memulai", + "home.sessions.group.today": "Hari ini", + "home.sessions.group.yesterday": "Kemarin", + "home.sessions.group.older": "Lebih lama", + "home.providerTip": + "Hubungkan ke 75+ penyedia untuk menggunakan model lain, termasuk Claude, GPT, Gemini, dan lainnya", + + "session.tab.session": "Sesi", + "session.tab.review": "Tinjauan", + "session.tab.context": "Konteks", + "session.tab.unknown": "Sesi Tidak Dikenal", + "session.panel.reviewAndFiles": "Tinjauan dan berkas", + "session.error.notFound": "Sesi ini tidak dapat ditemukan", + "session.error.notFound.description": "Tab ini mengarah ke sesi yang sudah tidak ada di server ini.", + "session.error.notFound.closeTab": "Tutup Tab", + "session.error.serverConnection": "Tidak dapat terhubung ke server ini", + "session.review.filesChanged": "{{count}} Berkas Berubah", + "session.review.change.one": "Perubahan", + "session.review.change.other": "Perubahan", + "session.review.loadingChanges": "Memuat perubahan...", + "session.review.empty": "Belum ada perubahan di sesi ini", + "session.review.noVcs": "Tidak ada sistem kontrol versi Git yang terdeteksi, perubahan tidak ditampilkan", + "session.review.noVcs.createGit.title": "Buat repositori Git", + "session.review.noVcs.createGit.description": "Lacak, tinjau, dan urungkan perubahan di proyek ini", + "session.review.noVcs.createGit.actionLoading": "Membuat repositori Git...", + "session.review.noVcs.createGit.action": "Buat repositori Git", + "session.review.noSnapshot": + "Pelacakan snapshot dinonaktifkan di konfigurasi, sehingga perubahan sesi tidak tersedia", + "session.review.noChanges": "Tidak ada perubahan", + "session.review.noUncommittedChanges": "Belum ada perubahan yang belum dikomit", + "session.review.noBranchChanges": "Belum ada perubahan cabang", + + "session.files.selectToOpen": "Pilih berkas untuk dibuka", + "session.files.all": "Semua berkas", + "session.files.empty": "Tidak ada berkas", + "session.files.binaryContent": "Berkas biner (konten tidak dapat ditampilkan)", + + "session.messages.renderEarlier": "Tampilkan pesan sebelumnya", + "session.messages.loadingEarlier": "Memuat pesan sebelumnya...", + "session.messages.loadEarlier": "Muat pesan sebelumnya", + "session.messages.loading": "Memuat pesan...", + "session.messages.jumpToLatest": "Lompat ke pesan terbaru", + + "session.context.addToContext": "Tambahkan {{selection}} ke konteks", + "session.todo.title": "Tugas", + "session.todo.collapse": "Ciutkan", + "session.todo.expand": "Bentangkan", + "session.todo.progress": "{{done}} dari {{total}} tugas selesai", + "session.question.progress": "{{current}} dari {{total}} pertanyaan", + "session.question.minimize": "Minimalkan pertanyaan", + "session.question.restore": "Pulihkan pertanyaan", + "session.question.pending.one": "{{count}} pertanyaan tertunda", + "session.question.pending.other": "{{count}} pertanyaan tertunda", + "session.followupDock.summary.one": "{{count}} pesan dalam antrean", + "session.followupDock.summary.other": "{{count}} pesan dalam antrean", + "session.followupDock.sendNow": "Kirim sekarang", + "session.followupDock.edit": "Sunting", + "session.followupDock.collapse": "Ciutkan pesan dalam antrean", + "session.followupDock.expand": "Bentangkan pesan dalam antrean", + "session.revertDock.summary.one": "{{count}} pesan diurungkan", + "session.revertDock.summary.other": "{{count}} pesan diurungkan", + "session.revertDock.collapse": "Ciutkan pesan yang diurungkan", + "session.revertDock.expand": "Bentangkan pesan yang diurungkan", + "session.revertDock.restore": "Pulihkan pesan", + + "session.new.title": "Buat apa saja", + "session.new.project.new": "Proyek baru", + "session.new.project.search": "Cari proyek", + "session.new.project.add": "Tambah proyek", + "session.new.worktree.main": "Cabang utama", + "session.new.worktree.mainWithBranch": "Cabang utama ({{branch}})", + "session.new.worktree.create": "Buat worktree baru", + "session.new.workspace.runIn": "Jalankan sesi dalam", + "session.new.workspace.triggerLocal": "Lokal", + "session.new.workspace.local": "Repositori lokal", + "session.new.workspace.existing": "Ruang kerja…", + "session.new.git.none": "Tanpa Git", + "session.new.lastModified": "Terakhir diubah", + + "session.header.search.placeholder": "Cari {{project}}", + "session.header.searchFiles": "Cari berkas", + "session.header.openIn": "Buka di", + "session.header.open.action": "Buka {{app}}", + "session.header.open.ariaLabel": "Buka di {{app}}", + "session.header.open.menu": "Opsi buka", + "session.header.open.copyPath": "Salin jalur", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "Tampilkan di Finder", + "session.header.reveal.fileExplorer": "Tampilkan di File Explorer", + "session.header.reveal.containingFolder": "Buka folder yang memuatnya", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.sublimeText": "Sublime Text", + "session.header.open.app.powershell": "PowerShell", + + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Konfigurasi server", + "status.popover.tab.servers": "Server", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugin", + "status.popover.action.manageServers": "Kelola server", + + "session.share.popover.title": "Publikasikan di web", + "session.share.popover.description.shared": "Sesi ini publik di web. Siapa pun dengan tautan dapat mengaksesnya.", + "session.share.popover.description.unshared": + "Bagikan sesi secara publik di web. Siapa pun dengan tautan dapat mengaksesnya.", + "session.share.action.share": "Bagikan", + "session.share.action.publish": "Publikasikan", + "session.share.action.publishing": "Mempublikasikan...", + "session.share.action.unpublish": "Batalkan publikasi", + "session.share.action.unpublishing": "Membatalkan publikasi...", + "session.share.action.view": "Lihat", + "session.share.copy.copied": "Tersalin", + "session.share.copy.copyLink": "Salin tautan", + + "lsp.tooltip.none": "Tidak ada server LSP", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Memuat prompt...", + "terminal.loading": "Memuat terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Tutup terminal", + "terminal.connectionLost.title": "Koneksi Terputus", + "terminal.connectionLost.abnormalClose": "WebSocket ditutup secara tidak normal: {{code}}", + "terminal.connectionLost.description": "Koneksi terminal terputus. Ini bisa terjadi saat server dimulai ulang.", + "terminal.connectTicket.csrfError": + "Tiket koneksi PTY ditolak oleh pemeriksaan asal atau CSRF. Periksa konfigurasi CORS server.", + "terminal.connectTicket.statusError": "Tiket koneksi PTY gagal dengan status {{status}}", + "titlebar.update": "Perbarui", + "titlebar.updateVersion": "Perbarui {{version}}", + + "common.closeTab": "Tutup tab", + "common.dismiss": "Tutup", + "common.moreCountSuffix": " (+{{count}} lainnya)", + "common.requestFailed": "Permintaan gagal", + "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", + "common.close": "Tutup", + "common.edit": "Sunting", + "common.loadMore": "Muat lebih banyak", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Spasi", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "tidak diketahui", + + "common.time.justNow": "Baru saja", + "common.time.minutesAgo.short": "{{count}}m lalu", + "common.time.hoursAgo.short": "{{count}}j lalu", + "common.time.daysAgo.short": "{{count}}h lalu", + + "sidebar.menu.toggle": "Alihkan menu", + "sidebar.nav.projectsAndSessions": "Proyek dan sesi", + "sidebar.settings": "Pengaturan", + "sidebar.help": "Bantuan", + "sidebar.workspaces.enable": "Aktifkan ruang kerja", + "sidebar.workspaces.disable": "Nonaktifkan ruang kerja", + "sidebar.gettingStarted.title": "Memulai", + "sidebar.gettingStarted.line1": "OpenCode menyertakan model gratis sehingga Anda dapat memulai dengan segera.", + "sidebar.gettingStarted.line2": + "Hubungkan penyedia mana pun untuk menggunakan model, termasuk Claude, GPT, Gemini, dll.", + "sidebar.project.recentSessions": "Sesi terbaru", + "sidebar.project.viewAllSessions": "Lihat semua sesi", + "sidebar.project.clearNotifications": "Hapus notifikasi", + "sidebar.empty.title": "Tidak ada proyek terbuka", + "sidebar.empty.description": "Buka proyek untuk memulai", + + "debugBar.ariaLabel": "Diagnostik kinerja pengembangan", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Transisi rute terakhir yang mencapai halaman sesi, diukur sejak perute dimulai hingga render pertama setelah stabil.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Frame per detik yang dihitung secara bergulir selama 5 detik terakhir.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Waktu frame terburuk selama 5 detik terakhir.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Frame di atas 32ms dalam 5 detik terakhir.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Waktu terblokir dan jumlah tugas panjang dalam 5 detik terakhir. Tugas maks: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Penundaan masukan terburuk yang diamati dalam 5 detik terakhir.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "Perkiraan durasi interaksi selama 5 detik terakhir. Ini mirip INP, bukan INP Web Vitals resmi.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Pergeseran tata letak kumulatif untuk masa pakai aplikasi saat ini.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Heap JS terpakai vs batas heap. Khusus Chromium.", + "debugBar.mem.tip": "Heap JS terpakai vs batas heap. {{used}} dari {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Paksa gaya fokus pada semua elemen interaktif", + "debugBar.focus.on": "AKTIF", + "debugBar.focus.off": "NONAKTIF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Paksa arah tata letak seluruh aplikasi tanpa mengubah bahasa yang dipilih", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + + "app.name.desktop": "OpenCode Desktop", + + "settings.section.desktop": "Desktop", + "settings.section.server": "Server", + "settings.tab.general": "Umum", + "settings.tab.shortcuts": "Pintasan", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Integrasi WSL", + "settings.desktop.wsl.description": "Jalankan server OpenCode di dalam WSL di Windows.", + + "settings.general.section.appearance": "Tampilan", + "settings.general.section.advanced": "Lanjutan", + "settings.general.section.notifications": "Notifikasi sistem", + "settings.general.section.updates": "Pembaruan", + "settings.general.section.sounds": "Efek suara", + "settings.general.section.feed": "Umpan", + "settings.general.section.display": "Tampilan", + + "settings.general.row.language.title": "Bahasa", + "settings.general.row.language.description": "Ubah bahasa tampilan untuk OpenCode", + "settings.general.row.shell.title": "Shell terminal", + "settings.general.row.shell.description": "Shell yang digunakan oleh terminal dan alat agen", + "settings.general.row.shell.autoDefault": "Otomatis (Bawaan)", + "settings.general.row.shell.terminalOnly": "hanya terminal", + "settings.general.row.appearance.title": "Tampilan", + "settings.general.row.appearance.description": "Sesuaikan tampilan OpenCode di perangkat Anda", + "settings.general.row.colorScheme.title": "Skema warna", + "settings.general.row.colorScheme.description": "Pilih apakah OpenCode mengikuti tema sistem, terang, atau gelap", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Sesuaikan tema OpenCode.", + "settings.general.row.font.title": "Font kode", + "settings.general.row.font.description": "Sesuaikan font yang digunakan di blok kode", + "settings.general.row.terminalFont.title": "Font terminal", + "settings.general.row.terminalFont.description": "Sesuaikan font yang digunakan di terminal", + "settings.general.row.uiFont.title": "Font UI", + "settings.general.row.uiFont.description": "Sesuaikan font yang digunakan di seluruh antarmuka", + "settings.general.row.followup.title": "Perilaku lanjutan", + "settings.general.row.followup.description": + "Pilih apakah prompt lanjutan segera mengarahkan atau menunggu dalam antrean", + "settings.general.row.followup.option.queue": "Antrean", + "settings.general.row.followup.option.steer": "Arahkan", + "settings.general.row.showFileTree.title": "Pohon berkas", + "settings.general.row.showFileTree.description": "Tampilkan panel pohon berkas di sesi", + "settings.general.row.showNavigation.title": "Kontrol navigasi", + "settings.general.row.showNavigation.description": "Tampilkan tombol mundur dan maju di bilah judul desktop", + "settings.general.row.showSearch.title": "Palet perintah", + "settings.general.row.showSearch.description": "Tampilkan tombol pencarian dan palet perintah di bilah judul", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Tampilkan tombol terminal di bilah judul desktop", + "settings.general.row.showStatus.title": "Status server", + "settings.general.row.showStatus.description": "Tampilkan tombol status server di bilah judul", + "settings.general.row.mobileTitlebarBottom.title": "Navigasi bawah", + "settings.general.row.mobileTitlebarBottom.description": + "Tempatkan bilah judul dan tab sesi di bagian bawah layar pada perangkat seluler", + "settings.general.row.showCustomAgents.title": "Tampilkan agen", + "settings.general.row.showCustomAgents.description": + "Beralih antaragen di penyusun. Saat disembunyikan, Agen Build digunakan sebagai bawaan.", + "settings.general.row.reasoningSummaries.title": "Tampilkan ringkasan penalaran", + "settings.general.row.reasoningSummaries.description": "Tampilkan ringkasan penalaran model di linimasa", + "settings.general.row.shellToolPartsExpanded.title": "Bentangkan bagian alat shell", + "settings.general.row.shellToolPartsExpanded.description": + "Tampilkan bagian alat shell yang dibentangkan secara bawaan di linimasa", + "settings.general.row.editToolPartsExpanded.title": "Bentangkan bagian alat edit", + "settings.general.row.editToolPartsExpanded.description": + "Tampilkan bagian alat edit, tulis, dan patch yang dibentangkan secara bawaan di linimasa", + "settings.general.row.newInterface.title": "Tata letak baru", + "settings.general.row.newInterface.badge": "Baru", + "settings.general.row.newInterface.description": + "Gunakan tab dan tata letak beranda baru. Beralihlah antartata letak untuk waktu terbatas.", + "settings.general.row.newInterfaceNotice.title": "Anda sekarang menggunakan tata letak baru", + "settings.general.row.newInterfaceNotice.description": "Tata letak sebelumnya sudah tidak tersedia", + "settings.general.row.newInterfaceNotice.dismiss": "Tutup", + "settings.general.row.pinchZoom.title": "Cubit untuk zum", + "settings.general.row.pinchZoom.description": + "Izinkan gerakan cubit pada trackpad dan Ctrl-gulir untuk melakukan zum", + + "settings.general.row.wayland.title": "Gunakan Wayland asli", + "settings.general.row.wayland.description": "Nonaktifkan sistem cadangan X11 di Wayland. Memerlukan mulai ulang.", + "settings.general.row.wayland.tooltip": + "Di Linux dengan monitor berlaju penyegaran berbeda, Wayland asli bisa lebih stabil.", + + "settings.general.row.releaseNotes.title": "Catatan rilis", + "settings.general.row.releaseNotes.description": "Tampilkan pop-up Apa yang Baru setelah pembaruan", + + "settings.updates.row.startup.title": "Periksa pembaruan saat dimulai", + "settings.updates.row.startup.description": "Secara otomatis memeriksa pembaruan saat OpenCode diluncurkan", + "settings.updates.row.check.title": "Periksa pembaruan", + "settings.updates.row.check.description": "Periksa pembaruan secara manual dan instal jika tersedia", + "settings.updates.action.checkNow": "Periksa sekarang", + "settings.updates.action.checking": "Memeriksa...", + "settings.updates.action.downloading": "Mengunduh...", + "settings.updates.action.installing": "Menginstal...", + "settings.updates.toast.latest.title": "Versi Anda sudah terbaru", + "settings.updates.toast.latest.description": "Anda menjalankan versi terbaru OpenCode.", + "sound.option.none": "Tidak ada", + "sound.option.alert01": "Peringatan 01", + "sound.option.alert02": "Peringatan 02", + "sound.option.alert03": "Peringatan 03", + "sound.option.alert04": "Peringatan 04", + "sound.option.alert05": "Peringatan 05", + "sound.option.alert06": "Peringatan 06", + "sound.option.alert07": "Peringatan 07", + "sound.option.alert08": "Peringatan 08", + "sound.option.alert09": "Peringatan 09", + "sound.option.alert10": "Peringatan 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + + "settings.general.notifications.agent.title": "Agen", + "settings.general.notifications.agent.description": + "Tampilkan notifikasi sistem saat agen selesai atau memerlukan perhatian", + "settings.general.notifications.permissions.title": "Izin", + "settings.general.notifications.permissions.description": "Tampilkan notifikasi sistem saat izin diperlukan", + "settings.general.notifications.errors.title": "Kesalahan", + "settings.general.notifications.errors.description": "Tampilkan notifikasi sistem saat terjadi kesalahan", + + "settings.general.sounds.agent.title": "Agen", + "settings.general.sounds.agent.description": "Mainkan suara saat agen selesai atau memerlukan perhatian", + "settings.general.sounds.permissions.title": "Izin", + "settings.general.sounds.permissions.description": "Mainkan suara saat izin diperlukan", + "settings.general.sounds.errors.title": "Kesalahan", + "settings.general.sounds.errors.description": "Mainkan suara saat terjadi kesalahan", + + "settings.shortcuts.title": "Pintasan keyboard", + "settings.shortcuts.reset.button": "Atur ulang ke bawaan", + "settings.shortcuts.reset.toast.title": "Pintasan diatur ulang", + "settings.shortcuts.reset.toast.description": "Pintasan keyboard telah diatur ulang ke bawaan.", + "settings.shortcuts.conflict.title": "Pintasan sudah digunakan", + "settings.shortcuts.conflict.description": "{{keybind}} sudah ditetapkan ke {{titles}}.", + "settings.shortcuts.unassigned": "Belum ditetapkan", + "settings.shortcuts.pressKeys": "Tekan tombol", + "settings.shortcuts.search.placeholder": "Cari pintasan", + "settings.shortcuts.search.empty": "Tidak ada pintasan ditemukan", + + "settings.shortcuts.group.general": "Umum", + "settings.shortcuts.group.session": "Sesi", + "settings.shortcuts.group.navigation": "Navigasi", + "settings.shortcuts.group.modelAndAgent": "Model dan agen", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + + "settings.providers.title": "Penyedia", + "settings.providers.description": "Pengaturan penyedia akan dapat dikonfigurasi di sini.", + "settings.providers.section.connected": "Penyedia terhubung", + "settings.providers.connected.empty": "Tidak ada penyedia terhubung", + "settings.providers.connected.environmentDescription": "Terhubung dari variabel lingkungan Anda", + "settings.providers.section.popular": "Penyedia populer", + "settings.providers.custom.description": "Tambahkan penyedia yang kompatibel dengan OpenAI berdasarkan URL dasar.", + "settings.providers.tag.environment": "Lingkungan", + "settings.providers.tag.config": "Konfigurasi", + "settings.providers.tag.custom": "Kustom", + "settings.providers.tag.other": "Lainnya", + "settings.models.title": "Model", + "settings.models.description": "Pengaturan model akan dapat dikonfigurasi di sini.", + "settings.agents.title": "Agen", + "settings.agents.description": "Pengaturan agen akan dapat dikonfigurasi di sini.", + "settings.commands.title": "Perintah", + "settings.commands.description": "Pengaturan perintah akan dapat dikonfigurasi di sini.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Pengaturan MCP akan dapat dikonfigurasi di sini.", + + "settings.permissions.title": "Izin", + "settings.permissions.description": "Kontrol alat apa yang dapat digunakan server secara bawaan.", + "settings.permissions.section.tools": "Alat", + "settings.permissions.toast.updateFailed.title": "Gagal memperbarui izin", + + "settings.permissions.action.allow": "Izinkan", + "settings.permissions.action.ask": "Tanya", + "settings.permissions.action.deny": "Tolak", + + "settings.permissions.tool.read.title": "Baca", + "settings.permissions.tool.read.description": "Membaca berkas (mencocokkan jalur berkas)", + "settings.permissions.tool.edit.title": "Sunting", + "settings.permissions.tool.edit.description": "Memodifikasi berkas, termasuk suntingan, tulisan, dan patch", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Cocokkan berkas menggunakan pola glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Cari konten berkas menggunakan ekspresi reguler", + "settings.permissions.tool.list.title": "Daftar", + "settings.permissions.tool.list.description": "Daftarkan berkas dalam direktori", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Jalankan perintah shell", + "settings.permissions.tool.task.title": "Tugas", + "settings.permissions.tool.task.description": "Luncurkan sub-agen", + "settings.permissions.tool.skill.title": "Keahlian", + "settings.permissions.tool.skill.description": "Muat keahlian berdasarkan nama", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Jalankan kueri server bahasa", + "settings.permissions.tool.todowrite.title": "Tulis Tugas", + "settings.permissions.tool.todowrite.description": "Perbarui daftar tugas", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Ambil konten dari URL", + "settings.permissions.tool.websearch.title": "Pencarian Web", + "settings.permissions.tool.websearch.description": "Cari di web", + "settings.permissions.tool.external_directory.title": "Direktori Eksternal", + "settings.permissions.tool.external_directory.description": "Akses berkas di luar direktori proyek", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Deteksi panggilan alat berulang dengan masukan yang identik", + + "session.delete.failed.title": "Gagal menghapus sesi", + "session.delete.title": "Hapus sesi", + "session.delete.confirm": 'Hapus sesi "{{name}}"?', + "session.delete.button": "Hapus sesi", + + "workspace.new": "Ruang kerja baru", + "workspace.type.local": "lokal", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "Gagal membuat ruang kerja", + "workspace.delete.failed.title": "Gagal menghapus ruang kerja", + "workspace.resetting.title": "Mengatur ulang ruang kerja", + "workspace.resetting.description": "Ini mungkin memakan waktu sebentar.", + "workspace.reset.failed.title": "Gagal mengatur ulang ruang kerja", + "workspace.reset.success.title": "Ruang kerja diatur ulang", + "workspace.reset.success.description": "Ruang kerja sekarang cocok dengan cabang bawaan.", + "workspace.error.stillPreparing": "Ruang kerja masih disiapkan", + "workspace.status.checking": "Memeriksa perubahan yang belum digabung...", + "workspace.status.error": "Tidak dapat memverifikasi status Git.", + "workspace.status.clean": "Tidak terdeteksi perubahan yang belum digabung.", + "workspace.status.dirty": "Terdeteksi perubahan yang belum digabung di ruang kerja ini.", + "workspace.delete.title": "Hapus ruang kerja", + "workspace.delete.confirm": 'Hapus ruang kerja "{{name}}"?', + "workspace.delete.button": "Hapus ruang kerja", + "workspace.reset.title": "Atur ulang ruang kerja", + "workspace.reset.confirm": 'Atur ulang ruang kerja "{{name}}"?', + "workspace.reset.button": "Atur ulang ruang kerja", + "workspace.reset.archived.none": "Tidak ada sesi aktif yang akan diarsipkan.", + "workspace.reset.archived.one": "1 sesi akan diarsipkan.", + "workspace.reset.archived.many": "{{count}} sesi akan diarsipkan.", + "workspace.reset.note": "Ini akan mengatur ulang ruang kerja agar cocok dengan cabang bawaan.", +} diff --git a/packages/app/src/i18n/is.ts b/packages/app/src/i18n/is.ts new file mode 100644 index 0000000000000000000000000000000000000000..a45ed9a40cb0b11203068415d6ab5e7c353a086e --- /dev/null +++ b/packages/app/src/i18n/is.ts @@ -0,0 +1,1157 @@ +import { DESKTOP_NATIVE_KEYS } from "./desktop-native" + +const desktop = [ + "OpenCode", + "Skrá", + "Breyta", + "Skoða", + "Fara", + "Gluggi", + "Hjálp", + "Leita að uppfærslum...", + "Stillingar", + "Endurhlaða Webview", + "Endurræsa", + "Flytja út atvikaskrár...", + "Ný seta", + "Opna verkefni...", + "Nýr gluggi", + "Loka glugga", + "Afturkalla", + "Endurtaka", + "Klippa", + "Afrita", + "Líma", + "Eyða", + "Velja allt", + "Víxla hliðarstiku", + "Víxla skjáhermi", + "Víxla skráartré", + "Endurhlaða", + "Víxla forritaraverkfærum", + "Raunstærð", + "Auka aðdrátt", + "Minnka aðdrátt", + "Víxla öllum skjánum", + "Til baka", + "Áfram", + "Fyrri seta", + "Næsta seta", + "Fyrra verkefni", + "Næsta verkefni", + "Lágmarka", + "Hámarka", + "OpenCode skjölun", + "Aðstoðarspjallborð", + "Deila ábendingu", + "Tilkynna villu", + "OpenCode-valmynd", + "Athugun á uppfærslu mistókst.", + "Villa í uppfærslu", + "Þú ert með nýjustu útgáfuna.", + "Engar uppfærslur", + "Uppfærsla {{version}} sótt. Endurræsa núna?", + "Uppfærsla tilbúin", + "Endurræsa", + "Seinna", + "Endurræsa forrit", + "Flytja út atvikaskrár", + "Bíða áfram", + "Hætta", + "Ekki tókst að hlaða OpenCode", + "OpenCode-glugganum var óvænt lokað", + "OpenCode svarar ekki", + "Þú getur endurræst forritið, opnað atvikaskrárnar eða haldið áfram að bíða.", + "Gluggi: {{window}}\nSlóð: {{url}}\nVilla: {{code}} {{description}}", + "Gluggi: {{window}}\nÁstæða: {{reason}}\nKóði: {{code}}", + "<óþekkt>", + "Veldu möppu", + "Veldu skrá", + "Vista skrá", + "Skrár", + "Staðbundinn þjónn", + "WSL er aðeins í boði á Windows", + "WSL er ekki í boði", + "Ekki tókst að telja upp uppsettar WSL-dreifingar", + "Ekki tókst að telja upp WSL-dreifingar á netinu", + "Ekki er hægt að keyra skipanir í dreifingu", + "Uppsetning WSL mistókst", + "Uppsetning dreifingar mistókst: {{distro}}", + "Uppsetning OpenCode mistókst", + "{{distro}} hefur þegar verið bætt við", + "opencode er ekki uppsett í þessari dreifingu", + "opencode er uppsett en ekki var hægt að keyra það", + "OpenCode er ekki uppsett í {{distro}}", + "Uppfærslu OpenCode lauk en {{distro}} tilkynnir enn {{installed}}; vænt var {{expected}}", + "engin útgáfa", + "WSL-þjónn hætti eftir ræsingu (code={{code}} signal={{signal}})", + "WSL-þjónn hætti áður en hann varð starfhæfur (code={{code}} signal={{signal}}){{output}}", + "Tímamörk heilsuprófunar hliðarferlis fyrir {{distro}} runnu út eftir {{timeout}} ms", + "Tímamörk {{command}} {{args}} runnu út eftir {{timeout}} ms", + "Ekki tókst að sækja gátt", + "Skráin var ekki valin í skráavalinu", + "Valin viðhengi fara yfir {{limit}} MB hámarkið", +] + +export const dict = { + ...Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key, index) => [key, desktop[index]])), + "command.category.suggested": "Tillögur", + "command.category.view": "Skoða", + "command.category.project": "Verkefni", + "command.category.provider": "Þjónustuveita", + "command.category.server": "Netþjónn", + "command.category.session": "Seta", + "command.category.theme": "Þema", + "command.category.language": "Tungumál", + "command.category.file": "Skrá", + "command.category.context": "Samhengi", + "command.category.terminal": "Skjáhermir", + "command.category.model": "Líkan", + "command.category.mcp": "MCP", + "command.category.agent": "Fulltrúi", + "command.category.permissions": "Heimildir", + "command.category.workspace": "Vinnurými", + "command.category.settings": "Stillingar", + "command.logs.export": "Flytja út atvikaskrár", + "theme.scheme.system": "Kerfi", + "theme.scheme.light": "Ljóst", + "theme.scheme.dark": "Dökkt", + "command.sidebar.toggle": "Skiptu um hliðarstiku", + "command.project.open": "Opna verkefni", + "command.project.previous": "Fyrra verkefni", + "command.project.next": "Næsta verkefni", + "command.project.index": "Skiptu yfir í verkefnið {{index}}", + "command.provider.connect": "Tengja þjónustuveitu", + "command.server.switch": "Skipta um netþjón", + "command.settings.open": "Opna stillingar", + "command.session.previous": "Fyrri seta", + "command.session.next": "Næsta seta", + "command.session.previous.unseen": "Fyrri ólesin seta", + "command.session.next.unseen": "Næsta ólesna seta", + "command.session.archive": "Setja setu í geymslu", + "command.palette": "Skipunarspjald", + "command.theme.cycle": "Skipta um þema", + "command.theme.set": "Notaðu þema: {{theme}}", + "command.theme.scheme.cycle": "Skipta um litasamsetningu", + "command.theme.scheme.set": "Notaðu litasamsetningu: {{scheme}}", + "command.language.cycle": "Skipta um tungumál", + "command.language.set": "Notaðu tungumál: {{language}}", + "command.session.new": "Ný seta", + "command.file.open": "Opna skrá", + "command.tab.close": "Loka flipa", + "command.tab.reopenClosed": "Opnaðu aftur lokaðan flipa", + "command.context.addSelection": "Bættu vali við samhengi", + "command.context.addSelection.description": "Bættu við völdum línum úr núverandi skrá", + "command.input.focus": "Færa fókus í innslátt", + "command.terminal.toggle": "Víxla skjáhermi", + "command.fileTree.toggle": "Skiptu um skráartré", + "command.review.toggle": "Skiptu um skoðun", + "command.terminal.new": "Nýr skjáhermir", + "command.terminal.new.description": "Stofna nýjan skjáhermisflipa", + "command.steps.toggle": "Skiptu um skref", + "command.steps.toggle.description": "Sýna eða fela skref fyrir núverandi skilaboð", + "command.message.previous": "Fyrri skilaboð", + "command.message.previous.description": "Farðu í fyrri notandaskilaboð", + "command.message.next": "Næsta skilaboð", + "command.message.next.description": "Farðu í næstu notendaskilaboð", + "command.model.choose": "Veldu líkan", + "command.model.choose.description": "Veldu aðra gerð", + "command.mcp.toggle": "Skiptu um MCPs", + "command.mcp.toggle.description": "Skiptu um MCPs", + "command.agent.cycle": "Skipta um fulltrúa", + "command.agent.cycle.description": "Skiptu yfir í næsta fulltrúa", + "command.agent.cycle.reverse": "Skipta yfir í fyrri fulltrúa", + "command.agent.cycle.reverse.description": "Skiptu yfir í fyrri fulltrúa", + "command.model.variant.cycle": "Hringrásarhugsunarátak", + "command.model.variant.cycle.description": "Skiptu yfir á næsta átaksstig", + "command.prompt.mode.shell": "Skel", + "command.prompt.mode.normal": "Kvaðning", + "command.permissions.autoaccept.enable": "Samþykkja sjálfkrafa heimildir", + "command.permissions.autoaccept.disable": "Hætta að samþykkja heimildir sjálfkrafa", + "command.workspace.toggle": "Skiptu um vinnusvæði", + "command.workspace.toggle.description": "Virkja eða slökkva á mörgum vinnusvæðum í hliðarstikunni", + "command.session.undo": "Afturkalla", + "command.session.undo.description": "Afturkalla síðustu skilaboð", + "command.session.redo": "Endurtaka", + "command.session.redo.description": "Endurtaktu síðustu afturkölluðu skilaboðin", + "command.session.compact": "Þjappa setu", + "command.session.compact.description": "Draga setuna saman til að minnka samhengi", + "command.session.fork": "Kvíslast frá skilaboðum", + "command.session.fork.description": "Stofna nýja setu út frá fyrri skilaboðum", + "command.session.share": "Deila setu", + "command.session.share.description": "Deila þessari setu og afrita slóðina á klippispjaldið", + "command.session.unshare": "Hætta að deila setu", + "command.session.unshare.description": "Hætta að deila þessari setu", + "command.session.export": "Flytja út setu", + "command.session.export.description": "Flytja allt setuafritið út sem JSON", + "palette.search.placeholder": "Leitaðu að skrám, skipunum og fundum", + "palette.search.placeholder.home": "Leitaðu að skipunum og fundum", + "palette.empty": "Engar niðurstöður fundust", + "palette.group.commands": "Skipanir", + "palette.group.files": "Skrár", + "dialog.provider.search.placeholder": "Leita að veitendum", + "dialog.provider.empty": "Engar veitendur fundust", + "dialog.provider.group.popular": "Vinsælt", + "dialog.provider.group.other": "Annað", + "dialog.provider.custom.label": "Sérsniðin OpenAI-samhæfður veitandi", + "dialog.provider.tag.recommended": "Mælt er með", + "dialog.provider.opencode.note": "Sýndar gerðir þar á meðal Claude, GPT, Gemini og fleira", + "dialog.provider.opencode.tagline": "Áreiðanleg bjartsýni módel", + "dialog.provider.opencodeGo.tagline": "Lággjaldaáskrift fyrir alla", + "dialog.provider.anthropic.note": "Beinn aðgangur að Claude gerðum, þar á meðal Pro og Max", + "dialog.provider.copilot.note": "AI módel fyrir kóðaaðstoð í gegnum GitHub Copilot", + "dialog.provider.openai.note": "GPT gerðir fyrir hröð, fær almenn gervigreind verkefni", + "dialog.provider.google.note": "Gemini módel fyrir hröð, skipulögð viðbrögð", + "dialog.provider.openrouter.note": "Fáðu aðgang að öllum studdum gerðum frá einum þjónustuaðila", + "dialog.provider.vercel.note": "Sameinaður aðgangur að gervigreindum gerðum með snjallri leið", + "dialog.model.select.title": "Veldu líkan", + "dialog.model.search.placeholder": "Leitaðu að gerðum", + "dialog.model.empty": "Engar niðurstöður líkana", + "dialog.model.manage": "Stjórna módelum", + "dialog.model.manage.description": "Sérsníddu hvaða gerðir birtast í gerðavalinu.", + "dialog.model.manage.provider.toggle": "Skiptu um allar {{provider}} gerðir", + "dialog.model.unpaid.freeModels.title": "Ókeypis gerðir frá OpenCode", + "dialog.model.unpaid.addMore.title": "Bættu við fleiri gerðum frá vinsælum veitendum", + "dialog.model.unpaid.viewMoreProviders": "Sjáðu 70+ þjónustuveitendur í viðbót", + "dialog.provider.viewAll": "Sýna fleiri veitendur", + "provider.connect.title": "Tengdu {{provider}}", + "provider.connect.title.anthropicProMax": "Skráðu þig inn með Claude Pro/Max", + "provider.connect.selectMethod": "Veldu innskráningaraðferð fyrir {{provider}}.", + "provider.connect.method.apiKey": "API lykill", + "provider.connect.method.browser": "Vafri", + "provider.connect.method.headless": "Höfuðlaus", + "provider.connect.status.inProgress": "Heimild í gangi...", + "provider.connect.status.waiting": "Bíður eftir heimild...", + "provider.connect.status.failed": "Heimild mistókst: {{error}}", + "provider.connect.apiKey.description": + "Sláðu inn API-lykilinn þinn fyrir {{provider}} til að tengja reikninginn og nota líkön frá {{provider}} í OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API lykill", + "provider.connect.apiKey.placeholder": "API lykill", + "provider.connect.apiKey.required": "API lykill er nauðsynlegur", + "provider.connect.opencodeZen.line1": + "OpenCode Zen veitir þér aðgang að safni af áreiðanlegum og fínstilltum gerðum fyrir kóðunarmiðla.", + "provider.connect.opencodeZen.line2": + "Með einum API lykli færðu aðgang að gerðum eins og Claude, GPT, Gemini, GLM og fleira.", + "provider.connect.opencodeZen.visit.prefix": "Heimsókn", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "til að safna API lykilnum þínum.", + "provider.connect.oauth.code.visit.prefix": "Heimsókn", + "provider.connect.oauth.code.visit.link": "þennan hlekk", + "provider.connect.oauth.code.visit.suffix": + "til að safna heimildarkóðanum þínum til að tengja reikninginn þinn og nota {{provider}} gerðir í OpenCode.", + "provider.connect.oauth.code.label": "{{method}} heimildarkóði", + "provider.connect.oauth.code.placeholder": "Heimildarkóði", + "provider.connect.oauth.code.required": "Heimildarkóði er nauðsynlegur", + "provider.connect.oauth.code.invalid": "Ógildur heimildarkóði", + "provider.connect.oauth.auto.visit.prefix": "Heimsókn", + "provider.connect.oauth.auto.visit.link": "þennan hlekk", + "provider.connect.oauth.auto.visit.suffix": + "og sláðu inn kóðann hér að neðan til að tengja reikninginn þinn og nota {{provider}} gerðir í OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Staðfestingarkóði", + "provider.connect.toast.connected.title": "{{provider}} tengdur", + "provider.connect.toast.connected.description": "{{provider}} gerðir eru nú fáanlegar til notkunar.", + "provider.custom.title": "Sérsniðin veitandi", + "provider.custom.unavailable": "Sérsniðnar veitendur eru ekki tiltækar á þessum netþjóni", + "provider.custom.description.prefix": "Stilltu OpenAI-samhæfan þjónustuaðila. Sjáðu", + "provider.custom.description.link": "stillingarskjöl fyrir veitanda", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Auðkenni veitanda", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Lágstafir, tölustafir, bandstrik eða undirstrik", + "provider.custom.field.name.label": "Birta nafn", + "provider.custom.field.name.placeholder": "AI veitandinn minn", + "provider.custom.field.baseURL.label": "Grunnur URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API lykill", + "provider.custom.field.apiKey.placeholder": "API lykill", + "provider.custom.field.apiKey.description": + "Valfrjálst. Skildu eftir autt ef þú stjórnar auðkenningu í gegnum hausa.", + "provider.custom.models.label": "Fyrirmyndir", + "provider.custom.models.id.label": "auðkenni", + "provider.custom.models.id.placeholder": "módel-id", + "provider.custom.models.name.label": "Nafn", + "provider.custom.models.name.placeholder": "Birtingarnafn", + "provider.custom.models.remove": "Fjarlægðu líkanið", + "provider.custom.models.add": "Bæta við líkani", + "provider.custom.headers.label": "Hausar (valfrjálst)", + "provider.custom.headers.key.label": "Haus", + "provider.custom.headers.key.placeholder": "Haus-nafn", + "provider.custom.headers.value.label": "Gildi", + "provider.custom.headers.value.placeholder": "gildi", + "provider.custom.headers.remove": "Fjarlægðu haus", + "provider.custom.headers.add": "Bæta við haus", + "provider.custom.error.providerID.required": "Auðkenni þjónustuveitanda er krafist", + "provider.custom.error.providerID.format": "Notaðu lágstafi, tölustafi, bandstrik eða undirstrik", + "provider.custom.error.providerID.exists": "Þetta auðkenni þjónustuveitunnar er þegar til", + "provider.custom.error.name.required": "Birtingarnafn er áskilið", + "provider.custom.error.baseURL.required": "Base URL er krafist", + "provider.custom.error.baseURL.format": "Verður að byrja á http:// eða https://", + "provider.custom.error.required": "Áskilið", + "provider.custom.error.duplicate": "Afrit", + "provider.disconnect.toast.disconnected.title": "{{provider}} aftengdur", + "provider.disconnect.toast.disconnected.description": "{{provider}} gerðir eru ekki lengur fáanlegar.", + "model.tag.free": "Ókeypis", + "model.tag.latest": "Nýjasta", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "texta", + "model.input.image": "mynd", + "model.input.audio": "hljóð", + "model.input.video": "myndband", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Leyfir: {{inputs}}", + "model.tooltip.context.label": "Samhengi", + "model.tooltip.inputs": "Inntak", + "model.tooltip.model": "Líkan", + "model.tooltip.provider": "Þjónustuveita", + "model.tooltip.reasoning": "Rökstuðningur", + "model.tooltip.reasoning.allowed": "Leyfir rökstuðning", + "model.tooltip.reasoning.none": "Enginn rökstuðningur", + "model.tooltip.context": "Samhengismörk {{limit}}", + "common.search.placeholder": "Leita", + "common.goBack": "Sigla til baka", + "common.goForward": "Sigla áfram", + "common.loading": "Hleðsla", + "common.loading.ellipsis": "...", + "common.cancel": "Hætta við", + "common.clear": "Hreinsa", + "common.open": "Opið", + "common.connect": "Tengdu", + "common.disconnect": "Aftengjast", + "common.continue": "Halda áfram", + "common.submit": "Sendu inn", + "common.save": "Vista", + "common.saving": "Vistar...", + "common.default": "Sjálfgefið", + "common.attachment": "viðhengi", + "prompt.placeholder.shell": "Sláðu inn skeljarskipun... {{example}}", + "prompt.placeholder.normal": 'Spyrðu hvað sem er... "{{example}}"', + "prompt.placeholder.simple": "Spurðu hvað sem er...", + "prompt.placeholder.summarizeComments": "Taktu saman athugasemdir…", + "prompt.placeholder.summarizeComment": "Tekið saman athugasemd...", + "prompt.mode.shell": "Skel", + "prompt.mode.normal": "Kvaðning", + "prompt.mode.shell.exit": "esc að hætta", + "session.child.promptDisabled": "Ekki er hægt að biðja um undirboðslotur.", + "session.child.backToParent": "Aftur í aðalfund.", + "prompt.example.1": "Lagaðu TODO í kóðagrunninum", + "prompt.example.2": "Hver er tæknistafla þessa verkefnis?", + "prompt.example.3": "Lagaðu biluð próf", + "prompt.example.4": "Útskýrðu hvernig auðkenning virkar", + "prompt.example.5": "Finndu og lagfærðu öryggisgalla", + "prompt.example.6": "Bættu við einingaprófum fyrir notendaþjónustuna", + "prompt.example.7": "Endurstilltu þessa aðgerð til að vera læsilegri", + "prompt.example.8": "Hvað þýðir þessi villa?", + "prompt.example.9": "Hjálpaðu mér að kemba þetta mál", + "prompt.example.10": "Búðu til API skjöl", + "prompt.example.11": "Fínstilltu gagnagrunnsfyrirspurnir", + "prompt.example.12": "Bættu við inntaksstaðfestingu", + "prompt.example.13": "Búðu til nýjan íhlut fyrir...", + "prompt.example.14": "Hvernig set ég þetta verkefni í notkun?", + "prompt.example.15": "Skoðaðu kóðann minn fyrir bestu starfsvenjur", + "prompt.example.16": "Bættu villumeðferð við þessa aðgerð", + "prompt.example.17": "Útskýrðu þetta regex mynstur", + "prompt.example.18": "Umbreyttu þessu í TypeScript", + "prompt.example.19": "Bættu við skráningu í gegnum kóðagrunninn", + "prompt.example.20": "Hvaða ósjálfstæði eru úrelt?", + "prompt.example.21": "Hjálpaðu mér að skrifa flutningshandrit", + "prompt.example.22": "Innleiða skyndiminni fyrir þennan endapunkt", + "prompt.example.23": "Bættu blaðsíðu við þennan lista", + "prompt.example.24": "Búðu til CLI skipun fyrir...", + "prompt.example.25": "Hvernig virka umhverfisbreytur hér?", + "prompt.popover.emptyResults": "Engar samsvarandi niðurstöður", + "prompt.popover.emptyCommands": "Engar samsvarandi skipanir", + "prompt.dropzone.label": "Slepptu myndum, PDF-skjölum eða textaskrám hér", + "prompt.dropzone.file.label": "Slepptu í @nefna skrá", + "prompt.slash.badge.custom": "sérsniðin", + "prompt.slash.badge.skill": "færni", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "virkur", + "prompt.context.includeActiveFile": "Láttu virka skrá fylgja með", + "prompt.context.removeActiveFile": "Fjarlægðu virka skrá úr samhengi", + "prompt.context.removeFile": "Fjarlægðu skrá úr samhengi", + "prompt.action.attachFile": "Bæta við skrám", + "prompt.menu.addImagesAndFiles": "Bættu við skrám og fleira", + "prompt.menu.imagesAndFiles": "Myndir og skrár", + "prompt.menu.commands": "Skipanir", + "prompt.menu.context": "Samhengi", + "prompt.menu.shellCommand": "Skeljarskipun", + "prompt.attachment.remove": "Fjarlægðu viðhengi", + "prompt.action.send": "Senda", + "prompt.action.stop": "Stöðva", + "prompt.toast.pasteUnsupported.title": "Óstudd viðhengi", + "prompt.toast.pasteUnsupported.description": "Aðeins er hægt að hengja myndir, PDF-skjöl eða textaskrár hér við.", + "prompt.toast.attachmentDuplicate.title": "Þessari skrá hefur þegar verið hlaðið upp", + "prompt.toast.modelAgentRequired.title": "Veldu fulltrúa og líkan", + "prompt.toast.modelAgentRequired.description": "Veldu fulltrúa og líkan áður en þú sendir kvaðningu.", + "prompt.toast.worktreeCreateFailed.title": "Mistókst að búa til vinnutré", + "prompt.toast.sessionCreateFailed.title": "Ekki tókst að búa til lotu", + "prompt.toast.shellSendFailed.title": "Mistókst að senda skel skipun", + "prompt.toast.commandSendFailed.title": "Mistókst að senda skipun", + "prompt.toast.promptSendFailed.title": "Mistókst að senda tilkynningu", + "prompt.toast.promptSendFailed.description": "Ekki tókst að sækja lotu", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} af {{total}} virkt", + "dialog.mcp.empty": "Engar MCPs stilltar", + "dialog.lsp.empty": "LSPs greind sjálfkrafa úr skráargerðum", + "dialog.plugins.empty": "Viðbætur stilltar í opencode.json", + "mcp.status.connected": "tengdur", + "mcp.status.failed": "mistókst", + "mcp.status.needs_auth": "þarf heimild", + "mcp.status.disabled": "fatlaður", + "mcp.auth.clickToAuthenticate": "Smelltu til að auðkenna", + "dialog.fork.empty": "Engin skilaboð til að punga frá", + "dialog.directory.search.placeholder": "Leita í möppum", + "dialog.directory.empty": "Engar möppur fundust", + "dialog.directory.action.selectFile": "Veldu skrá", + "dialog.directory.action.selectFolder": "Veldu möppu", + "dialog.directory.root": "Rót", + "dialog.directory.parent": "Foreldri", + "dialog.directory.readError": "Get ekki lesið þessa möppu", + "app.server.unreachable": "Ekki tókst að ná í {{server}}", + "app.server.retrying": "Reynir sjálfkrafa aftur...", + "app.server.otherServers": "Aðrir netþjónar", + "dialog.server.title": "Netþjónar", + "dialog.server.description": "Skiptu um hvaða OpenCode miðlara þetta app tengist.", + "dialog.server.search.placeholder": "Leitaðu að netþjónum", + "dialog.server.empty": "Engir netþjónar ennþá", + "dialog.server.add.title": "Bæta við netþjóni", + "dialog.server.add.url": "Heimilisfang netþjóns", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Gat ekki tengst þjóninum", + "dialog.server.add.checking": "Athugar...", + "dialog.server.add.button": "Bæta við netþjóni", + "dialog.server.add.name": "Nafn netþjóns (valfrjálst)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Notandanafn (valfrjálst)", + "dialog.server.add.usernamePlaceholder": "notendanafn", + "dialog.server.add.password": "Lykilorð (valfrjálst)", + "dialog.server.add.passwordPlaceholder": "lykilorð", + "dialog.server.edit.title": "Breyta miðlara", + "dialog.server.default.title": "Sjálfgefinn þjónn", + "dialog.server.default.description": + "Tengstu við þennan netþjón við ræsingu forrits í stað þess að ræsa staðbundinn netþjón. Krefst endurræsingar.", + "dialog.server.default.none": "Enginn þjónn valinn", + "dialog.server.default.set": "Stilltu núverandi netþjón sem sjálfgefið", + "dialog.server.default.clear": "Hreinsa", + "dialog.server.action.remove": "Fjarlægðu netþjón", + "dialog.server.menu.edit": "Breyta", + "dialog.server.menu.default": "Stillt sem sjálfgefið", + "dialog.server.menu.defaultRemove": "Fjarlægja sjálfgefið", + "dialog.server.menu.delete": "Eyða", + "dialog.server.current": "Núverandi þjónn", + "dialog.server.status.default": "Sjálfgefið", + "wsl.server.add": "Bættu við WSL netþjóni", + "wsl.server.addShort": "Bættu við WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL netþjónn", + "wsl.server.retryStart": "Reyndu að byrja aftur", + "wsl.server.updating": "Uppfærir...", + "wsl.onboarding.step.distro": "Veldu distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Athugar WSL...", + "wsl.onboarding.restartRequired": "Windows þarf endurræsingu til að klára uppsetningu WSL.", + "wsl.onboarding.ready": "WSL er tilbúinn.", + "wsl.onboarding.required": "WSL þarf til að halda áfram.", + "wsl.onboarding.checkingDistros": "Athugar dreifingar...", + "wsl.onboarding.installingDistro": "Setur upp {{distro}}...", + "wsl.onboarding.checkingDistro": "Athugar {{distro}}...", + "wsl.onboarding.listingDistros": "Birtir dreifingar...", + "wsl.onboarding.distroReady": "{{distro}} er tilbúinn.", + "wsl.onboarding.distroNotInstalled": "{{distro}} er ekki uppsett ennþá.", + "wsl.onboarding.openDistroOnce": "Opnaðu {{distro}} einu sinni til að ljúka uppsetningu.", + "wsl.onboarding.finishingDistro": "Að klára uppsetningu fyrir {{distro}}.", + "wsl.onboarding.pickDistro": "Veldu dreifingu eða settu upp fyrir neðan.", + "wsl.onboarding.checkingOpencode": "Athugar OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Athugar OpenCode í {{distro}}...", + "wsl.onboarding.updatingOpencode": "Uppfærir OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Uppfærir OpenCode í {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Uppfærðu OpenCode í {{distro}}.", + "wsl.onboarding.updateOpencode": "Uppfærðu OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode er tilbúið í {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode er tilbúinn.", + "wsl.onboarding.installOpencodeIn": "Settu upp OpenCode í {{distro}}.", + "wsl.onboarding.installOpencode": "Settu upp OpenCode", + "wsl.onboarding.chooseDistroFirst": "Veldu dreifingu fyrst.", + "wsl.onboarding.loadFailed": "Mistókst að hlaða WSL stöðu.", + "wsl.onboarding.loading": "Hleður...", + "wsl.onboarding.installedDistros": "Uppsett dreifing", + "wsl.onboarding.checkAgain": "Athugaðu aftur", + "wsl.onboarding.distroStatus.ready": "Tilbúið", + "wsl.onboarding.distroStatus.checking": "Athugar...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode ekki uppsett", + "wsl.onboarding.distroStatus.missingTools": "Vantar bash, krullu", + "wsl.onboarding.distroStatus.unsupported": "Óstudd · Notaðu WSL 2", + "wsl.onboarding.needAnotherDistro": "Þarftu aðra dreifingu?", + "wsl.onboarding.needAnotherDistroHint": "Settu upp Linux dreifingu úr WSL vörulistanum", + "wsl.onboarding.wslNotInstalled.title": "WSL ekki uppsett", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows undirkerfi fyrir Linux) þarf áður en OpenCode getur bætt við WSL netþjóni", + "wsl.onboarding.wslUnavailable.title": "WSL ekki í boði", + "wsl.onboarding.wslUnavailable.description": "OpenCode gat ekki staðfest WSL á þessari vél.", + "wsl.onboarding.installWsl": "Settu upp WSL", + "wsl.onboarding.windowsRestartRequired": + "Endurræstu Windows til að klára að setja upp WSL, opnaðu síðan OpenCode aftur.", + "wsl.onboarding.next": "Næst", + "wsl.onboarding.refresh": "Endurnýja", + "wsl.onboarding.allDistrosAdded": "Öllum uppsettum dreifingum er þegar bætt við.", + "wsl.onboarding.noDistros": "Engin dreifing hefur fundist ennþá.", + "wsl.onboarding.install": "Settu upp", + "wsl.onboarding.installing": "Setur upp...", + "wsl.onboarding.installDistro": "Settu upp distro", + "wsl.onboarding.searchDistros": "Leitaðu að dreifingum", + "wsl.onboarding.wsl2Required": "WSL 2 er krafist.", + "wsl.onboarding.toolsRequired": "Þetta distro þarf bash og krullu.", + "wsl.onboarding.openTerminal": "Opið flugstöð", + "wsl.onboarding.path": "Slóð: {{path}}", + "wsl.onboarding.notFound": "ekki fundið", + "wsl.onboarding.version": "Útgáfa: {{version}}", + "wsl.onboarding.unknown": "óþekkt", + "wsl.onboarding.desktopVersion": "skrifborð {{version}}", + "wsl.onboarding.versionMismatch": "Uppsett útgáfa passar ekki við útgáfu skrifborðsforritsins.", + "wsl.onboarding.adding": "Bætir við...", + "help.tabs.toast.ariaLabel": "Við kynnum Tab. Skipuleggðu vinnu þína og virkar lotur með flipa", + "help.tabs.toast.dismiss": "Hunsa upplýsingar Tab", + "help.tabs.title": "Við kynnum Tab", + "help.tabs.description": "Skipuleggðu vinnu þína og virkar lotur með flipa", + "help.tabs.date": "14. júlí", + "help.tabs.introduction": "OpenCode Desktop er nú byggt í kringum flipa.", + "help.tabs.sessions": + "Byrjaðu nýja lotu í flipa eða opnaðu fyrirliggjandi lotu úr einhverju verkefnanna þinna. Opnaðu nýjan flipa þegar þú ert að byrja á einhverju nýju og lokaðu honum þegar þú ert búinn.", + "help.tabs.organize": + "Með því að hafa nokkra flipa opna er auðveldara að skipuleggja virku seturnar þínar. Endurnefndu flipa á eftirminnilegan hátt ef þú ætlar að geyma þá.", + "help.tabs.home": "Þú finnur allar lotur og verkefni á nýja Home skjánum. Með því að velja lotu opnast það í flipa.", + "help.tabs.persistence": "Þegar þú opnar forritið aftur eru fliparnir þínir enn opnir.", + "help.tabs.worktrees": + "Nýja hönnunin styður ekki Git Worktrees ennþá, hún kemur fljótlega. Þannig að ef þú vilt halda áfram að nota fyrra útlitið geturðu skipt á milli útlita í stillingum. Hafðu bara í huga að nýja skipulagið verður varanlegt eftir nokkrar vikur.", + "server.row.noUsername": "ekkert notendanafn", + "dialog.project.edit.title": "Breyta verkefni", + "dialog.project.edit.name": "Nafn", + "dialog.project.edit.icon": "Táknmynd", + "dialog.project.edit.icon.alt": "Verkefnistákn", + "dialog.project.edit.icon.hint": "Smelltu eða dragðu mynd", + "dialog.project.edit.icon.recommended": "Mælt með: 128x128px", + "dialog.project.edit.color": "Litur", + "dialog.project.edit.color.select": "Veldu {{color}} lit", + "dialog.project.edit.worktree.startup": "Ræsingarhandrit vinnusvæðis", + "dialog.project.edit.worktree.startup.description": "Keyrir eftir að nýtt vinnusvæði (worktree) er búið til.", + "dialog.project.edit.worktree.startup.placeholder": "t.d. bun setja upp", + "dialog.releaseNotes.action.getStarted": "Byrjaðu", + "dialog.releaseNotes.action.next": "Næst", + "dialog.releaseNotes.action.hideFuture": "Ekki sýna þetta í framtíðinni", + "dialog.releaseNotes.media.alt": "Gefa út forskoðun", + "context.breakdown.title": "Sundurliðun samhengis", + "context.breakdown.note": + "Áætlað sundurliðun á inntakstáknum. „Annað“ felur í sér skilgreiningar á verkfærum og kostnaður.", + "context.breakdown.system": "Kerfi", + "context.breakdown.user": "Notandi", + "context.breakdown.assistant": "Aðstoðarmaður", + "context.breakdown.tool": "Verkfærasímtöl", + "context.breakdown.other": "Annað", + "context.systemPrompt.title": "Kerfisboð", + "context.rawMessages.title": "Hrá skilaboð", + "context.export.session": "Flytja út setu", + "context.stats.session": "Lota", + "context.stats.messages": "Skilaboð", + "context.stats.provider": "Þjónustuveita", + "context.stats.model": "Líkan", + "context.stats.limit": "Samhengismörk", + "context.stats.totalTokens": "Samtals tákn", + "context.stats.usage": "Notkun", + "context.stats.inputTokens": "Inntaksmerki", + "context.stats.outputTokens": "Úttakstókar", + "context.stats.reasoningTokens": "Röksemdamerki", + "context.stats.cacheTokens": "Skyndiminni tákn (lesa/skrifa)", + "context.stats.userMessages": "Notendaskilaboð", + "context.stats.assistantMessages": "Skilaboð aðstoðarmanns", + "context.stats.totalCost": "Heildarkostnaður", + "context.stats.sessionCreated": "Fundur búin til", + "context.stats.lastActivity": "Síðasta virkni", + "context.usage.tokens": "Tákn", + "context.usage.usage": "Notkun", + "context.usage.cost": "Kostnaður", + "context.usage.clickToView": "Smelltu til að skoða samhengi", + "context.usage.view": "Skoða samhengisnotkun", + "language.en": "ensku", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasilía)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Tungumál", + "toast.language.description": "Skipt yfir í {{language}}", + "toast.theme.title": "Þema skipt", + "toast.scheme.title": "Litasamsetning", + "toast.workspace.enabled.title": "Vinnusvæði virkt", + "toast.workspace.enabled.description": "Mörg vinnutré eru nú sýnd í hliðarstikunni", + "toast.workspace.disabled.title": "Vinnusvæði óvirk", + "toast.workspace.disabled.description": "Aðeins aðalvinnutréð er sýnt í hliðarstikunni", + "toast.permissions.autoaccept.on.title": "Sjálfvirkt samþykki heimilda", + "toast.permissions.autoaccept.on.description": "Leyfibeiðnir verða sjálfkrafa samþykktar", + "toast.permissions.autoaccept.off.title": "Hætti að samþykkja heimildir sjálfkrafa", + "toast.permissions.autoaccept.off.description": "Leyfisbeiðnir þurfa samþykki", + "toast.model.none.title": "Engin gerð valin", + "toast.model.none.description": "Tengdu þjónustuaðila til að draga saman þessa lotu", + "toast.file.loadFailed.title": "Mistókst að hlaða skrá", + "toast.file.listFailed.title": "Mistókst að skrá skrár", + "toast.context.noLineSelection.title": "Ekkert línuval", + "toast.context.noLineSelection.description": "Veldu línusvið í skráarflipa fyrst.", + "toast.session.share.copyFailed.title": "Mistókst að afrita URL á klemmuspjald", + "toast.session.share.success.title": "Fundi deilt", + "toast.session.share.success.description": "Deildu URL afritað á klemmuspjald!", + "toast.session.share.failed.title": "Ekki tókst að deila setu", + "toast.session.share.failed.description": "Villa kom upp þegar lotunni var deilt", + "toast.session.unshare.success.title": "Ekki deilt lotu", + "toast.session.unshare.success.description": "Lokað var við deilingu lotunnar!", + "toast.session.unshare.failed.title": "Mistókst að hætta við deilingu lotu", + "toast.session.unshare.failed.description": "Villa kom upp við að hætta að deila lotunni", + "toast.session.export.success.title": "Session flutt út", + "toast.session.export.success.description": "Vistað lota í {{filename}}", + "toast.session.export.failed.title": "Mistókst að flytja út lotu", + "toast.session.export.failed.description": "Villa kom upp við útflutning á lotunni", + "toast.session.listFailed.title": "Mistókst að hlaða lotum fyrir {{project}}", + "toast.project.reloadFailed.title": "Mistókst að endurhlaða {{project}}", + "toast.update.title": "Uppfærsla í boði", + "toast.update.description": "Ný útgáfa af OpenCode ({{version}}) er nú fáanleg til uppsetningar.", + "toast.update.action.installRestart": "Settu upp og endurræstu", + "toast.update.action.notYet": "Ekki enn", + "error.page.title": "Eitthvað fór úrskeiðis", + "error.page.description": "Villa kom upp við að hlaða forritinu.", + "error.page.description.localServerStartup": "Villa kom upp þegar staðbundinn þjónn var ræstur.", + "error.page.details.label": "Villuupplýsingar", + "error.page.action.restart": "Endurræstu", + "error.page.action.report": "Tilkynna villu", + "error.page.action.reported": "Villa tilkynnt", + "error.page.action.exportLogs": "Flytja út atvikaskrár", + "error.page.action.checking": "Athugar...", + "error.page.action.checkUpdates": "Leitaðu að uppfærslum", + "error.page.action.updateTo": "Uppfærsla í {{version}}", + "error.page.circular": "[Hringbréf]", + "error.page.report.prefix": "Vinsamlegast tilkynnið þessa villu til OpenCode teymisins", + "error.page.report.discord": "á Discord", + "error.page.version": "Útgáfa: {{version}}", + "error.dev.rootNotFound": + "Rótarþáttur fannst ekki. Gleymdirðu að bæta því við index.html þinn? Eða er id eigindin kannski rangt stafsett?", + "error.serverSync.connectFailed": "Gat ekki tengst þjóninum. Er netþjónn í gangi hjá `{{url}}`?", + "error.serverSDK.noServerAvailable": "Enginn netþjónn tiltækur", + "error.serverSDK.serverNotAvailable": "Miðlari ekki tiltækur", + "error.childStore.persistedCacheCreateFailed": "Mistókst að búa til viðvarandi skyndiminni", + "error.childStore.persistedProjectMetadataCreateFailed": "Mistókst að búa til viðvarandi lýsigögn verkefnis", + "error.childStore.persistedProjectIconCreateFailed": "Mistókst að búa til viðvarandi verkefnistákn", + "error.childStore.storeCreateFailed": "Mistókst að búa til verslun", + "directory.error.invalidUrl": "Ógild skrá í URL.", + "error.chain.unknown": "Óþekkt villa", + "error.server.invalidConfiguration": "Ógild stilling", + "error.chain.causedBy": "Orsakast af:", + "error.chain.apiError": "API villa", + "error.chain.status": "Staða: {{status}}", + "error.chain.retryable": "Hægt að reyna aftur: {{retryable}}", + "error.chain.responseBody": "Svarhluti:\n{{body}}", + "error.chain.didYouMean": "Áttirðu við: {{suggestions}}", + "error.chain.modelNotFound": "Gerð fannst ekki: {{provider}}/{{model}}", + "error.chain.checkConfig": "Athugaðu stillingar (opencode.json) þjónustuveitu/módelheiti", + "error.chain.mcpFailed": "MCP þjónn „{{name}}“ mistókst. Athugið, OpenCode styður ekki MCP auðkenningu ennþá.", + "error.chain.providerAuthFailed": "Auðkenning veitu mistókst ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + "Mistókst að frumstilla þjónustuveituna „{{provider}}“. Athugaðu skilríki og stillingar.", + "error.chain.configJsonInvalid": "Stillingarskrá á {{path}} er ekki gild JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Stillingarskrá á {{path}} er ekki gild JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Skráin „{{dir}}“ í {{path}} er ekki gild. Endurnefna möppuna í "{{suggestion}}" eða fjarlægðu hana. Þetta er algeng innsláttarvilla.', + "error.chain.configFrontmatterError": "Mistókst að flokka frammál í {{path}}:\n{{message}}", + "error.chain.configInvalid": "Stillingarskrá á {{path}} er ógild", + "error.chain.configInvalidWithMessage": "Stillingarskrá á {{path}} er ógild: {{message}}", + "notification.permission.title": "Leyfi krafist", + "notification.permission.description": "{{sessionTitle}} í {{projectName}} þarf leyfi", + "notification.question.title": "Spurning", + "notification.question.description": "{{sessionTitle}} í {{projectName}} hefur spurningu", + "notification.action.goToSession": "Farðu á fund", + "notification.session.responseReady.title": "Svar tilbúið", + "notification.session.error.title": "Lotuvilla", + "notification.session.error.fallbackDescription": "Villa kom upp", + "home.recentProjects": "Nýleg verkefni", + "home.empty.title": "Engin nýleg verkefni", + "home.empty.description": "Byrjaðu með því að opna staðbundið verkefni", + "home.title": "Heim", + "home.projects": "Verkefni", + "home.project.add": "Bæta við verkefni", + "home.recentlyClosed": "Nýlega lokað", + "home.server.collapse": "Dragðu saman netþjónaverkefni", + "home.server.expand": "Stækkaðu netþjónaverkefni", + "home.sessions.search.placeholder": "Leitarlotur", + "home.sessions.search.placeholder.scoped": "Leitarlotur í {{scope}}", + "home.sessions.search.sessions": "Fundir", + "home.sessions.search.noResults": "Engar lotur fundust fyrir {{query}}", + "home.sessions.empty": "Ekkert hérna ennþá", + "home.sessions.empty.description": "Búðu til lotu til að byrja", + "home.sessions.group.today": "Í dag", + "home.sessions.group.yesterday": "Í gær", + "home.sessions.group.older": "Eldri", + "home.providerTip": "Tengstu við 75+ veitendur til að nota aðrar gerðir, þar á meðal Claude, GPT, Gemini, osfrv", + "session.tab.session": "Lota", + "session.tab.review": "Upprifjun", + "session.tab.context": "Samhengi", + "session.tab.unknown": "Óþekkt lota", + "session.panel.reviewAndFiles": "Yfirferð og skrár", + "session.error.notFound": "Þessi lota finnst ekki", + "session.error.notFound.description": "Þessi flipi bendir á lotu sem er ekki lengur til á þessum netþjóni.", + "session.error.notFound.closeTab": "Lokaðu Tab", + "session.error.serverConnection": "Get ekki tengst þessum þjóni", + "session.review.filesChanged": "Skráum breytt {{count}}", + "session.review.change.one": "Breyta", + "session.review.change.other": "Breytingar", + "session.review.loadingChanges": "Hleður breytingum...", + "session.review.empty": "Engar breytingar á þessu þingi ennþá", + "session.review.noVcs": "Ekkert Git útgáfustýringarkerfi fannst, breytingar ekki birtar", + "session.review.noVcs.createGit.title": "Búðu til Git geymslu", + "session.review.noVcs.createGit.description": "Fylgstu með, skoðaðu og afturkallaðu breytingar á þessu verkefni", + "session.review.noVcs.createGit.actionLoading": "Býr til Git geymslu...", + "session.review.noVcs.createGit.action": "Búðu til Git geymslu", + "session.review.noSnapshot": + "Slökkt er á skyndimyndarakningu í stillingum, þannig að lotubreytingar eru ekki tiltækar", + "session.review.noChanges": "Engar breytingar", + "session.review.noUncommittedChanges": "Engar óbundnar breytingar ennþá", + "session.review.noBranchChanges": "Engar greinarbreytingar ennþá", + "session.files.selectToOpen": "Veldu skrá til að opna", + "session.files.all": "Allar skrár", + "session.files.empty": "Engar skrár", + "session.files.binaryContent": "Tvöfaldur skrá (ekki hægt að birta efni)", + "session.messages.renderEarlier": "Sendu fyrri skilaboð", + "session.messages.loadingEarlier": "Hleður fyrri skilaboðum...", + "session.messages.loadEarlier": "Hlaða fyrri skilaboðum", + "session.messages.loading": "Hleður skilaboð...", + "session.messages.jumpToLatest": "Farðu í nýjasta", + "session.context.addToContext": "Bættu {{selection}} við samhengi", + "session.todo.title": "Verkefnalisti", + "session.todo.collapse": "Hrun", + "session.todo.expand": "Stækkaðu", + "session.todo.progress": "{{done}} af {{total}} verkefnum lokið", + "session.question.progress": "{{current}} af {{total}} spurningum", + "session.question.minimize": "Lágmarka spurninguna", + "session.question.restore": "Endurheimta spurningu", + "session.question.pending.one": "{{count}} spurning í bið", + "session.question.pending.other": "{{count}} spurningar í bið", + "session.followupDock.summary.one": "{{count}} skilaboð í biðröð", + "session.followupDock.summary.other": "{{count}} skilaboð í biðröð", + "session.followupDock.sendNow": "Sendu núna", + "session.followupDock.edit": "Breyta", + "session.followupDock.collapse": "Draga saman skilaboð í biðröð", + "session.followupDock.expand": "Stækkaðu skilaboð í biðröð", + "session.revertDock.summary.one": "{{count}} afturkallað skilaboð", + "session.revertDock.summary.other": "{{count}} afturkallað skilaboð", + "session.revertDock.collapse": "Dragðu saman skilaboð sem eru afturkölluð", + "session.revertDock.expand": "Stækka afturkölluð skilaboð", + "session.revertDock.restore": "Endurheimta skilaboð", + "session.new.title": "Byggja hvað sem er", + "session.new.project.new": "Nýtt verkefni", + "session.new.project.search": "Leita að verkefnum", + "session.new.project.add": "Bæta við verkefni", + "session.new.worktree.main": "Aðalútibú", + "session.new.worktree.mainWithBranch": "Aðalútibú ({{branch}})", + "session.new.worktree.create": "Búðu til nýtt vinnutré", + "session.new.workspace.runIn": "Keyra setu í", + "session.new.workspace.triggerLocal": "Staðbundið", + "session.new.workspace.local": "Staðbundin geymsla", + "session.new.workspace.existing": "Vinnurými…", + "session.new.git.none": "Engin Git", + "session.new.lastModified": "Síðast breytt", + "session.header.search.placeholder": "Leitaðu að {{project}}", + "session.header.searchFiles": "Leitaðu að skrám", + "session.header.openIn": "Opið inn", + "session.header.open.action": "Opnaðu {{app}}", + "session.header.open.ariaLabel": "Opið í {{app}}", + "session.header.open.menu": "Opna valkosti", + "session.header.open.copyPath": "Afrita slóð", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "Skráastjóri", + "session.header.reveal.finder": "Sýna í Finder", + "session.header.reveal.fileExplorer": "Sýna í File Explorer", + "session.header.reveal.containingFolder": "Opnaðu möppuna sem inniheldur", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Skel", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Staða", + "status.popover.ariaLabel": "Stillingar miðlara", + "status.popover.tab.servers": "Netþjónar", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Viðbætur", + "status.popover.action.manageServers": "Stjórna netþjónum", + "session.share.popover.title": "Birta á vefnum", + "session.share.popover.description.shared": + "Þessi seta er opinber á vefnum. Allir sem hafa tengilinn geta opnað hana.", + "session.share.popover.description.unshared": + "Deildu setunni opinberlega á vefnum. Allir sem hafa tengilinn geta opnað hana.", + "session.share.action.share": "Deila", + "session.share.action.publish": "Birta", + "session.share.action.publishing": "Birtir...", + "session.share.action.unpublish": "Afbirta", + "session.share.action.unpublishing": "Hættir að birta...", + "session.share.action.view": "Skoða", + "session.share.copy.copied": "Afritað", + "session.share.copy.copyLink": "Afritaðu tengil", + "lsp.tooltip.none": "Engir LSP netþjónar", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Hleður tilkynningu...", + "terminal.loading": "Hleður útstöð...", + "terminal.title": "Skel", + "terminal.title.numbered": "Skjáhermir {{number}}", + "terminal.close": "Lokaðu flugstöðinni", + "terminal.connectionLost.title": "Tenging rofin", + "terminal.connectionLost.abnormalClose": "WebSocket lokað óeðlilega: {{code}}", + "terminal.connectionLost.description": "Tengingin var rofin. Þetta getur gerst þegar þjónninn endurræsir sig.", + "terminal.connectTicket.csrfError": + "PTY tengimiði hafnað af uppruna eða CSRF ávísun. Athugaðu þjóninn CORS stillingar.", + "terminal.connectTicket.statusError": "PTY tengimiði mistókst með {{status}}", + "titlebar.update": "Uppfærsla", + "titlebar.updateVersion": "Uppfærðu {{version}}", + "common.closeTab": "Loka flipa", + "common.dismiss": "Segja frá", + "common.moreCountSuffix": "(+{{count}} meira)", + "common.requestFailed": "Beiðni mistókst", + "common.moreOptions": "Fleiri valkostir", + "common.learnMore": "Lærðu meira", + "common.rename": "Endurnefna", + "common.export": "Útflutningur", + "common.reset": "Endurstilla", + "common.archive": "Skjalasafn", + "common.delete": "Eyða", + "common.close": "Loka", + "common.edit": "Breyta", + "common.loadMore": "Hlaða meira", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Eyða", + "common.key.home": "Heim", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Settu inn", + "common.unknown": "óþekkt", + "common.time.justNow": "Bara núna", + "common.time.minutesAgo.short": "{{count}}m síðan", + "common.time.hoursAgo.short": "{{count}}h síðan", + "common.time.daysAgo.short": "{{count}}d síðan", + "sidebar.menu.toggle": "Skipta um valmynd", + "sidebar.nav.projectsAndSessions": "Verkefni og setur", + "sidebar.settings": "Stillingar", + "sidebar.help": "Hjálp", + "sidebar.workspaces.enable": "Virkjaðu vinnusvæði", + "sidebar.workspaces.disable": "Slökktu á vinnusvæðum", + "sidebar.gettingStarted.title": "Að byrja", + "sidebar.gettingStarted.line1": "OpenCode inniheldur ókeypis gerðir svo þú getir byrjað strax.", + "sidebar.gettingStarted.line2": + "Tengdu hvaða þjónustuaðila sem er til að nota módel, inc. Claude, GPT, Gemini osfrv.", + "sidebar.project.recentSessions": "Nýlegar setur", + "sidebar.project.viewAllSessions": "Skoða allar lotur", + "sidebar.project.clearNotifications": "Hreinsaðu tilkynningar", + "sidebar.empty.title": "Engin verkefni opin", + "sidebar.empty.description": "Opnaðu verkefni til að byrja", + "debugBar.ariaLabel": "Þróunarárangursgreiningar", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Síðasta lokið leiðarskipti sem snertir lotusíðu, mæld frá byrjun beinis þar til fyrsta málningin eftir að hún sest.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Rámar á sekúndu rúllandi síðustu 5 sekúndur.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Versti rammatími síðustu 5 sekúndur.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Rammar yfir 32ms á síðustu 5 sekúndum.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Útilokaður tími og löng verkefni á síðustu 5 sekúndum. Hámarksverkefni: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Versta inntakseinkun sem hefur sést á síðustu 5 sekúndum.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "Áætluð lengd víxlverkunar síðustu 5 sekúndur. Þetta er INP-líkt, ekki opinberu Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Uppsöfnuð skipulagsbreyting fyrir núverandi líftíma appsins.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Notað JS hrúga vs hrúgutakmark. Aðeins Chromium.", + "debugBar.mem.tip": "Notað JS hrúga vs hrúgutakmark. {{used}} af {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Þvingaðu fókusstíla á alla gagnvirka þætti", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Þvingaðu stefnu útlits forritsins í heild sinni án þess að breyta valnu tungumáli", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode skjáborð", + "settings.section.desktop": "Skrifborð", + "settings.section.server": "Netþjónn", + "settings.tab.general": "Almennt", + "settings.tab.shortcuts": "Flýtileiðir", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL samþætting", + "settings.desktop.wsl.description": "Keyrðu OpenCode netþjóninn inni í WSL á Windows.", + "settings.general.section.appearance": "Útlit", + "settings.general.section.advanced": "Ítarlegri", + "settings.general.section.notifications": "Kerfistilkynningar", + "settings.general.section.updates": "Uppfærslur", + "settings.general.section.sounds": "Hljóðbrellur", + "settings.general.section.feed": "Fæða", + "settings.general.section.display": "Skjár", + "settings.general.row.language.title": "Tungumál", + "settings.general.row.language.description": "Breyttu skjátungumálinu fyrir OpenCode", + "settings.general.row.shell.title": "Skel skjáhermis", + "settings.general.row.shell.description": "Skelin sem skjáhermirinn og verkfæri fulltrúans nota", + "settings.general.row.shell.autoDefault": "Sjálfvirkt (sjálfgefið)", + "settings.general.row.shell.terminalOnly": "eingöngu flugstöð", + "settings.general.row.appearance.title": "Útlit", + "settings.general.row.appearance.description": "Sérsníddu hvernig OpenCode lítur út á tækinu þínu", + "settings.general.row.colorScheme.title": "Litasamsetning", + "settings.general.row.colorScheme.description": "Veldu hvort OpenCode fylgir kerfis-, ljósu eða dökku þema", + "settings.general.row.theme.title": "Þema", + "settings.general.row.theme.description": "Sérsníddu hvernig OpenCode er þema.", + "settings.general.row.font.title": "Kóða leturgerð", + "settings.general.row.font.description": "Sérsníddu leturgerðina sem notuð er í kóðablokkum", + "settings.general.row.terminalFont.title": "Leturgerð skjáhermis", + "settings.general.row.terminalFont.description": "Sérsníddu leturgerðina sem notuð er í flugstöðinni", + "settings.general.row.uiFont.title": "UI leturgerð", + "settings.general.row.uiFont.description": "Sérsníddu leturgerðina sem notuð er í öllu viðmótinu", + "settings.general.row.followup.title": "Eftirfylgnihegðun", + "settings.general.row.followup.description": "Veldu hvort eftirfylgnibeiðnir stýra strax eða bíða í biðröð", + "settings.general.row.followup.option.queue": "Biðröð", + "settings.general.row.followup.option.steer": "Stýra", + "settings.general.row.showFileTree.title": "Skráartré", + "settings.general.row.showFileTree.description": "Sýndu skráartréspjaldið í lotum", + "settings.general.row.showNavigation.title": "Leiðsögustýringar", + "settings.general.row.showNavigation.description": "Sýndu aftur- og áframhnappana á titilstikunni á skjáborðinu", + "settings.general.row.showSearch.title": "Skipunarspjald", + "settings.general.row.showSearch.description": "Sýndu leitar- og skipanaspjaldhnappinn á titilstikunni", + "settings.general.row.showTerminal.title": "Skel", + "settings.general.row.showTerminal.description": "Sýndu flugstöðvarhnappinn á titilstikunni á skjáborðinu", + "settings.general.row.showStatus.title": "Staða netþjóns", + "settings.general.row.showStatus.description": "Sýndu stöðuhnapp netþjónsins á titilstikunni", + "settings.general.row.mobileTitlebarBottom.title": "Botnleiðsögn", + "settings.general.row.mobileTitlebarBottom.description": + "Settu titilstikuna og lotuflipana neðst á skjánum á farsímanum", + "settings.general.row.showCustomAgents.title": "Sýna fulltrúa", + "settings.general.row.showCustomAgents.description": + "Skiptu á milli fulltrúa í innsláttarreitnum. Þegar þeir eru faldir er Build-fulltrúinn sjálfgefinn.", + "settings.general.row.reasoningSummaries.title": "Sýndu samantektir um rökstuðning", + "settings.general.row.reasoningSummaries.description": "Birta rökstuðningssamantektir líkana á tímalínunni", + "settings.general.row.shellToolPartsExpanded.title": "Stækkaðu hluta skeljaverkfæra", + "settings.general.row.shellToolPartsExpanded.description": "Sýna hluta skelverkfæra stækkað sjálfgefið á tímalínunni", + "settings.general.row.editToolPartsExpanded.title": "Stækkaðu breytingaverkfærahluta", + "settings.general.row.editToolPartsExpanded.description": + "Sýna breytinga-, skrifa- og plástraverkfæri hluta sjálfgefið stækkað á tímalínunni", + "settings.general.row.newInterface.title": "Nýtt skipulag", + "settings.general.row.newInterface.badge": "Nýtt", + "settings.general.row.newInterface.description": + "Notaðu nýju flipana og heimilisútlitið. Skiptu á milli útlita í takmarkaðan tíma.", + "settings.general.row.newInterfaceNotice.title": "Þú ert núna að nota nýtt skipulag", + "settings.general.row.newInterfaceNotice.description": "Fyrra skipulagið er ekki lengur tiltækt", + "settings.general.row.newInterfaceNotice.dismiss": "Segja frá", + "settings.general.row.pinchZoom.title": "Klíptu til að þysja", + "settings.general.row.pinchZoom.description": "Leyfðu að klípa og Ctrl-fletta bendingar til að þysja", + "settings.general.row.wayland.title": "Notaðu innfæddan Wayland", + "settings.general.row.wayland.description": "Slökktu á X11 fallback á Wayland. Krefst endurræsingar.", + "settings.general.row.wayland.tooltip": + "Á Linux með skjám með blönduðum hressingarhraða getur innfæddur Wayland verið stöðugri.", + "settings.general.row.releaseNotes.title": "Útgáfuskýrslur", + "settings.general.row.releaseNotes.description": "Sýna hvað er nýtt sprettiglugga eftir uppfærslur", + "settings.updates.row.startup.title": "Leitaðu að uppfærslum við ræsingu", + "settings.updates.row.startup.description": "Athugaðu sjálfkrafa fyrir uppfærslur þegar OpenCode ræsir", + "settings.updates.row.check.title": "Leitaðu að uppfærslum", + "settings.updates.row.check.description": "Athugaðu handvirkt fyrir uppfærslur og settu upp ef það er tiltækt", + "settings.updates.action.checkNow": "Athugaðu núna", + "settings.updates.action.checking": "Athugar...", + "settings.updates.action.downloading": "Sækir...", + "settings.updates.action.installing": "Setur upp...", + "settings.updates.toast.latest.title": "Þú ert uppfærður", + "settings.updates.toast.latest.description": "Þú ert að keyra nýjustu útgáfuna af OpenCode.", + "sound.option.none": "Engin", + "sound.option.alert01": "Viðvörun 01", + "sound.option.alert02": "Viðvörun 02", + "sound.option.alert03": "Viðvörun 03", + "sound.option.alert04": "Viðvörun 04", + "sound.option.alert05": "Viðvörun 05", + "sound.option.alert06": "Viðvörun 06", + "sound.option.alert07": "Viðvörun 07", + "sound.option.alert08": "Viðvörun 08", + "sound.option.alert09": "Viðvörun 09", + "sound.option.alert10": "Viðvörun 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nei 01", + "sound.option.nope02": "Nei 02", + "sound.option.nope03": "Nei 03", + "sound.option.nope04": "Nei 04", + "sound.option.nope05": "Nei 05", + "sound.option.nope06": "Nei 06", + "sound.option.nope07": "Nei 07", + "sound.option.nope08": "Nei 08", + "sound.option.nope09": "Nei 09", + "sound.option.nope10": "Nei 10", + "sound.option.nope11": "Nei 11", + "sound.option.nope12": "Nei 12", + "sound.option.yup01": "Já 01", + "sound.option.yup02": "Já 02", + "sound.option.yup03": "Já 03", + "sound.option.yup04": "Já 04", + "sound.option.yup05": "Já 05", + "sound.option.yup06": "Já 06", + "settings.general.notifications.agent.title": "Fulltrúi", + "settings.general.notifications.agent.description": + "Sýna kerfistilkynningu þegar fulltrúi lýkur verki eða þarfnast athygli", + "settings.general.notifications.permissions.title": "Heimildir", + "settings.general.notifications.permissions.description": "Sýna kerfistilkynningu þegar leyfi er krafist", + "settings.general.notifications.errors.title": "Villur", + "settings.general.notifications.errors.description": "Sýna kerfistilkynningu þegar villa kemur upp", + "settings.general.sounds.agent.title": "Fulltrúi", + "settings.general.sounds.agent.description": "Spila hljóð þegar fulltrúinn lýkur verki eða þarfnast athygli", + "settings.general.sounds.permissions.title": "Heimildir", + "settings.general.sounds.permissions.description": "Spilaðu hljóð þegar leyfi er krafist", + "settings.general.sounds.errors.title": "Villur", + "settings.general.sounds.errors.description": "Spilaðu hljóð þegar villa kemur upp", + "settings.shortcuts.title": "Flýtivísar", + "settings.shortcuts.reset.button": "Endurstilla í sjálfgefna stillingar", + "settings.shortcuts.reset.toast.title": "Flýtivísar endurstillt", + "settings.shortcuts.reset.toast.description": "Flýtivísar hafa verið endurstilltir á sjálfgefnar stillingar.", + "settings.shortcuts.conflict.title": "Flýtileið þegar í notkun", + "settings.shortcuts.conflict.description": "{{keybind}} er þegar úthlutað til {{titles}}.", + "settings.shortcuts.unassigned": "Óúthlutað", + "settings.shortcuts.pressKeys": "Ýttu á takka", + "settings.shortcuts.search.placeholder": "Leitarflýtivísar", + "settings.shortcuts.search.empty": "Engar flýtileiðir fundust", + "settings.shortcuts.group.general": "Almennt", + "settings.shortcuts.group.session": "Lota", + "settings.shortcuts.group.navigation": "Leiðsögn", + "settings.shortcuts.group.modelAndAgent": "Líkan og fulltrúi", + "settings.shortcuts.group.terminal": "Skel", + "settings.shortcuts.group.prompt": "Kvaðning", + "settings.providers.title": "Veitendur", + "settings.providers.description": "Stillingar þjónustuveitunnar verða stillanlegar hér.", + "settings.providers.section.connected": "Tengdar veitendur", + "settings.providers.connected.empty": "Engar tengdar veitendur", + "settings.providers.connected.environmentDescription": "Tengdur frá umhverfisbreytum þínum", + "settings.providers.section.popular": "Vinsælir veitendur", + "settings.providers.custom.description": "Bættu við OpenAI-samhæfðum veitanda með grunni URL.", + "settings.providers.tag.environment": "Umhverfi", + "settings.providers.tag.config": "Uppsetning", + "settings.providers.tag.custom": "Sérsniðin", + "settings.providers.tag.other": "Annað", + "settings.models.title": "Fyrirmyndir", + "settings.models.description": "Líkanstillingar verða stillanlegar hér.", + "settings.agents.title": "Umboðsmenn", + "settings.agents.description": "Umboðsmannsstillingar verða stillanlegar hér.", + "settings.commands.title": "Skipanir", + "settings.commands.description": "Skipunarstillingar verða stillanlegar hér.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP stillingar verða stillanlegar hér.", + "settings.permissions.title": "Heimildir", + "settings.permissions.description": "Stjórnaðu því hvaða verkfæri þjónninn getur notað sjálfgefið.", + "settings.permissions.section.tools": "Verkfæri", + "settings.permissions.toast.updateFailed.title": "Mistókst að uppfæra heimildir", + "settings.permissions.action.allow": "Leyfa", + "settings.permissions.action.ask": "Spurðu", + "settings.permissions.action.deny": "Neita", + "settings.permissions.tool.read.title": "Lestu", + "settings.permissions.tool.read.description": "Að lesa skrá (passar við skráarslóðina)", + "settings.permissions.tool.edit.title": "Breyta", + "settings.permissions.tool.edit.description": "Breyttu skrám, þar með talið breytingum, skrifum og plástra", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Passaðu skrár með því að nota glob mynstur", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Leitaðu að innihaldi skráar með því að nota reglulegar tjáningar", + "settings.permissions.tool.list.title": "Listi", + "settings.permissions.tool.list.description": "Listaðu skrár í möppu", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Keyra skel skipanir", + "settings.permissions.tool.task.title": "Verkefni", + "settings.permissions.tool.task.description": "Ræstu undir-umboðsmenn", + "settings.permissions.tool.skill.title": "Færni", + "settings.permissions.tool.skill.description": "Hlaða hæfileika með nafni", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Keyra fyrirspurnir um tungumálaþjón", + "settings.permissions.tool.todowrite.title": "Todo Skrifa", + "settings.permissions.tool.todowrite.description": "Uppfærðu verkefnalistann", + "settings.permissions.tool.webfetch.title": "Vefur sækja", + "settings.permissions.tool.webfetch.description": "Sæktu efni úr URL", + "settings.permissions.tool.websearch.title": "Vefleit", + "settings.permissions.tool.websearch.description": "Leitaðu á vefnum", + "settings.permissions.tool.external_directory.title": "Ytri skráarskrá", + "settings.permissions.tool.external_directory.description": "Fáðu aðgang að skrám utan verkefnaskránnar", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Finndu endurtekin verkfærasímtöl með eins inntak", + "session.delete.failed.title": "Ekki tókst að eyða lotu", + "session.delete.title": "Eyða setu", + "session.delete.confirm": 'Eyða setunni "{{name}}"?', + "session.delete.button": "Eyða setu", + "workspace.new": "Nýtt vinnurými", + "workspace.type.local": "staðbundið", + "workspace.type.sandbox": "sandkassi", + "workspace.create.failed.title": "Mistókst að búa til vinnusvæði", + "workspace.delete.failed.title": "Mistókst að eyða vinnusvæði", + "workspace.resetting.title": "Núllstillir vinnusvæði", + "workspace.resetting.description": "Þetta gæti tekið eina mínútu.", + "workspace.reset.failed.title": "Mistókst að endurstilla vinnusvæði", + "workspace.reset.success.title": "Vinnusvæði endurstillt", + "workspace.reset.success.description": "Vinnusvæði passar nú við sjálfgefna útibúið.", + "workspace.error.stillPreparing": "Vinnusvæði er enn í undirbúningi", + "workspace.status.checking": "Leitar að ósameinuðum breytingum...", + "workspace.status.error": "Ekki hægt að staðfesta git stöðu.", + "workspace.status.clean": "Engar ósamræmdar breytingar fundust.", + "workspace.status.dirty": "Ósamræmdar breytingar fundust á þessu vinnusvæði.", + "workspace.delete.title": "Eyða vinnusvæði", + "workspace.delete.confirm": 'Eyða vinnusvæðinu "{{name}}"?', + "workspace.delete.button": "Eyða vinnusvæði", + "workspace.reset.title": "Endurstilla vinnusvæði", + "workspace.reset.confirm": 'Endurstilla vinnusvæði "{{name}}"?', + "workspace.reset.button": "Endurstilla vinnusvæði", + "workspace.reset.archived.none": "Engar virkar lotur verða settar í geymslu.", + "workspace.reset.archived.one": "1 seta verður sett í geymslu.", + "workspace.reset.archived.many": "{{count}} setur verða settar í geymslu.", + "workspace.reset.note": "Þetta mun endurstilla vinnusvæðið til að passa við sjálfgefna útibúið.", + "dialog.usageExceeded.dontShowAgain": "Ekki sýna aftur", +} diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts new file mode 100644 index 0000000000000000000000000000000000000000..b96e8b544b2bae7a3f439df31a8fb452fe5b1e57 --- /dev/null +++ b/packages/app/src/i18n/ja.ts @@ -0,0 +1,1169 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ファイル", + "desktop.menu.edit": "編集", + "desktop.menu.view": "表示", + "desktop.menu.go": "移動", + "desktop.menu.window": "ウインドウ", + "desktop.menu.help": "ヘルプ", + "desktop.menu.checkForUpdates": "アップデートを確認...", + "desktop.menu.settings": "設定", + "desktop.menu.reloadWebview": "Webビューを再読み込み", + "desktop.menu.restart": "再起動", + "desktop.menu.exportLogs": "ログをエクスポート...", + "desktop.menu.newSession": "新しいセッション", + "desktop.menu.openProject": "プロジェクトを開く...", + "desktop.menu.newWindow": "新しいウインドウ", + "desktop.menu.closeWindow": "ウインドウを閉じる", + "desktop.menu.undo": "取り消す", + "desktop.menu.redo": "やり直す", + "desktop.menu.cut": "切り取り", + "desktop.menu.copy": "コピー", + "desktop.menu.paste": "ペースト", + "desktop.menu.delete": "削除", + "desktop.menu.selectAll": "すべてを選択", + "desktop.menu.toggleSidebar": "サイドバーの表示を切り替える", + "desktop.menu.toggleTerminal": "ターミナルの表示を切り替える", + "desktop.menu.toggleFileTree": "ファイルツリーの表示を切り替える", + "desktop.menu.reload": "再読み込み", + "desktop.menu.toggleDeveloperTools": "開発者ツールの表示を切り替える", + "desktop.menu.actualSize": "実際のサイズ", + "desktop.menu.zoomIn": "拡大", + "desktop.menu.zoomOut": "縮小", + "desktop.menu.toggleFullScreen": "フルスクリーン表示を切り替える", + "desktop.menu.back": "戻る", + "desktop.menu.forward": "進む", + "desktop.menu.previousSession": "前のセッション", + "desktop.menu.nextSession": "次のセッション", + "desktop.menu.previousProject": "前のプロジェクト", + "desktop.menu.nextProject": "次のプロジェクト", + "desktop.menu.minimize": "最小化", + "desktop.menu.maximize": "最大化", + "desktop.menu.documentation": "OpenCodeドキュメント", + "desktop.menu.supportForum": "サポートフォーラム", + "desktop.menu.shareFeedback": "フィードバックを送信", + "desktop.menu.reportBug": "バグを報告", + "desktop.menu.ariaLabel": "OpenCodeメニュー", + + "desktop.updater.dialog.checkFailed.message": "アップデートを確認できませんでした。", + "desktop.updater.dialog.checkFailed.title": "アップデートエラー", + "desktop.updater.dialog.upToDate.message": "最新の状態です。", + "desktop.updater.dialog.upToDate.title": "アップデートはありません", + "desktop.updater.dialog.ready.message": "アップデート{{version}}をダウンロードしました。今すぐ再起動しますか?", + "desktop.updater.dialog.ready.title": "アップデートの準備ができました", + "desktop.updater.dialog.restart": "再起動", + "desktop.updater.dialog.later": "後で", + + "desktop.recovery.action.relaunch": "再起動", + "desktop.recovery.action.exportLogs": "ログをエクスポート", + "desktop.recovery.action.keepWaiting": "待機を続ける", + "desktop.recovery.action.quit": "終了", + "desktop.recovery.loadFailed": "OpenCodeを読み込めませんでした", + "desktop.recovery.terminated": "OpenCodeのウインドウが予期せず終了しました", + "desktop.recovery.unresponsive": "OpenCodeが応答していません", + "desktop.recovery.unresponsive.detail": "アプリを再起動するか、ログを開くか、待機を続けることができます。", + "desktop.recovery.loadFailed.detail": "ウインドウ: {{window}}\nURL: {{url}}\nエラー: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "ウインドウ: {{window}}\n理由: {{reason}}\nコード: {{code}}", + "desktop.recovery.unknown": "<不明>", + + "desktop.dialog.chooseFolder": "フォルダを選択", + "desktop.dialog.chooseFile": "ファイルを選択", + "desktop.dialog.saveFile": "ファイルを保存", + "desktop.dialog.files": "ファイル", + "desktop.server.local": "ローカルサーバー", + + "desktop.wsl.error.windowsOnly": "WSLはWindowsでのみ利用できます", + "desktop.wsl.error.unavailable": "WSLを利用できません", + "desktop.wsl.error.listInstalled": "インストール済みのWSLディストリビューションの一覧を取得できませんでした", + "desktop.wsl.error.listOnline": "オンラインのWSLディストリビューションの一覧を取得できませんでした", + "desktop.wsl.error.executeDistro": "ディストリビューションでコマンドを実行できません", + "desktop.wsl.error.installWsl": "WSLのインストールに失敗しました", + "desktop.wsl.error.installDistro": "ディストリビューションのインストールに失敗しました: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCodeのインストールに失敗しました", + "desktop.wsl.error.alreadyAdded": "{{distro}}はすでに追加されています", + "desktop.wsl.error.opencodeMissing": "このディストリビューションにはopencodeがインストールされていません", + "desktop.wsl.error.opencodeCannotRun": "opencodeはインストールされていますが、実行できませんでした", + "desktop.wsl.error.opencodeNotInstalled": "{{distro}}にはOpenCodeがインストールされていません", + "desktop.wsl.error.updateVersion": + "OpenCodeのアップデートは完了しましたが、{{distro}}から報告されたバージョンはまだ{{installed}}です。想定されるバージョンは{{expected}}です", + "desktop.wsl.error.noVersion": "バージョンなし", + "desktop.wsl.error.serverExited": "WSLサーバーが起動後に終了しました (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSLサーバーが正常に稼働する前に終了しました (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}}のSidecarのヘルスチェックが{{timeout}}ms後にタイムアウトしました", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}}が{{timeout}}ms後にタイムアウトしました", + "desktop.wsl.error.failedPort": "ポートを取得できませんでした", + + "desktop.picker.error.notSelected": "ピッカーでファイルが選択されませんでした", + "desktop.picker.error.sizeLimit": "選択した添付ファイルが上限の{{limit}} MBを超えています", + + "command.category.suggested": "おすすめ", + "command.category.view": "表示", + "command.category.project": "プロジェクト", + "command.category.provider": "プロバイダー", + "command.category.server": "サーバー", + "command.category.session": "セッション", + "command.category.theme": "テーマ", + "command.category.language": "言語", + "command.category.file": "ファイル", + "command.category.context": "コンテキスト", + "command.category.terminal": "ターミナル", + "command.category.model": "モデル", + "command.category.mcp": "MCP", + "command.category.agent": "エージェント", + "command.category.permissions": "権限", + "command.category.workspace": "ワークスペース", + "command.category.settings": "設定", + "command.logs.export": "ログをエクスポート", + "theme.scheme.system": "システム", + "theme.scheme.light": "ライト", + "theme.scheme.dark": "ダーク", + "command.sidebar.toggle": "サイドバーの切り替え", + "command.project.open": "プロジェクトを開く", + "command.project.previous": "前のプロジェクト", + "command.project.next": "次のプロジェクト", + "command.project.index": "プロジェクト{{index}}に切り替え", + "command.provider.connect": "プロバイダーに接続", + "command.server.switch": "サーバーの切り替え", + "command.settings.open": "設定を開く", + "command.session.previous": "前のセッション", + "command.session.next": "次のセッション", + "command.session.previous.unseen": "前の未読セッション", + "command.session.next.unseen": "次の未読セッション", + "command.session.archive": "セッションをアーカイブ", + "command.palette": "コマンドパレット", + "command.theme.cycle": "テーマの切り替え", + "command.theme.set": "テーマを使用: {{theme}}", + "command.theme.scheme.cycle": "配色の切り替え", + "command.theme.scheme.set": "配色を使用: {{scheme}}", + "command.language.cycle": "言語の切り替え", + "command.language.set": "言語を使用: {{language}}", + "command.session.new": "新しいセッション", + "command.file.open": "ファイルを開く", + "command.tab.close": "タブを閉じる", + "command.tab.reopenClosed": "閉じたタブを再度開く", + "command.context.addSelection": "選択範囲をコンテキストに追加", + "command.context.addSelection.description": "現在のファイルから選択した行を追加", + "command.input.focus": "入力欄にフォーカス", + "command.terminal.toggle": "ターミナルの切り替え", + "command.fileTree.toggle": "ファイルツリーを切り替え", + "command.review.toggle": "レビューの切り替え", + "command.terminal.new": "新しいターミナル", + "command.terminal.new.description": "新しいターミナルタブを作成", + "command.steps.toggle": "ステップの切り替え", + "command.steps.toggle.description": "現在のメッセージのステップを表示または非表示", + "command.message.previous": "前のメッセージ", + "command.message.previous.description": "前のユーザーメッセージに移動", + "command.message.next": "次のメッセージ", + "command.message.next.description": "次のユーザーメッセージに移動", + "command.model.choose": "モデルを選択", + "command.model.choose.description": "別のモデルを選択", + "command.mcp.toggle": "MCPの切り替え", + "command.mcp.toggle.description": "MCPを切り替える", + "command.agent.cycle": "エージェントの切り替え", + "command.agent.cycle.description": "次のエージェントに切り替え", + "command.agent.cycle.reverse": "エージェントを逆順に切り替え", + "command.agent.cycle.reverse.description": "前のエージェントに切り替え", + "command.model.variant.cycle": "思考レベルの切り替え", + "command.model.variant.cycle.description": "次の思考レベルに切り替え", + "command.prompt.mode.shell": "シェル", + "command.prompt.mode.normal": "プロンプト", + "command.permissions.autoaccept.enable": "権限を自動承認する", + "command.permissions.autoaccept.disable": "権限の自動承認を停止する", + "command.workspace.toggle": "ワークスペースを切り替え", + "command.workspace.toggle.description": "サイドバーでの複数のワークスペースの有効化・無効化", + "command.session.undo": "元に戻す", + "command.session.undo.description": "最後のメッセージを元に戻す", + "command.session.redo": "やり直す", + "command.session.redo.description": "元に戻したメッセージをやり直す", + "command.session.compact": "セッションを圧縮", + "command.session.compact.description": "セッションを要約してコンテキストサイズを削減", + "command.session.fork": "メッセージからフォーク", + "command.session.fork.description": "以前のメッセージから新しいセッションを作成", + "command.session.share": "セッションを共有", + "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": "結果が見つかりません", + "palette.group.commands": "コマンド", + "palette.group.files": "ファイル", + "dialog.provider.search.placeholder": "プロバイダーを検索", + "dialog.provider.empty": "プロバイダーが見つかりません", + "dialog.provider.group.popular": "人気", + "dialog.provider.group.other": "その他", + "dialog.provider.custom.label": "OpenAI互換のカスタムプロバイダー", + "dialog.provider.tag.recommended": "推奨", + "dialog.provider.opencode.note": "Claude, GPT, Geminiなどを含む厳選されたモデル", + "dialog.provider.opencode.tagline": "信頼性の高い最適化モデル", + "dialog.provider.opencodeGo.tagline": "すべての人に低価格のサブスクリプション", + "dialog.provider.anthropic.note": "ProやMaxを含むClaudeモデルに直接アクセス", + "dialog.provider.copilot.note": "GitHub Copilotを通じてコーディングを支援するAIモデル", + "dialog.provider.openai.note": "高速で高性能な汎用AIタスク向けのGPTモデル", + "dialog.provider.google.note": "高速で構造化された応答のためのGeminiモデル", + "dialog.provider.openrouter.note": "1つのプロバイダーからすべてのサポートされているモデルにアクセス", + "dialog.provider.vercel.note": "スマートルーターによるAIモデルへの統合アクセス", + "dialog.model.select.title": "モデルを選択", + "dialog.model.search.placeholder": "モデルを検索", + "dialog.model.empty": "モデルが見つかりません", + "dialog.model.manage": "モデルを管理", + "dialog.model.manage.description": "モデルセレクターに表示するモデルをカスタマイズします。", + "dialog.model.manage.provider.toggle": "すべての{{provider}}モデルを切り替え", + "dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル", + "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加", + "dialog.model.unpaid.viewMoreProviders": "その他70以上のプロバイダーを見る", + "dialog.provider.viewAll": "さらにプロバイダーを表示", + "provider.connect.title": "{{provider}}を接続", + "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン", + "provider.connect.selectMethod": "{{provider}}のログイン方法を選択してください。", + "provider.connect.method.apiKey": "APIキー", + "provider.connect.method.browser": "ブラウザ", + "provider.connect.method.headless": "ヘッドレス モード", + "provider.connect.status.inProgress": "認証中...", + "provider.connect.status.waiting": "認証を待機中...", + "provider.connect.status.failed": "認証に失敗しました: {{error}}", + "provider.connect.apiKey.description": + "{{provider}}のAPIキーを入力してアカウントを接続し、OpenCodeで{{provider}}モデルを使用します。", + "provider.connect.apiKey.label": "{{provider}} APIキー", + "provider.connect.apiKey.placeholder": "APIキー", + "provider.connect.apiKey.required": "APIキーが必要です", + "provider.connect.opencodeZen.line1": + "OpenCode Zenは、コーディングエージェント向けに最適化された信頼性の高いモデルへのアクセスを提供します。", + "provider.connect.opencodeZen.line2": "1つのAPIキーで、Claude、GPT、Gemini、GLMなどのモデルにアクセスできます。", + "provider.connect.opencodeZen.visit.prefix": " ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " にアクセスしてAPIキーを取得してください。", + "provider.connect.oauth.code.visit.prefix": " ", + "provider.connect.oauth.code.visit.link": "このリンク", + "provider.connect.oauth.code.visit.suffix": + " にアクセスして認証コードを取得し、アカウントを接続してOpenCodeで{{provider}}モデルを使用してください。", + "provider.connect.oauth.code.label": "{{method}} 認証コード", + "provider.connect.oauth.code.placeholder": "認証コード", + "provider.connect.oauth.code.required": "認証コードが必要です", + "provider.connect.oauth.code.invalid": "無効な認証コード", + "provider.connect.oauth.auto.visit.prefix": " ", + "provider.connect.oauth.auto.visit.link": "このリンク", + "provider.connect.oauth.auto.visit.suffix": + " にアクセスし、以下のコードを入力してアカウントを接続し、OpenCodeで{{provider}}モデルを使用してください。", + "provider.connect.oauth.auto.confirmationCode": "確認コード", + "provider.connect.toast.connected.title": "{{provider}}が接続されました", + "provider.connect.toast.connected.description": "{{provider}}モデルが使用可能になりました。", + "provider.custom.title": "カスタムプロバイダー", + "provider.custom.unavailable": "このサーバーではカスタムプロバイダーを利用できません", + "provider.custom.description.prefix": "OpenAI互換のプロバイダーを設定します。詳細は", + "provider.custom.description.link": "プロバイダー設定ドキュメント", + "provider.custom.description.suffix": "をご覧ください。", + "provider.custom.field.providerID.label": "プロバイダーID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "小文字、数字、ハイフン、アンダースコア", + "provider.custom.field.name.label": "表示名", + "provider.custom.field.name.placeholder": "マイAIプロバイダー", + "provider.custom.field.baseURL.label": "ベースURL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "APIキー", + "provider.custom.field.apiKey.placeholder": "APIキー", + "provider.custom.field.apiKey.description": "オプション。ヘッダーで認証を管理する場合は空のままにしてください。", + "provider.custom.models.label": "モデル", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "名前", + "provider.custom.models.name.placeholder": "表示名", + "provider.custom.models.remove": "モデルを削除", + "provider.custom.models.add": "モデルを追加", + "provider.custom.headers.label": "ヘッダー (オプション)", + "provider.custom.headers.key.label": "ヘッダー", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "値", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "ヘッダーを削除", + "provider.custom.headers.add": "ヘッダーを追加", + "provider.custom.error.providerID.required": "プロバイダーIDが必要です", + "provider.custom.error.providerID.format": "小文字、数字、ハイフン、アンダースコアを使用してください", + "provider.custom.error.providerID.exists": "そのプロバイダーIDは既に存在します", + "provider.custom.error.name.required": "表示名が必要です", + "provider.custom.error.baseURL.required": "ベースURLが必要です", + "provider.custom.error.baseURL.format": "http:// または https:// で始まる必要があります", + "provider.custom.error.required": "必須", + "provider.custom.error.duplicate": "重複", + "provider.disconnect.toast.disconnected.title": "{{provider}}が切断されました", + "provider.disconnect.toast.disconnected.description": "{{provider}}のモデルは利用できなくなりました。", + "model.tag.free": "無料", + "model.tag.latest": "最新", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "テキスト", + "model.input.image": "画像", + "model.input.audio": "音声", + "model.input.video": "動画", + "model.input.pdf": "PDF", + "model.tooltip.allows": "対応: {{inputs}}", + "model.tooltip.context.label": "コンテキスト", + "model.tooltip.inputs": "入力", + "model.tooltip.model": "モデル", + "model.tooltip.provider": "プロバイダー", + "model.tooltip.reasoning": "推論", + "model.tooltip.reasoning.allowed": "推論を許可", + "model.tooltip.reasoning.none": "推論なし", + "model.tooltip.context": "コンテキスト上限 {{limit}}", + "common.search.placeholder": "検索", + "common.goBack": "戻る", + "common.goForward": "進む", + "common.loading": "読み込み中", + "common.loading.ellipsis": "...", + "common.cancel": "キャンセル", + "common.clear": "クリア", + "common.connect": "接続", + "common.disconnect": "切断", + "common.continue": "続行", + "common.submit": "送信", + "common.save": "保存", + "common.saving": "保存中...", + "common.default": "デフォルト", + "common.attachment": "添付ファイル", + "prompt.placeholder.shell": "シェルコマンドを入力... {{example}}", + "prompt.placeholder.normal": '何でも聞いてください... "{{example}}"', + "prompt.placeholder.simple": "何でも聞いてください...", + "prompt.placeholder.summarizeComments": "コメントを要約…", + "prompt.placeholder.summarizeComment": "コメントを要約…", + "prompt.mode.shell": "シェル", + "prompt.mode.normal": "プロンプト", + "prompt.mode.shell.exit": "escで終了", + "session.child.promptDisabled": "サブエージェントのセッションにはプロンプトを送信できません。", + "session.child.backToParent": "メインセッションに戻る。", + "prompt.example.1": "コードベースのTODOを修正", + "prompt.example.2": "このプロジェクトの技術スタックは何ですか?", + "prompt.example.3": "壊れたテストを修正", + "prompt.example.4": "認証の仕組みを説明して", + "prompt.example.5": "セキュリティの脆弱性を見つけて修正", + "prompt.example.6": "ユーザーサービスのユニットテストを追加", + "prompt.example.7": "この関数を読みやすくリファクタリング", + "prompt.example.8": "このエラーはどういう意味ですか?", + "prompt.example.9": "この問題のデバッグを手伝って", + "prompt.example.10": "APIドキュメントを生成", + "prompt.example.11": "データベースクエリを最適化", + "prompt.example.12": "入力バリデーションを追加", + "prompt.example.13": "〜の新しいコンポーネントを作成", + "prompt.example.14": "このプロジェクトをデプロイするには?", + "prompt.example.15": "ベストプラクティスの観点でコードをレビュー", + "prompt.example.16": "この関数にエラーハンドリングを追加", + "prompt.example.17": "この正規表現パターンを説明して", + "prompt.example.18": "これをTypeScriptに変換", + "prompt.example.19": "コードベース全体にログを追加", + "prompt.example.20": "古い依存関係はどれですか?", + "prompt.example.21": "マイグレーションスクリプトの作成を手伝って", + "prompt.example.22": "このエンドポイントにキャッシュを実装", + "prompt.example.23": "このリストにページネーションを追加", + "prompt.example.24": "〜のCLIコマンドを作成", + "prompt.example.25": "ここでは環境変数はどう機能しますか?", + "prompt.popover.emptyResults": "一致する結果がありません", + "prompt.popover.emptyCommands": "一致するコマンドがありません", + "prompt.dropzone.label": "画像、PDF、またはテキストファイルをここにドロップしてください", + "prompt.dropzone.file.label": "ドロップしてファイルを@メンション", + "prompt.slash.badge.custom": "カスタム", + "prompt.slash.badge.skill": "スキル", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "アクティブ", + "prompt.context.includeActiveFile": "アクティブなファイルを含める", + "prompt.context.removeActiveFile": "コンテキストからアクティブなファイルを削除", + "prompt.context.removeFile": "コンテキストからファイルを削除", + "prompt.action.attachFile": "ファイルを添付", + "prompt.menu.addImagesAndFiles": "ファイルなどを追加", + "prompt.menu.imagesAndFiles": "画像とファイル", + "prompt.menu.commands": "コマンド", + "prompt.menu.context": "コンテキスト", + "prompt.menu.shellCommand": "シェルコマンド", + "prompt.attachment.remove": "添付ファイルを削除", + "prompt.action.send": "送信", + "prompt.action.stop": "停止", + "prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル", + "prompt.toast.attachmentDuplicate.title": "このファイルはすでにアップロードされています", + "prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。", + "prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択", + "prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。", + "prompt.toast.worktreeCreateFailed.title": "ワークツリーの作成に失敗しました", + "prompt.toast.sessionCreateFailed.title": "セッションの作成に失敗しました", + "prompt.toast.shellSendFailed.title": "シェルコマンドの送信に失敗しました", + "prompt.toast.commandSendFailed.title": "コマンドの送信に失敗しました", + "prompt.toast.promptSendFailed.title": "プロンプトの送信に失敗しました", + "prompt.toast.promptSendFailed.description": "セッションを取得できませんでした", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{total}}個中{{enabled}}個が有効", + "dialog.mcp.empty": "MCPが設定されていません", + "dialog.lsp.empty": "ファイルタイプから自動検出されたLSP", + "dialog.plugins.empty": "opencode.jsonで設定されたプラグイン", + "mcp.status.connected": "接続済み", + "mcp.status.failed": "失敗", + "mcp.status.needs_auth": "認証が必要", + "mcp.auth.clickToAuthenticate": "クリックして認証", + "mcp.status.disabled": "無効", + "dialog.fork.empty": "フォーク元のメッセージがありません", + "dialog.directory.search.placeholder": "フォルダを検索", + "dialog.directory.empty": "フォルダが見つかりません", + "dialog.directory.action.selectFile": "ファイルを選択", + "dialog.directory.action.selectFolder": "フォルダを選択", + "dialog.directory.root": "ルート", + "dialog.directory.parent": "親フォルダ", + "dialog.directory.readError": "このフォルダを読み取れません", + "dialog.server.title": "サーバー", + "dialog.server.description": "このアプリが接続するOpenCodeサーバーを切り替えます。", + "dialog.server.search.placeholder": "サーバーを検索", + "dialog.server.empty": "サーバーはまだありません", + "dialog.server.add.title": "サーバーを追加", + "dialog.server.add.url": "サーバーURL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "サーバーに接続できませんでした", + "dialog.server.add.checking": "確認中...", + "dialog.server.add.button": "サーバーを追加", + "dialog.server.add.name": "サーバー名 (オプション)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "ユーザー名 (オプション)", + "dialog.server.add.password": "パスワード (オプション)", + "dialog.server.edit.title": "サーバーを編集", + "dialog.server.default.title": "デフォルトサーバー", + "dialog.server.default.description": + "ローカルサーバーを起動する代わりに、アプリ起動時にこのサーバーに接続します。再起動が必要です。", + "dialog.server.default.none": "サーバーが選択されていません", + "dialog.server.default.set": "現在のサーバーをデフォルトに設定", + "dialog.server.default.clear": "クリア", + "dialog.server.action.remove": "サーバーを削除", + "dialog.server.menu.edit": "編集", + "dialog.server.menu.default": "デフォルトに設定", + "dialog.server.menu.defaultRemove": "デフォルト設定を解除", + "dialog.server.menu.delete": "削除", + "dialog.server.current": "現在のサーバー", + "dialog.server.status.default": "デフォルト", + "wsl.server.add": "WSLサーバーを追加", + "wsl.server.addShort": "WSLを追加", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSLサーバー", + "wsl.server.retryStart": "起動を再試行", + "wsl.server.updating": "更新中...", + "wsl.onboarding.step.distro": "ディストリビューションを選択", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSLを確認中...", + "wsl.onboarding.restartRequired": "WSLのインストールを完了するにはWindowsの再起動が必要です。", + "wsl.onboarding.ready": "WSLの準備ができました。", + "wsl.onboarding.required": "続行するにはWSLが必要です。", + "wsl.onboarding.checkingDistros": "ディストリビューションを確認中...", + "wsl.onboarding.installingDistro": "{{distro}}をインストール中...", + "wsl.onboarding.checkingDistro": "{{distro}}を確認中...", + "wsl.onboarding.listingDistros": "ディストリビューションの一覧を取得中...", + "wsl.onboarding.distroReady": "{{distro}}の準備ができました。", + "wsl.onboarding.distroNotInstalled": "{{distro}}はまだインストールされていません。", + "wsl.onboarding.openDistroOnce": "セットアップを完了するには、{{distro}}を一度開いてください。", + "wsl.onboarding.finishingDistro": "{{distro}}のセットアップを完了しています。", + "wsl.onboarding.pickDistro": "ディストリビューションを選択するか、以下からインストールしてください。", + "wsl.onboarding.checkingOpencode": "OpenCodeを確認中...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}}内のOpenCodeを確認中...", + "wsl.onboarding.updatingOpencode": "OpenCodeを更新中...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}}内のOpenCodeを更新中...", + "wsl.onboarding.updateOpencodeIn": "{{distro}}内のOpenCodeを更新してください。", + "wsl.onboarding.updateOpencode": "OpenCodeを更新", + "wsl.onboarding.opencodeReadyIn": "{{distro}}内のOpenCodeの準備ができました。", + "wsl.onboarding.opencodeReady": "OpenCodeの準備ができました。", + "wsl.onboarding.installOpencodeIn": "{{distro}}にOpenCodeをインストールしてください。", + "wsl.onboarding.installOpencode": "OpenCodeをインストール", + "wsl.onboarding.chooseDistroFirst": "先にディストリビューションを選択してください。", + "wsl.onboarding.loadFailed": "WSLの状態を読み込めませんでした。", + "wsl.onboarding.loading": "読み込み中...", + "wsl.onboarding.installedDistros": "インストール済みのディストリビューション", + "wsl.onboarding.checkAgain": "再確認", + "wsl.onboarding.distroStatus.ready": "準備完了", + "wsl.onboarding.distroStatus.checking": "確認中...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCodeがインストールされていません", + "wsl.onboarding.distroStatus.missingTools": "bash、curlがありません", + "wsl.onboarding.distroStatus.unsupported": "非対応 · WSL 2を使用してください", + "wsl.onboarding.needAnotherDistro": "別のディストリビューションが必要ですか?", + "wsl.onboarding.needAnotherDistroHint": "WSLカタログからLinuxディストリビューションをインストール", + "wsl.onboarding.wslNotInstalled.title": "WSLがインストールされていません", + "wsl.onboarding.wslNotInstalled.description": + "OpenCodeでWSLサーバーを追加するには、WSL (Windows Subsystem for Linux) が必要です", + "wsl.onboarding.wslUnavailable.title": "WSLを利用できません", + "wsl.onboarding.wslUnavailable.description": "このマシンのWSLを確認できませんでした。", + "wsl.onboarding.installWsl": "WSLをインストール", + "wsl.onboarding.windowsRestartRequired": + "WSLのインストールを完了するにはWindowsを再起動し、OpenCodeをもう一度開いてください。", + "wsl.onboarding.next": "次へ", + "wsl.onboarding.refresh": "更新", + "wsl.onboarding.allDistrosAdded": "インストール済みのディストリビューションはすべて追加済みです。", + "wsl.onboarding.noDistros": "ディストリビューションはまだ検出されていません。", + "wsl.onboarding.install": "インストール", + "wsl.onboarding.installing": "インストール中...", + "wsl.onboarding.installDistro": "ディストリビューションをインストール", + "wsl.onboarding.searchDistros": "ディストリビューションを検索", + "wsl.onboarding.wsl2Required": "WSL 2が必要です。", + "wsl.onboarding.toolsRequired": "このディストリビューションにはbashとcurlが必要です。", + "wsl.onboarding.openTerminal": "ターミナルを開く", + "wsl.onboarding.path": "パス: {{path}}", + "wsl.onboarding.notFound": "見つかりません", + "wsl.onboarding.version": "バージョン: {{version}}", + "wsl.onboarding.unknown": "不明", + "wsl.onboarding.desktopVersion": "デスクトップ {{version}}", + "wsl.onboarding.versionMismatch": "インストールされているバージョンがデスクトップアプリのバージョンと一致しません。", + "wsl.onboarding.adding": "追加中...", + + "help.tabs.toast.ariaLabel": "タブのご紹介。タブを使って作業と進行中のセッションを整理できます", + "help.tabs.toast.dismiss": "タブに関する案内を閉じる", + "help.tabs.title": "タブのご紹介", + "help.tabs.description": "タブを使って作業と進行中のセッションを整理できます", + "help.tabs.date": "7月14日", + "help.tabs.introduction": "OpenCode Desktopでタブを中心とした操作ができるようになりました。", + "help.tabs.sessions": + "新しいセッションをタブで開始したり、プロジェクトから既存のセッションを開いたりできます。新しい作業を始めるときは新しいタブを開き、作業が終わったら閉じます。", + "help.tabs.organize": + "複数のタブを開いておくと、進行中のセッションを整理しやすくなります。タブを残しておく場合は、覚えやすい名前に変更できます。", + "help.tabs.home": + "新しいホーム画面には、すべてのセッションとプロジェクトが表示されます。セッションを選択するとタブで開きます。", + "help.tabs.persistence": "アプリを再度開いたときも、タブは開いたままです。", + "help.tabs.worktrees": + "新しいデザインはまだGitワークツリーに対応していませんが、近日中に対応する予定です。以前のレイアウトを引き続き使用する場合は、設定でレイアウトを切り替えることができます。新しいレイアウトは数週間後に標準となる予定です。", + "dialog.project.edit.title": "プロジェクトを編集", + "dialog.project.edit.name": "名前", + "dialog.project.edit.icon": "アイコン", + "dialog.project.edit.icon.alt": "プロジェクトアイコン", + "dialog.project.edit.icon.hint": "クリックまたは画像をドラッグ", + "dialog.project.edit.icon.recommended": "推奨: 128x128px", + "dialog.project.edit.color": "色", + "dialog.project.edit.color.select": "{{color}}の色を選択", + "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": "システム", + "context.breakdown.user": "ユーザー", + "context.breakdown.assistant": "アシスタント", + "context.breakdown.tool": "ツール呼び出し", + "context.breakdown.other": "その他", + "context.systemPrompt.title": "システムプロンプト", + "context.rawMessages.title": "生のメッセージ", + "context.export.session": "セッションをエクスポート", + + "context.stats.session": "セッション", + "context.stats.messages": "メッセージ", + "context.stats.provider": "プロバイダー", + "context.stats.model": "モデル", + "context.stats.limit": "コンテキスト制限", + "context.stats.totalTokens": "総トークン数", + "context.stats.usage": "使用量", + "context.stats.inputTokens": "入力トークン", + "context.stats.outputTokens": "出力トークン", + "context.stats.reasoningTokens": "推論トークン", + "context.stats.cacheTokens": "キャッシュトークン (読込/書込)", + "context.stats.userMessages": "ユーザーメッセージ", + "context.stats.assistantMessages": "アシスタントメッセージ", + "context.stats.totalCost": "総コスト", + "context.stats.sessionCreated": "セッション作成日時", + "context.stats.lastActivity": "最終アクティビティ", + "context.usage.tokens": "トークン", + "context.usage.usage": "使用量", + "context.usage.cost": "コスト", + "context.usage.clickToView": "クリックしてコンテキストを表示", + "context.usage.view": "コンテキスト使用量を表示", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "言語", + "toast.language.description": "{{language}}に切り替えました", + "toast.theme.title": "テーマが切り替わりました", + "toast.scheme.title": "配色", + "toast.workspace.enabled.title": "ワークスペースが有効になりました", + "toast.workspace.enabled.description": "サイドバーに複数のワークツリーが表示されます", + "toast.workspace.disabled.title": "ワークスペースが無効になりました", + "toast.workspace.disabled.description": "サイドバーにはメインのワークツリーのみが表示されます", + "toast.permissions.autoaccept.on.title": "権限を自動承認しています", + "toast.permissions.autoaccept.on.description": "権限の要求は自動的に承認されます", + "toast.permissions.autoaccept.off.title": "権限の自動承認を停止しました", + "toast.permissions.autoaccept.off.description": "権限の要求には承認が必要になります", + "toast.model.none.title": "モデルが選択されていません", + "toast.model.none.description": "このセッションを要約するにはプロバイダーを接続してください", + "toast.file.loadFailed.title": "ファイルの読み込みに失敗しました", + "toast.file.listFailed.title": "ファイル一覧の取得に失敗しました", + "toast.context.noLineSelection.title": "行が選択されていません", + "toast.context.noLineSelection.description": "まずファイルタブで行範囲を選択してください。", + "toast.session.share.copyFailed.title": "URLのコピーに失敗しました", + "toast.session.share.success.title": "セッションを共有しました", + "toast.session.share.success.description": "共有URLをクリップボードにコピーしました!", + "toast.session.share.failed.title": "セッションの共有に失敗しました", + "toast.session.share.failed.description": "セッションの共有中にエラーが発生しました", + "toast.session.unshare.success.title": "セッションの共有を解除しました", + "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}}) がインストール可能です。", + "toast.update.action.installRestart": "インストールして再起動", + "toast.update.action.notYet": "今はしない", + "error.page.title": "問題が発生しました", + "error.page.description": "アプリケーションの読み込み中にエラーが発生しました。", + "error.page.description.localServerStartup": "ローカルサーバーの起動中にエラーが発生しました。", + "error.page.details.label": "エラー詳細", + "error.page.action.restart": "再起動", + "error.page.action.report": "エラーを報告", + "error.page.action.reported": "エラーを報告しました", + "error.page.action.exportLogs": "ログをエクスポート", + "error.page.action.checking": "確認中...", + "error.page.action.checkUpdates": "アップデートを確認", + "error.page.action.updateTo": "{{version}}にアップデート", + "error.page.report.prefix": "このエラーをOpenCodeチームに報告してください: ", + "error.page.report.discord": "Discord", + "error.page.version": "バージョン: {{version}}", + "error.dev.rootNotFound": + "ルート要素が見つかりません。index.htmlに追加するのを忘れていませんか?またはid属性のスペルが間違っていませんか?", + "error.serverSync.connectFailed": "サーバーに接続できませんでした。`{{url}}`でサーバーが実行されていますか?", + "directory.error.invalidUrl": "URL内のディレクトリが無効です。", + "error.chain.unknown": "不明なエラー", + "error.chain.causedBy": "原因:", + "error.chain.apiError": "APIエラー", + "error.chain.status": "ステータス: {{status}}", + "error.chain.retryable": "再試行可能: {{retryable}}", + "error.chain.responseBody": "レスポンス本文:\n{{body}}", + "error.chain.didYouMean": "もしかして: {{suggestions}}", + "error.chain.modelNotFound": "モデルが見つかりません: {{provider}}/{{model}}", + "error.chain.checkConfig": "config (opencode.json) のプロバイダー/モデル名を確認してください", + "error.chain.mcpFailed": 'MCPサーバー "{{name}}" が失敗しました。注意: OpenCodeはまだMCP認証をサポートしていません。', + "error.chain.providerAuthFailed": "プロバイダー認証に失敗しました ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'プロバイダー "{{provider}}" の初期化に失敗しました。認証情報と設定を確認してください。', + "error.chain.configJsonInvalid": "{{path}} の設定ファイルは有効なJSON(C)ではありません", + "error.chain.configJsonInvalidWithMessage": "{{path}} の設定ファイルは有効なJSON(C)ではありません: {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} 内のディレクトリ "{{dir}}" は無効です。"{{suggestion}}" に名前を変更するか削除してください。これはよくあるタイプミスです。', + "error.chain.configFrontmatterError": "{{path}} のフロントマターの解析に失敗しました:\n{{message}}", + "error.chain.configInvalid": "{{path}} の設定ファイルが無効です", + "error.chain.configInvalidWithMessage": "{{path}} の設定ファイルが無効です: {{message}}", + "notification.permission.title": "権限が必要です", + "notification.permission.description": "{{projectName}} の {{sessionTitle}} が権限を必要としています", + "notification.question.title": "質問", + "notification.question.description": "{{projectName}} の {{sessionTitle}} から質問があります", + "notification.action.goToSession": "セッションへ移動", + "notification.session.responseReady.title": "応答の準備ができました", + "notification.session.error.title": "セッションエラー", + "notification.session.error.fallbackDescription": "エラーが発生しました", + "home.recentProjects": "最近のプロジェクト", + "home.empty.title": "最近のプロジェクトはありません", + "home.empty.description": "ローカルプロジェクトを開いて始めましょう", + "home.title": "ホーム", + "home.projects": "プロジェクト", + "home.project.add": "プロジェクトを追加", + "home.recentlyClosed": "最近閉じたプロジェクト", + "home.server.collapse": "サーバーのプロジェクトを折りたたむ", + "home.server.expand": "サーバーのプロジェクトを展開", + "home.sessions.search.placeholder": "セッションを検索", + "home.sessions.search.placeholder.scoped": "{{scope}}のセッションを検索", + "home.sessions.search.sessions": "セッション", + "home.sessions.search.noResults": "{{query}}に一致するセッションが見つかりません", + "home.sessions.empty": "まだ何もありません", + "home.sessions.empty.description": "セッションを作成して始めましょう", + "home.sessions.group.today": "今日", + "home.sessions.group.yesterday": "昨日", + "home.sessions.group.older": "それ以前", + "home.providerTip": "75以上のプロバイダーに接続して、Claude、GPT、Geminiなどの他のモデルを利用できます", + "session.tab.session": "セッション", + "session.tab.review": "レビュー", + "session.tab.context": "コンテキスト", + "session.tab.unknown": "不明なセッション", + "session.panel.reviewAndFiles": "レビューとファイル", + "session.error.notFound": "このセッションが見つかりません", + "session.error.notFound.description": "このタブが参照するセッションは、このサーバーには存在しません。", + "session.error.notFound.closeTab": "タブを閉じる", + "session.error.serverConnection": "このサーバーに接続できません", + "session.review.filesChanged": "変更されたファイル: {{count}}件", + "session.review.change.one": "変更", + "session.review.change.other": "変更", + "session.review.loadingChanges": "変更を読み込み中...", + "session.review.empty": "このセッションでの変更はまだありません", + "session.review.noVcs": "Gitバージョン管理システムが検出されないため、変更は表示されません", + "session.review.noSnapshot": "設定でスナップショット追跡が無効になっているため、セッションの変更は利用できません", + "session.review.noChanges": "変更なし", + "session.review.noUncommittedChanges": "コミットされていない変更はまだありません", + "session.review.noBranchChanges": "ブランチの変更はまだありません", + "session.files.selectToOpen": "開くファイルを選択", + "session.files.all": "すべてのファイル", + "session.files.empty": "ファイルなし", + "session.files.binaryContent": "バイナリファイル(内容を表示できません)", + "session.messages.renderEarlier": "以前のメッセージを表示", + "session.messages.loadingEarlier": "以前のメッセージを読み込み中...", + "session.messages.loadEarlier": "以前のメッセージを読み込む", + "session.messages.loading": "メッセージを読み込み中...", + "session.messages.jumpToLatest": "最新へジャンプ", + "session.context.addToContext": "{{selection}}をコンテキストに追加", + "session.todo.title": "ToDo", + "session.todo.collapse": "折りたたむ", + "session.todo.expand": "展開", + "session.question.minimize": "質問を最小化", + "session.question.restore": "質問を復元", + "session.question.pending.one": "{{count}}件の保留中の質問", + "session.question.pending.other": "{{count}}件の保留中の質問", + "session.followupDock.summary.one": "{{count}} 件のメッセージが待機中", + "session.followupDock.summary.other": "{{count}} 件のメッセージが待機中", + "session.followupDock.sendNow": "今すぐ送信", + "session.followupDock.edit": "編集", + "session.followupDock.collapse": "待機中のメッセージを折りたたむ", + "session.followupDock.expand": "待機中のメッセージを展開", + "session.revertDock.summary.one": "{{count}} 件のロールバックされたメッセージ", + "session.revertDock.summary.other": "{{count}} 件のロールバックされたメッセージ", + "session.revertDock.collapse": "ロールバックされたメッセージを折りたたむ", + "session.revertDock.expand": "ロールバックされたメッセージを展開", + "session.revertDock.restore": "メッセージを復元", + "session.new.title": "何でも作る", + "session.new.project.new": "新しいプロジェクト", + "session.new.project.search": "プロジェクトを検索", + "session.new.project.add": "プロジェクトを追加", + "session.new.worktree.main": "メインブランチ", + "session.new.worktree.mainWithBranch": "メインブランチ ({{branch}})", + "session.new.worktree.create": "新しいワークツリーを作成", + "session.new.workspace.runIn": "セッションの実行先", + "session.new.workspace.triggerLocal": "ローカル", + "session.new.workspace.local": "ローカルリポジトリ", + "session.new.workspace.existing": "ワークスペース…", + "session.new.git.none": "Git なし", + "session.new.lastModified": "最終更新", + "session.header.search.placeholder": "{{project}}を検索", + "session.header.searchFiles": "ファイルを検索", + "session.header.openIn": "で開く", + "session.header.open.action": "{{app}}を開く", + "session.header.open.ariaLabel": "{{app}}で開く", + "session.header.open.menu": "開くオプション", + "session.header.open.copyPath": "パスをコピー", + "status.popover.trigger": "ステータス", + "status.popover.ariaLabel": "サーバー設定", + "status.popover.tab.servers": "サーバー", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "プラグイン", + "status.popover.action.manageServers": "サーバーを管理", + "session.share.popover.title": "ウェブで公開", + "session.share.popover.description.shared": + "このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。", + "session.share.popover.description.unshared": + "セッションをウェブで公開します。リンクを知っている人なら誰でもアクセスできるようになります。", + "session.share.action.share": "共有", + "session.share.action.publish": "公開", + "session.share.action.publishing": "公開中...", + "session.share.action.unpublish": "非公開にする", + "session.share.action.unpublishing": "非公開にしています...", + "session.share.action.view": "表示", + "session.share.copy.copied": "コピーしました", + "session.share.copy.copyLink": "リンクをコピー", + "lsp.tooltip.none": "LSPサーバーなし", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "プロンプトを読み込み中...", + "terminal.loading": "ターミナルを読み込み中...", + "terminal.title": "ターミナル", + "terminal.title.numbered": "ターミナル {{number}}", + "terminal.close": "ターミナルを閉じる", + "terminal.connectionLost.title": "接続が失われました", + "terminal.connectionLost.description": + "ターミナルの接続が中断されました。これはサーバーが再起動したときに発生することがあります。", + "common.closeTab": "タブを閉じる", + "common.dismiss": "閉じる", + "common.requestFailed": "リクエストに失敗しました", + "common.moreOptions": "その他のオプション", + "common.learnMore": "詳細", + "common.rename": "名前を変更", + "common.export": "エクスポート", + "common.reset": "リセット", + "common.archive": "アーカイブ", + "common.delete": "削除", + "common.close": "閉じる", + "common.edit": "編集", + "common.loadMore": "さらに読み込む", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "メニューを切り替え", + "sidebar.nav.projectsAndSessions": "プロジェクトとセッション", + "sidebar.settings": "設定", + "sidebar.help": "ヘルプ", + "sidebar.workspaces.enable": "ワークスペースを有効化", + "sidebar.workspaces.disable": "ワークスペースを無効化", + "sidebar.gettingStarted.title": "はじめに", + "sidebar.gettingStarted.line1": "OpenCodeには無料モデルが含まれているため、すぐに開始できます。", + "sidebar.gettingStarted.line2": "プロバイダーを接続して、Claude、GPT、Geminiなどのモデルを使用できます。", + "sidebar.project.recentSessions": "最近のセッション", + "sidebar.project.viewAllSessions": "すべてのセッションを表示", + "sidebar.project.clearNotifications": "通知をクリア", + "sidebar.empty.title": "開いているプロジェクトはありません", + "sidebar.empty.description": "プロジェクトを開いて始めましょう", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "デスクトップ", + "settings.section.server": "サーバー", + "settings.tab.general": "一般", + "settings.tab.shortcuts": "ショートカット", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL連携", + "settings.desktop.wsl.description": "WindowsのWSL環境でOpenCodeサーバーを実行します。", + "settings.general.section.appearance": "外観", + "settings.general.section.advanced": "詳細設定", + "settings.general.section.notifications": "システム通知", + "settings.general.section.updates": "アップデート", + "settings.general.section.sounds": "効果音", + "settings.general.section.feed": "フィード", + "settings.general.section.display": "ディスプレイ", + "settings.general.row.language.title": "言語", + "settings.general.row.language.description": "OpenCodeの表示言語を変更します", + "settings.general.row.shell.title": "ターミナルシェル", + "settings.general.row.shell.description": + "ターミナルで使用するシェルを選択します。互換性のあるシェルはエージェントのツール呼び出しにも使用されます。", + "settings.general.row.shell.autoDefault": "自動 (デフォルト)", + "settings.general.row.shell.terminalOnly": "ターミナルのみ", + "settings.general.row.appearance.title": "外観", + "settings.general.row.appearance.description": "デバイスでのOpenCodeの表示をカスタマイズします", + "settings.general.row.colorScheme.title": "配色", + "settings.general.row.colorScheme.description": "OpenCodeがシステム、ライト、またはダークテーマに従うかを選択します", + "settings.general.row.theme.title": "テーマ", + "settings.general.row.theme.description": "OpenCodeのテーマをカスタマイズします。", + "settings.general.row.font.title": "コードフォント", + "settings.general.row.font.description": "コードブロックで使用するフォントをカスタマイズします", + "settings.general.row.terminalFont.title": "ターミナルのフォント", + "settings.general.row.terminalFont.description": "ターミナルで使用するフォントをカスタマイズ", + "settings.general.row.uiFont.title": "UIフォント", + "settings.general.row.uiFont.description": "インターフェース全体で使用するフォントをカスタマイズします", + "settings.general.row.followup.title": "フォローアップの動作", + "settings.general.row.followup.description": + "フォローアッププロンプトを即座に実行するか、キューで待機させるかを選択します", + "settings.general.row.followup.option.queue": "キューに追加", + "settings.general.row.followup.option.steer": "即座に実行 (Steer)", + "settings.general.row.showFileTree.title": "ファイルツリー", + "settings.general.row.showFileTree.description": "セッションにファイルツリーパネルを表示します", + "settings.general.row.showNavigation.title": "ナビゲーションコントロール", + "settings.general.row.showNavigation.description": "デスクトップのタイトルバーに戻るボタンと進むボタンを表示します", + "settings.general.row.showSearch.title": "コマンドパレット", + "settings.general.row.showSearch.description": "タイトルバーに検索とコマンドパレットのボタンを表示します", + "settings.general.row.showTerminal.title": "ターミナル", + "settings.general.row.showTerminal.description": "デスクトップのタイトルバーにターミナルボタンを表示します", + "settings.general.row.showStatus.title": "サーバーステータス", + "settings.general.row.showStatus.description": "タイトルバーにサーバーステータスボタンを表示します", + "settings.general.row.mobileTitlebarBottom.title": "下部ナビゲーション", + "settings.general.row.mobileTitlebarBottom.description": + "モバイルではタイトルバーとセッションタブを画面下部に配置します", + "settings.general.row.showCustomAgents.title": "エージェントを表示", + "settings.general.row.showCustomAgents.description": + "コンポーザーでエージェントを切り替えます。非表示の場合は、デフォルトでBuildエージェントが使用されます。", + "settings.general.row.reasoningSummaries.title": "推論の要約を表示", + "settings.general.row.reasoningSummaries.description": "タイムラインにモデルの推論の要約を表示します", + "settings.general.row.shellToolPartsExpanded.title": "shell ツールパーツを展開", + "settings.general.row.shellToolPartsExpanded.description": + "タイムラインで shell ツールパーツをデフォルトで展開して表示します", + "settings.general.row.editToolPartsExpanded.title": "edit ツールパーツを展開", + "settings.general.row.editToolPartsExpanded.description": + "タイムラインで edit、write、patch ツールパーツをデフォルトで展開して表示します", + "settings.general.row.newInterface.title": "新しいレイアウト", + "settings.general.row.newInterface.badge": "新機能", + "settings.general.row.newInterface.description": + "新しいタブとホーム画面のレイアウトを使用します。期間限定でレイアウトを切り替えられます。", + "settings.general.row.newInterfaceNotice.title": "新しいレイアウトを使用しています", + "settings.general.row.newInterfaceNotice.description": "以前のレイアウトは利用できなくなりました", + "settings.general.row.newInterfaceNotice.dismiss": "閉じる", + "settings.general.row.pinchZoom.title": "ピンチでズーム", + "settings.general.row.pinchZoom.description": + "トラックパッドのピンチ操作とCtrl+スクロール操作によるズームを許可します", + "settings.general.row.wayland.title": "ネイティブWaylandを使用", + "settings.general.row.wayland.description": "WaylandでのX11フォールバックを無効にします。再起動が必要です。", + "settings.general.row.wayland.tooltip": + "リフレッシュレートが混在するモニターを使用しているLinuxでは、ネイティブWaylandの方が安定する場合があります。", + "settings.general.row.releaseNotes.title": "リリースノート", + "settings.general.row.releaseNotes.description": "アップデート後に「新機能」ポップアップを表示", + "settings.updates.row.startup.title": "起動時にアップデートを確認", + "settings.updates.row.startup.description": "OpenCode の起動時に自動でアップデートを確認します", + "settings.updates.row.check.title": "アップデートを確認", + "settings.updates.row.check.description": "手動でアップデートを確認し、利用可能ならインストールします", + "settings.updates.action.checkNow": "今すぐ確認", + "settings.updates.action.checking": "確認中...", + "settings.updates.action.downloading": "ダウンロード中...", + "settings.updates.action.installing": "インストール中...", + "settings.updates.toast.latest.title": "最新です", + "settings.updates.toast.latest.description": "OpenCode は最新バージョンです。", + "sound.option.none": "なし", + "sound.option.alert01": "アラート 01", + "sound.option.alert02": "アラート 02", + "sound.option.alert03": "アラート 03", + "sound.option.alert04": "アラート 04", + "sound.option.alert05": "アラート 05", + "sound.option.alert06": "アラート 06", + "sound.option.alert07": "アラート 07", + "sound.option.alert08": "アラート 08", + "sound.option.alert09": "アラート 09", + "sound.option.alert10": "アラート 10", + "sound.option.bipbop01": "ビップボップ 01", + "sound.option.bipbop02": "ビップボップ 02", + "sound.option.bipbop03": "ビップボップ 03", + "sound.option.bipbop04": "ビップボップ 04", + "sound.option.bipbop05": "ビップボップ 05", + "sound.option.bipbop06": "ビップボップ 06", + "sound.option.bipbop07": "ビップボップ 07", + "sound.option.bipbop08": "ビップボップ 08", + "sound.option.bipbop09": "ビップボップ 09", + "sound.option.bipbop10": "ビップボップ 10", + "sound.option.staplebops01": "ステープルボップス 01", + "sound.option.staplebops02": "ステープルボップス 02", + "sound.option.staplebops03": "ステープルボップス 03", + "sound.option.staplebops04": "ステープルボップス 04", + "sound.option.staplebops05": "ステープルボップス 05", + "sound.option.staplebops06": "ステープルボップス 06", + "sound.option.staplebops07": "ステープルボップス 07", + "sound.option.nope01": "いいえ 01", + "sound.option.nope02": "いいえ 02", + "sound.option.nope03": "いいえ 03", + "sound.option.nope04": "いいえ 04", + "sound.option.nope05": "いいえ 05", + "sound.option.nope06": "いいえ 06", + "sound.option.nope07": "いいえ 07", + "sound.option.nope08": "いいえ 08", + "sound.option.nope09": "いいえ 09", + "sound.option.nope10": "いいえ 10", + "sound.option.nope11": "いいえ 11", + "sound.option.nope12": "いいえ 12", + "sound.option.yup01": "はい 01", + "sound.option.yup02": "はい 02", + "sound.option.yup03": "はい 03", + "sound.option.yup04": "はい 04", + "sound.option.yup05": "はい 05", + "sound.option.yup06": "はい 06", + "settings.general.notifications.agent.title": "エージェント", + "settings.general.notifications.agent.description": + "エージェントが完了したか、注意が必要な場合にシステム通知を表示します", + "settings.general.notifications.permissions.title": "権限", + "settings.general.notifications.permissions.description": "権限が必要な場合にシステム通知を表示します", + "settings.general.notifications.errors.title": "エラー", + "settings.general.notifications.errors.description": "エラーが発生した場合にシステム通知を表示します", + "settings.general.sounds.agent.title": "エージェント", + "settings.general.sounds.agent.description": "エージェントが完了したか、注意が必要な場合に音を再生します", + "settings.general.sounds.permissions.title": "権限", + "settings.general.sounds.permissions.description": "権限が必要な場合に音を再生します", + "settings.general.sounds.errors.title": "エラー", + "settings.general.sounds.errors.description": "エラーが発生した場合に音を再生します", + "settings.shortcuts.title": "キーボードショートカット", + "settings.shortcuts.reset.button": "デフォルトにリセット", + "settings.shortcuts.reset.toast.title": "ショートカットをリセットしました", + "settings.shortcuts.reset.toast.description": "キーボードショートカットがデフォルトにリセットされました。", + "settings.shortcuts.conflict.title": "ショートカットは既に使用されています", + "settings.shortcuts.conflict.description": "{{keybind}} は既に {{titles}} に割り当てられています。", + "settings.shortcuts.unassigned": "未割り当て", + "settings.shortcuts.pressKeys": "キーを押してください", + "settings.shortcuts.search.placeholder": "ショートカットを検索", + "settings.shortcuts.search.empty": "ショートカットが見つかりません", + "settings.shortcuts.group.general": "一般", + "settings.shortcuts.group.session": "セッション", + "settings.shortcuts.group.navigation": "ナビゲーション", + "settings.shortcuts.group.modelAndAgent": "モデルとエージェント", + "settings.shortcuts.group.terminal": "ターミナル", + "settings.shortcuts.group.prompt": "プロンプト", + "settings.providers.title": "プロバイダー", + "settings.providers.description": "プロバイダー設定は今後ここで構成できるようになります。", + "settings.providers.section.connected": "接続済みプロバイダー", + "settings.providers.connected.empty": "接続済みプロバイダーはありません", + "settings.providers.section.popular": "人気のプロバイダー", + "settings.providers.tag.environment": "環境", + "settings.providers.tag.config": "設定", + "settings.providers.tag.custom": "カスタム", + "settings.providers.tag.other": "その他", + "settings.models.title": "モデル", + "settings.models.description": "モデル設定は今後ここで構成できるようになります。", + "settings.agents.title": "エージェント", + "settings.agents.description": "エージェント設定は今後ここで構成できるようになります。", + "settings.commands.title": "コマンド", + "settings.commands.description": "コマンド設定は今後ここで構成できるようになります。", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP設定は今後ここで構成できるようになります。", + "settings.permissions.title": "権限", + "settings.permissions.description": "サーバーがデフォルトで使用できるツールを制御します。", + "settings.permissions.section.tools": "ツール", + "settings.permissions.toast.updateFailed.title": "権限の更新に失敗しました", + "settings.permissions.action.allow": "許可", + "settings.permissions.action.ask": "確認", + "settings.permissions.action.deny": "拒否", + "settings.permissions.tool.read.title": "読み込み", + "settings.permissions.tool.read.description": "ファイルの読み込み (ファイルパスに一致)", + "settings.permissions.tool.edit.title": "編集", + "settings.permissions.tool.edit.description": "ファイルの変更(編集、書き込み、パッチ、複数編集を含む)", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Globパターンを使用したファイルの一致", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "正規表現を使用したファイル内容の検索", + "settings.permissions.tool.list.title": "リスト", + "settings.permissions.tool.list.description": "ディレクトリ内のファイル一覧表示", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "シェルコマンドの実行", + "settings.permissions.tool.task.title": "タスク", + "settings.permissions.tool.task.description": "サブエージェントの起動", + "settings.permissions.tool.skill.title": "スキル", + "settings.permissions.tool.skill.description": "名前によるスキルの読み込み", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "言語サーバークエリの実行", + "settings.permissions.tool.todowrite.title": "Todo書き込み", + "settings.permissions.tool.todowrite.description": "Todoリストの更新", + "settings.permissions.tool.webfetch.title": "Web取得", + "settings.permissions.tool.webfetch.description": "URLからコンテンツを取得", + "settings.permissions.tool.websearch.title": "Web検索", + "settings.permissions.tool.websearch.description": "ウェブを検索", + "settings.permissions.tool.external_directory.title": "外部ディレクトリ", + "settings.permissions.tool.external_directory.description": "プロジェクトディレクトリ外のファイルへのアクセス", + "settings.permissions.tool.doom_loop.title": "無限ループ", + "settings.permissions.tool.doom_loop.description": "同一入力による繰り返しのツール呼び出しを検出", + "session.delete.failed.title": "セッションの削除に失敗しました", + "session.delete.title": "セッションの削除", + "session.delete.confirm": 'セッション "{{name}}" を削除しますか?', + "session.delete.button": "セッションを削除", + "workspace.new": "新しいワークスペース", + "workspace.type.local": "ローカル", + "workspace.type.sandbox": "サンドボックス", + "workspace.create.failed.title": "ワークスペースの作成に失敗しました", + "workspace.delete.failed.title": "ワークスペースの削除に失敗しました", + "workspace.resetting.title": "ワークスペースをリセット中", + "workspace.resetting.description": "これには少し時間がかかる場合があります。", + "workspace.reset.failed.title": "ワークスペースのリセットに失敗しました", + "workspace.reset.success.title": "ワークスペースをリセットしました", + "workspace.reset.success.description": "ワークスペースはデフォルトブランチと一致しています。", + "workspace.error.stillPreparing": "ワークスペースはまだ準備中です", + "workspace.status.checking": "未マージの変更を確認中...", + "workspace.status.error": "gitステータスを確認できません。", + "workspace.status.clean": "未マージの変更は検出されませんでした。", + "workspace.status.dirty": "このワークスペースで未マージの変更が検出されました。", + "workspace.delete.title": "ワークスペースの削除", + "workspace.delete.confirm": 'ワークスペース "{{name}}" を削除しますか?', + "workspace.delete.button": "ワークスペースを削除", + "workspace.reset.title": "ワークスペースのリセット", + "workspace.reset.confirm": 'ワークスペース "{{name}}" をリセットしますか?', + "workspace.reset.button": "ワークスペースをリセット", + "workspace.reset.archived.none": "アクティブなセッションはアーカイブされません。", + "workspace.reset.archived.one": "1つのセッションがアーカイブされます。", + "workspace.reset.archived.many": "{{count}}個のセッションがアーカイブされます。", + "workspace.reset.note": "これにより、ワークスペースはデフォルトブランチと一致するようにリセットされます。", + "common.open": "開く", + "dialog.releaseNotes.action.getStarted": "始める", + "dialog.releaseNotes.action.next": "次へ", + "dialog.releaseNotes.action.hideFuture": "今後表示しない", + "dialog.releaseNotes.media.alt": "リリースのプレビュー", + "toast.project.reloadFailed.title": "{{project}} の再読み込みに失敗しました", + "error.server.invalidConfiguration": "無効な設定", + "common.moreCountSuffix": " (他 {{count}} 件)", + "common.time.justNow": "たった今", + "common.time.minutesAgo.short": "{{count}} 分前", + "common.time.hoursAgo.short": "{{count}} 時間前", + "common.time.daysAgo.short": "{{count}} 日前", + "settings.providers.connected.environmentDescription": "環境変数から接続されました", + "settings.providers.custom.description": "ベース URL を指定して OpenAI 互換のプロバイダーを追加します。", + + "app.server.unreachable": "{{server}} に到達できませんでした", + "app.server.retrying": "自動的に再試行中...", + "app.server.otherServers": "その他のサーバー", + "dialog.server.add.usernamePlaceholder": "ユーザー名", + "dialog.server.add.passwordPlaceholder": "パスワード", + "server.row.noUsername": "ユーザー名なし", + "session.review.noVcs.createGit.title": "Git リポジトリを作成", + "session.review.noVcs.createGit.description": "このプロジェクトの変更を追跡、レビュー、元に戻す", + "session.review.noVcs.createGit.actionLoading": "Git リポジトリを作成中...", + "session.review.noVcs.createGit.action": "Git リポジトリを作成", + "session.todo.progress": "{{done}} 個中 {{total}} 個の Todo が完了", + "session.question.progress": "{{total}} 問中 {{current}} 問", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "エクスプローラー", + "session.header.reveal.finder": "Finderで表示", + "session.header.reveal.fileExplorer": "エクスプローラーで表示", + "session.header.reveal.containingFolder": "親フォルダーを開く", + "session.header.open.fileManager": "ファイルマネージャー", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "ターミナル", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "開発パフォーマンス診断", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "セッションページに関係する最後に完了したルート遷移。ルーターの開始から処理完了後の最初の描画までを測定。", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "過去5秒間のローリングフレーム/秒。", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "過去5秒間の最悪フレーム時間。", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "過去5秒間で32msを超えたフレーム。", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "過去5秒間のブロック時間と長時間タスク数。最大タスク: {{max}}。", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "過去5秒間で観測された最悪の入力遅延。", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "過去5秒間の概算インタラクション時間。これは INP に似ていますが、公式の Web Vitals INP ではありません。", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "現在のアプリ寿命の累積レイアウトシフト。", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "使用中の JS ヒープ対ヒープ制限。Chromium のみ。", + "debugBar.mem.tip": "使用中の JS ヒープ対ヒープ制限。{{limit}} 中 {{used}}。", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "すべてのインタラクティブ要素にフォーカススタイルを強制適用", + "debugBar.focus.on": "オン", + "debugBar.focus.off": "オフ", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "選択中の言語を変更せずに、アプリ全体のレイアウト方向を強制設定", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "不明", + "error.page.circular": "[循環]", + "error.serverSDK.noServerAvailable": "利用可能なサーバーがありません", + "error.serverSDK.serverNotAvailable": "サーバーが利用できません", + "error.childStore.persistedCacheCreateFailed": "永続キャッシュの作成に失敗しました", + "error.childStore.persistedProjectMetadataCreateFailed": "永続プロジェクトメタデータの作成に失敗しました", + "error.childStore.persistedProjectIconCreateFailed": "永続プロジェクトアイコンの作成に失敗しました", + "error.childStore.storeCreateFailed": "ストアの作成に失敗しました", + "terminal.connectionLost.abnormalClose": "WebSocket が異常終了しました: {{code}}", + "terminal.connectTicket.csrfError": + "PTY接続チケットは、オリジンまたはCSRFチェックによって拒否されました。サーバーのCORS設定を確認してください。", + "terminal.connectTicket.statusError": "PTY接続チケットがステータス {{status}} で失敗しました", + + "titlebar.update": "アップデート", + "titlebar.updateVersion": "アップデート {{version}}", +} diff --git a/packages/app/src/i18n/ka.ts b/packages/app/src/i18n/ka.ts new file mode 100644 index 0000000000000000000000000000000000000000..be052930a2ec7fb72ce3afe949138848e4af1bff --- /dev/null +++ b/packages/app/src/i18n/ka.ts @@ -0,0 +1,1156 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ფაილი", + "desktop.menu.edit": "რედაქტირება", + "desktop.menu.view": "ნახვა", + "desktop.menu.go": "გადადით", + "desktop.menu.window": "ფანჯარა", + "desktop.menu.help": "დახმარება", + "desktop.menu.checkForUpdates": "შეამოწმეთ განახლებები...", + "desktop.menu.settings": "პარამეტრები", + "desktop.menu.reloadWebview": "გადატვირთვა Webview", + "desktop.menu.restart": "გადატვირთვა", + "desktop.menu.exportLogs": "ჟურნალების ექსპორტი...", + "desktop.menu.newSession": "ახალი სესია", + "desktop.menu.openProject": "გახსნა პროექტი...", + "desktop.menu.newWindow": "ახალი ფანჯარა", + "desktop.menu.closeWindow": "ფანჯრის დახურვა", + "desktop.menu.undo": "გაუქმება", + "desktop.menu.redo": "გამეორება", + "desktop.menu.cut": "მოჭრა", + "desktop.menu.copy": "ასლი", + "desktop.menu.paste": "ჩასმა", + "desktop.menu.delete": "წაშლა", + "desktop.menu.selectAll": "აირჩიეთ ყველა", + "desktop.menu.toggleSidebar": "გვერდითი ზოლის გადართვა", + "desktop.menu.toggleTerminal": "ტერმინალის ჩვენების გადართვა", + "desktop.menu.toggleFileTree": "ფაილის ხის გადართვა", + "desktop.menu.reload": "გადატვირთვა", + "desktop.menu.toggleDeveloperTools": "შემმუშავებლის ხელსაწყოების გადართვა", + "desktop.menu.actualSize": "ფაქტობრივი ზომა", + "desktop.menu.zoomIn": "გადიდება", + "desktop.menu.zoomOut": "Soom Out", + "desktop.menu.toggleFullScreen": "სრული ეკრანის გადართვა", + "desktop.menu.back": "უკან", + "desktop.menu.forward": "წინ", + "desktop.menu.previousSession": "წინა სესია", + "desktop.menu.nextSession": "შემდეგი სესია", + "desktop.menu.previousProject": "წინა პროექტი", + "desktop.menu.nextProject": "შემდეგი პროექტი", + "desktop.menu.minimize": "მინიმიზაცია", + "desktop.menu.maximize": "მაქსიმიზაცია", + "desktop.menu.documentation": "OpenCode დოკუმენტაცია", + "desktop.menu.supportForum": "მხარდაჭერის ფორუმი", + "desktop.menu.shareFeedback": "გაზიარება გამოხმაურება", + "desktop.menu.reportBug": "შეცდომის შესახებ შეტყობინება", + "desktop.menu.ariaLabel": "OpenCode მენიუ", + "desktop.updater.dialog.checkFailed.message": "განახლების შემოწმება ვერ მოხერხდა.", + "desktop.updater.dialog.checkFailed.title": "განახლების შეცდომა", + "desktop.updater.dialog.upToDate.message": "თქვენ განახლებული ხართ.", + "desktop.updater.dialog.upToDate.title": "განახლების გარეშე", + "desktop.updater.dialog.ready.message": "განახლება {{version}} ჩამოტვირთულია. გადატვირთვა ახლა?", + "desktop.updater.dialog.ready.title": "განახლება მზად არის", + "desktop.updater.dialog.restart": "გადატვირთვა", + "desktop.updater.dialog.later": "მოგვიანებით", + "desktop.recovery.action.relaunch": "ხელახლა გაშვება", + "desktop.recovery.action.exportLogs": "ჟურნალების ექსპორტი", + "desktop.recovery.action.keepWaiting": "მოელოდეთ", + "desktop.recovery.action.quit": "გამოსვლა", + "desktop.recovery.loadFailed": "OpenCode ვერ ჩაიტვირთა", + "desktop.recovery.terminated": "OpenCode ფანჯარა მოულოდნელად შეწყდა", + "desktop.recovery.unresponsive": "OpenCode არ პასუხობს", + "desktop.recovery.unresponsive.detail": "შეგიძლიათ ხელახლა გაუშვათ აპი, გახსნათ ჟურნალები ან გააგრძელოთ ლოდინი.", + "desktop.recovery.loadFailed.detail": "ფანჯარა: {{window}}\nURL: {{url}}\nშეცდომა: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "ფანჯარა: {{window}}\nმიზეზი: {{reason}}\nკოდი: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "აირჩიე საქაღალდე", + "desktop.dialog.chooseFile": "აირჩიე ფაილი", + "desktop.dialog.saveFile": "ფაილის შენახვა", + "desktop.dialog.files": "ფაილები", + "desktop.server.local": "ლოკალური სერვერი", + "desktop.wsl.error.windowsOnly": "WSL ხელმისაწვდომია მხოლოდ Windows", + "desktop.wsl.error.unavailable": "WSL მიუწვდომელია", + "desktop.wsl.error.listInstalled": "დაინსტალირებული WSL დისტროსიების სია ვერ მოიძებნა", + "desktop.wsl.error.listOnline": "ვერ განხორციელდა ონლაინ WSL დისტრიბუტების სიაში", + "desktop.wsl.error.executeDistro": "შეუძლებელია ბრძანებების შესრულება დისტროში", + "desktop.wsl.error.installWsl": "WSL ინსტალაცია ვერ მოხერხდა", + "desktop.wsl.error.installDistro": "დისტროს დაყენება ვერ მოხერხდა: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode ინსტალაცია ვერ მოხერხდა", + "desktop.wsl.error.alreadyAdded": "{{distro}} უკვე დამატებულია", + "desktop.wsl.error.opencodeMissing": "opencode არ არის დაინსტალირებული ამ დისტროში", + "desktop.wsl.error.opencodeCannotRun": "opencode დაინსტალირებულია, მაგრამ ვერ გაშვება", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode არ არის დაინსტალირებული {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode განახლება დასრულდა, მაგრამ {{distro}} კვლავ იუწყება {{installed}}; მოსალოდნელია {{expected}}", + "desktop.wsl.error.noVersion": "ვერსიის გარეშე", + "desktop.wsl.error.serverExited": "WSL სერვერი გავიდა გაშვების შემდეგ (კოდი={{code}} სიგნალი={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL სერვერი გავიდა გაჯანსაღებამდე (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Sidecar for {{distro}} ჯანმრთელობის შემოწმების დრო ამოიწურა {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} დრო ამოიწურა {{timeout}}ms", + "desktop.wsl.error.failedPort": "პორტის მიღება ვერ მოხერხდა", + "desktop.picker.error.notSelected": "ფაილი არ იყო არჩეული ამომრჩევლის მიერ", + "desktop.picker.error.sizeLimit": "არჩეული დანართები აღემატება {{limit}} MB ლიმიტს", + "command.category.suggested": "შემოთავაზებულია", + "command.category.view": "ნახვა", + "command.category.project": "პროექტი", + "command.category.provider": "პროვაიდერი", + "command.category.server": "სერვერი", + "command.category.session": "სესია", + "command.category.theme": "თემა", + "command.category.language": "ენა", + "command.category.file": "ფაილი", + "command.category.context": "კონტექსტი", + "command.category.terminal": "ტერმინალი", + "command.category.model": "მოდელი", + "command.category.mcp": "MCP", + "command.category.agent": "აგენტი", + "command.category.permissions": "ნებართვები", + "command.category.workspace": "სამუშაო სივრცე", + "command.category.settings": "პარამეტრები", + "command.logs.export": "ექსპორტის ჟურნალები", + "theme.scheme.system": "სისტემა", + "theme.scheme.light": "ღია", + "theme.scheme.dark": "მუქი", + "command.sidebar.toggle": "გვერდითი ზოლის გადართვა", + "command.project.open": "გახსნა პროექტი", + "command.project.previous": "წინა პროექტი", + "command.project.next": "შემდეგი პროექტი", + "command.project.index": "პროექტზე გადართვა {{index}}", + "command.provider.connect": "დააკავშირე პროვაიდერი", + "command.server.switch": "სერვერის გადართვა", + "command.settings.open": "გახსნა პარამეტრები", + "command.session.previous": "წინა სესია", + "command.session.next": "შემდეგი სესია", + "command.session.previous.unseen": "წინა წაუკითხავი სესია", + "command.session.next.unseen": "შემდეგი წაუკითხავი სესია", + "command.session.archive": "სესიის დაარქივება", + "command.palette": "ბრძანების პალიტრა", + "command.theme.cycle": "ციკლის თემა", + "command.theme.set": "გამოიყენე თემა: {{theme}}", + "command.theme.scheme.cycle": "ციკლის ფერის სქემა", + "command.theme.scheme.set": "გამოიყენე ფერთა სქემა: {{scheme}}", + "command.language.cycle": "ციკლის ენა", + "command.language.set": "გამოიყენე ენა: {{language}}", + "command.session.new": "ახალი სესია", + "command.file.open": "გახსენით ფაილი", + "command.tab.close": "ჩანართის დახურვა", + "command.tab.reopenClosed": "დახურული ჩანართის ხელახლა გახსნა", + "command.context.addSelection": "მონიშვნის დამატება კონტექსტში", + "command.context.addSelection.description": "მორჩეული ხაზების დამატება მიმდინარე ფაილიდან", + "command.input.focus": "ფოკუსის შეყვანა", + "command.terminal.toggle": "ტერმინალის გადართვა", + "command.fileTree.toggle": "ფაილის ხის გადართვა", + "command.review.toggle": "მიმოხილვის გადართვა", + "command.terminal.new": "ახალი ტერმინალი", + "command.terminal.new.description": "ტერმინალის ახალი ჩანართის შექმნა", + "command.steps.toggle": "გადართვა ნაბიჯები", + "command.steps.toggle.description": "აჩვენეთ ან დამალეთ ნაბიჯები მიმდინარე შეტყობინებისთვის", + "command.message.previous": "წინა შეტყობინება", + "command.message.previous.description": "გადადით მომხმარებლის წინა შეტყობინებაზე", + "command.message.next": "შემდეგი შეტყობინება", + "command.message.next.description": "გადადით მომხმარებლის შემდეგ შეტყობინებაზე", + "command.model.choose": "აირჩიე მოდელი", + "command.model.choose.description": "აირჩიე სხვა მოდელი", + "command.mcp.toggle": "გადართვა MCPs", + "command.mcp.toggle.description": "გადართვა MCPs", + "command.agent.cycle": "შემდეგ აგენტზე გადასვლა", + "command.agent.cycle.description": "გადართვა შემდეგ აგენტზე", + "command.agent.cycle.reverse": "წინა აგენტზე გადასვლა", + "command.agent.cycle.reverse.description": "წინა აგენტზე გადასვლა", + "command.model.variant.cycle": "აზროვნების ძალისხმევის დონის შეცვლა", + "command.model.variant.cycle.description": "გადართვა ძალისხმევის შემდეგ დონეზე", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "მოთხოვნა", + "command.permissions.autoaccept.enable": "ნებართვების ავტომატური მიღება", + "command.permissions.autoaccept.disable": "ნებართვების ავტომატური მიღების შეწყვეტა", + "command.workspace.toggle": "სამუშაო სივრცის გადართვა", + "command.workspace.toggle.description": "ჩართეთ ან გამორთეთ მრავალი სამუშაო სივრცე გვერდითა ზოლში", + "command.session.undo": "გაუქმება", + "command.session.undo.description": "ბოლო შეტყობინების გაუქმება", + "command.session.redo": "გამეორება", + "command.session.redo.description": "გაამეორეთ ბოლო გაუქმებული შეტყობინება", + "command.session.compact": "კომპაქტური სესია", + "command.session.compact.description": "შეაჯამეთ სესია კონტექსტის ზომის შესამცირებლად", + "command.session.fork": "შეტყობინებიდან განშტოება", + "command.session.fork.description": "შექმენით ახალი სესია წინა შეტყობინებისგან", + "command.session.share": "გაზიარება სესიის", + "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": "შედეგები არ მოიძებნა", + "palette.group.commands": "ბრძანებები", + "palette.group.files": "ფაილები", + "dialog.provider.search.placeholder": "ძებნის პროვაიდერები", + "dialog.provider.empty": "პროვაიდერები არ მოიძებნა", + "dialog.provider.group.popular": "პოპულარული", + "dialog.provider.group.other": "სხვა", + "dialog.provider.custom.label": "მორგებული OpenAI-თავსებადი პროვაიდერი", + "dialog.provider.tag.recommended": "რეკომენდებულია", + "dialog.provider.opencode.note": "კურირებული მოდელები, მათ შორის Claude, GPT, Gemini და სხვა", + "dialog.provider.opencode.tagline": "სანდო ოპტიმიზებული მოდელები", + "dialog.provider.opencodeGo.tagline": "დაბალფასიანი გამოწერა ყველასთვის", + "dialog.provider.anthropic.note": "პირდაპირი წვდომა Claude მოდელზე, მათ შორის Pro და Max", + "dialog.provider.copilot.note": "AI მოდელები კოდირების დახმარებისთვის GitHub Copilot", + "dialog.provider.openai.note": "GPT მოდელები სწრაფი, ზოგადი AI ამოცანების შესასრულებლად", + "dialog.provider.google.note": "Gemini მოდელები სწრაფი, სტრუქტურირებული პასუხებისთვის", + "dialog.provider.openrouter.note": "წვდომა ყველა მხარდაჭერილ მოდელზე ერთი პროვაიდერისგან", + "dialog.provider.vercel.note": "ერთიანი წვდომა AI მოდელებზე ჭკვიანი მარშრუტით", + "dialog.model.select.title": "აირჩიეთ მოდელი", + "dialog.model.search.placeholder": "მოდელების ძიება", + "dialog.model.empty": "მოდელის შედეგები არ არის", + "dialog.model.manage": "მოდელების მართვა", + "dialog.model.manage.description": "მოარგეთ რომელი მოდელები გამოჩნდება მოდელის ამომრჩეველში.", + "dialog.model.manage.provider.toggle": "ყველა {{provider}} მოდელის გადართვა", + "dialog.model.unpaid.freeModels.title": "უფასო მოდელები მოწოდებულია OpenCode", + "dialog.model.unpaid.addMore.title": "დაამატე მეტი მოდელები პოპულარული პროვაიდერებისგან", + "dialog.model.unpaid.viewMoreProviders": "იხილეთ 70+ სხვა პროვაიდერი", + "dialog.provider.viewAll": "აჩვენე მეტი პროვაიდერი", + "provider.connect.title": "დაკავშირება {{provider}}", + "provider.connect.title.anthropicProMax": "შესვლა Claude Pro/Max-ით", + "provider.connect.selectMethod": "აირჩიეთ შესვლის მეთოდი {{provider}}-ისთვის.", + "provider.connect.method.apiKey": "API გასაღები", + "provider.connect.method.browser": "ბრაუზერი", + "provider.connect.method.headless": "გრაფიკული ინტერფეისის გარეშე", + "provider.connect.status.inProgress": "ავტორიზაცია მიმდინარეობს...", + "provider.connect.status.waiting": "ველოდები ავტორიზაციას...", + "provider.connect.status.failed": "ავტორიზაცია ვერ მოხერხდა: {{error}}", + "provider.connect.apiKey.description": + "შეიყვანეთ თქვენი {{provider}} API გასაღები თქვენი ანგარიშის დასაკავშირებლად და გამოიყენეთ {{provider}} მოდელები OpenCode-ში.", + "provider.connect.apiKey.label": "{{provider}} API გასაღები", + "provider.connect.apiKey.placeholder": "API გასაღები", + "provider.connect.apiKey.required": "API გასაღები საჭიროა", + "provider.connect.opencodeZen.line1": + "OpenCode Zen გაძლევთ წვდომას კოდირების აგენტებისთვის საიმედო ოპტიმიზირებული მოდელების კურირებულ კომპლექტზე.", + "provider.connect.opencodeZen.line2": + "ერთი API გასაღებით თქვენ მიიღებთ წვდომას ისეთ მოდელებზე, როგორიცაა Claude, GPT, Gemini, GLM და სხვა.", + "provider.connect.opencodeZen.visit.prefix": "ეწვიეთ ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " თქვენი API გასაღების შესაგროვებლად.", + "provider.connect.oauth.code.visit.prefix": "ეწვიეთ ", + "provider.connect.oauth.code.visit.link": "ეს ბმული", + "provider.connect.oauth.code.visit.suffix": + " შეაგროვეთ თქვენი ავტორიზაციის კოდი თქვენი ანგარიშის დასაკავშირებლად და გამოიყენოთ {{provider}} მოდელები OpenCode-ში.", + "provider.connect.oauth.code.label": "{{method}} ავტორიზაციის კოდი", + "provider.connect.oauth.code.placeholder": "ავტორიზაციის კოდი", + "provider.connect.oauth.code.required": "საჭიროა ავტორიზაციის კოდი", + "provider.connect.oauth.code.invalid": "არასწორი ავტორიზაციის კოდი", + "provider.connect.oauth.auto.visit.prefix": "ეწვიეთ ", + "provider.connect.oauth.auto.visit.link": "ეს ბმული", + "provider.connect.oauth.auto.visit.suffix": + " და შეიყვანეთ ქვემოთ კოდი თქვენი ანგარიშის დასაკავშირებლად და გამოიყენეთ {{provider}} მოდელები OpenCode-ში.", + "provider.connect.oauth.auto.confirmationCode": "დადასტურების კოდი", + "provider.connect.toast.connected.title": "{{provider}} დაკავშირებული", + "provider.connect.toast.connected.description": "{{provider}} მოდელები ახლა ხელმისაწვდომია გამოსაყენებლად.", + "provider.custom.title": "მომხმარებელი პროვაიდერი", + "provider.custom.unavailable": "მორგებული პროვაიდერები მიუწვდომელია ამ სერვერზე", + "provider.custom.description.prefix": "OpenAI-თავსებადი პროვაიდერის კონფიგურაცია. იხილეთ ", + "provider.custom.description.link": "პროვაიდერის კონფიგურაციის დოკუმენტები", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "პროვაიდერის ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "მცირე ასოები, რიცხვები, დეფისები ან ქვედა ხაზი", + "provider.custom.field.name.label": "საჩვენებელი სახელი", + "provider.custom.field.name.placeholder": "ჩემი AI პროვაიდერი", + "provider.custom.field.baseURL.label": "ბაზა URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API გასაღები", + "provider.custom.field.apiKey.placeholder": "API გასაღები", + "provider.custom.field.apiKey.description": + "სურვილისამებრ. დატოვეთ ცარიელი, თუ თქვენ მართავთ ავტორიზაციას სათაურების მეშვეობით.", + "provider.custom.models.label": "მოდელები", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "სახელი", + "provider.custom.models.name.placeholder": "საჩვენებელი სახელი", + "provider.custom.models.remove": "მოდელის ამოღება", + "provider.custom.models.add": "მოდელის დამატება", + "provider.custom.headers.label": "Headers (სურვილისამებრ)", + "provider.custom.headers.key.label": "ჰედერი", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "მნიშვნელობა", + "provider.custom.headers.value.placeholder": "მნიშვნელობა", + "provider.custom.headers.remove": "სათაურის წაშლა", + "provider.custom.headers.add": "სათაურის დამატება", + "provider.custom.error.providerID.required": "საჭიროა პროვაიდერის ID", + "provider.custom.error.providerID.format": "გამოიყენე მცირე ასოები, რიცხვები, დეფისები ან ქვედა ხაზი", + "provider.custom.error.providerID.exists": "ეს პროვაიდერის ID უკვე არსებობს", + "provider.custom.error.name.required": "საჩვენებელი სახელი აუცილებელია", + "provider.custom.error.baseURL.required": "საჭიროა ბაზის URL", + "provider.custom.error.baseURL.format": "უნდა დაიწყოს http:// ან https://", + "provider.custom.error.required": "საჭირო", + "provider.custom.error.duplicate": "დუბლიკატი", + "provider.disconnect.toast.disconnected.title": "{{provider}} გათიშულია", + "provider.disconnect.toast.disconnected.description": "{{provider}} მოდელები აღარ არის ხელმისაწვდომი.", + "model.tag.free": "უფასო", + "model.tag.latest": "უახლესი", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "ტექსტი", + "model.input.image": "სურათი", + "model.input.audio": "აუდიო", + "model.input.video": "ვიდეო", + "model.input.pdf": "pdf", + "model.tooltip.allows": "ნებას რთავს: {{inputs}}", + "model.tooltip.context.label": "კონტექსტი", + "model.tooltip.inputs": "შეყვანები", + "model.tooltip.model": "მოდელი", + "model.tooltip.provider": "პროვაიდერი", + "model.tooltip.reasoning": "მსჯელობა", + "model.tooltip.reasoning.allowed": "ნებას რთავს მსჯელობას", + "model.tooltip.reasoning.none": "მსჯელობის გარეშე", + "model.tooltip.context": "კონტექსტის ლიმიტი {{limit}}", + "common.search.placeholder": "ძებნა", + "common.goBack": "ნავიგაცია უკან", + "common.goForward": "ნავიგაცია წინ", + "common.loading": "იტვირთება", + "common.loading.ellipsis": "...", + "common.cancel": "გაუქმება", + "common.clear": "გასუფთავება", + "common.open": "გახსნა", + "common.connect": "დაკავშირება", + "common.disconnect": "გათიშვა", + "common.continue": "გაგრძელება", + "common.submit": "გაგზავნა", + "common.save": "შენახვა", + "common.saving": "შენახვა...", + "common.default": "ნაგულისხმევი", + "common.attachment": "დანართი", + "prompt.placeholder.shell": "შეიყვანეთ shell ბრძანება... {{example}}", + "prompt.placeholder.normal": 'იკითხეთ რაიმე... "{{example}}"', + "prompt.placeholder.simple": "იკითხე რაიმე...", + "prompt.placeholder.summarizeComments": "კომენტარების შეჯამება…", + "prompt.placeholder.summarizeComment": "კომენტარის შეჯამება…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "მოთხოვნა", + "prompt.mode.shell.exit": "გასასვლელად დააჭირეთ esc-ს", + "session.child.promptDisabled": "სუბაგენტის სესიების მოთხოვნა შეუძლებელია.", + "session.child.backToParent": "უკან მთავარ სესიაზე.", + "prompt.example.1": "გაასწორეთ TODO კოდების ბაზაში", + "prompt.example.2": "რა არის ამ პროექტის ტექნიკური დასტა?", + "prompt.example.3": "გატეხილი ტესტების შესწორება", + "prompt.example.4": "ახსნა, როგორ მუშაობს ავთენტიფიკაცია", + "prompt.example.5": "იპოვეთ და შეასწორეთ უსაფრთხოების ხარვეზები", + "prompt.example.6": "დაამატეთ ერთეულის ტესტები მომხმარებლის სერვისისთვის", + "prompt.example.7": "გააკეთეთ ეს ფუნქცია უფრო წასაკითხად", + "prompt.example.8": "რას ნიშნავს ეს შეცდომა?", + "prompt.example.9": "დამეხმარეთ ამ საკითხის გამართვა", + "prompt.example.10": "შექმენით API დოკუმენტაცია", + "prompt.example.11": "მონაცემთა ბაზის მოთხოვნების ოპტიმიზაცია", + "prompt.example.12": "შეყვანის ვალიდაციის დამატება", + "prompt.example.13": "შექმენით ახალი კომპონენტი...", + "prompt.example.14": "როგორ განვათავსო ეს პროექტი?", + "prompt.example.15": "მიმოიხილე ჩემი კოდი საუკეთესო პრაქტიკისთვის", + "prompt.example.16": "შეცდომის დამუშავების დამატება ამ ფუნქციას", + "prompt.example.17": "განმარტეთ ეს რეგექსის ნიმუში", + "prompt.example.18": "გადააკეთე ეს TypeScript-ად", + "prompt.example.19": "დაამატეთ ჟურნალი კოდების ბაზაში", + "prompt.example.20": "რა დამოკიდებულებებია მოძველებული?", + "prompt.example.21": "დამეხმარეთ მიგრაციის სკრიპტის დაწერაში", + "prompt.example.22": "ქეშირების განხორციელება ამ ბოლო წერტილისთვის", + "prompt.example.23": "დაამატე გვერდი ამ სიაში", + "prompt.example.24": "შექმენით CLI ბრძანება...", + "prompt.example.25": "როგორ მუშაობს აქ გარემოს ცვლადები?", + "prompt.popover.emptyResults": "შესაბამისი შედეგები არ არის", + "prompt.popover.emptyCommands": "შესაბამისი ბრძანებები არ არის", + "prompt.dropzone.label": "ჩააგდე სურათები, PDF ან ტექსტური ფაილები აქ", + "prompt.dropzone.file.label": "ჩაშვება @mention ფაილზე", + "prompt.slash.badge.custom": "მორგებული", + "prompt.slash.badge.skill": "უნარი", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "აქტიური", + "prompt.context.includeActiveFile": "აქტიური ფაილის ჩართვა", + "prompt.context.removeActiveFile": "აქტიური ფაილის ამოღება კონტექსტიდან", + "prompt.context.removeFile": "ფაილის ამოღება კონტექსტიდან", + "prompt.action.attachFile": "ფაილების დამატება", + "prompt.menu.addImagesAndFiles": "ფაილების დამატება და სხვა", + "prompt.menu.imagesAndFiles": "სურათები და ფაილები", + "prompt.menu.commands": "ბრძანებები", + "prompt.menu.context": "კონტექსტი", + "prompt.menu.shellCommand": "Shell ბრძანება", + "prompt.attachment.remove": "დანართის წაშლა", + "prompt.action.send": "გაგზავნა", + "prompt.action.stop": "შეჩერება", + "prompt.toast.pasteUnsupported.title": "მხარდაუჭერელი დანართი", + "prompt.toast.pasteUnsupported.description": "აქ შეიძლება დაერთოს მხოლოდ სურათები, PDF ან ტექსტური ფაილები.", + "prompt.toast.attachmentDuplicate.title": "ეს ფაილი უკვე ატვირთულია", + "prompt.toast.modelAgentRequired.title": "აირჩიეთ აგენტი და მოდელი", + "prompt.toast.modelAgentRequired.description": "აირჩიეთ აგენტი და მოდელი მოთხოვნის გაგზავნამდე.", + "prompt.toast.worktreeCreateFailed.title": "ვერ შეიქმნა სამუშაო ხე", + "prompt.toast.sessionCreateFailed.title": "სესიის შექმნა ვერ მოხერხდა", + "prompt.toast.shellSendFailed.title": "Shell ბრძანების გაგზავნა ვერ მოხერხდა", + "prompt.toast.commandSendFailed.title": "ბრძანების გაგზავნა ვერ მოხერხდა", + "prompt.toast.promptSendFailed.title": "მოთხოვნის გაგზავნა ვერ მოხერხდა", + "prompt.toast.promptSendFailed.description": "შეუძლებელია სესიის მოძიება", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} / {{total}}-დან ჩართულია", + "dialog.mcp.empty": "არა MCPs კონფიგურირებული", + "dialog.lsp.empty": "LSPs ავტომატურად გამოვლენილია ფაილის ტიპებიდან", + "dialog.plugins.empty": "პლაგინები კონფიგურირებულია opencode.json-ში", + "mcp.status.connected": "დაკავშირებულია", + "mcp.status.failed": "ვერ მოხერხდა", + "mcp.status.needs_auth": "საჭიროა ავტორიზაცია", + "mcp.status.disabled": "გამორთულია", + "mcp.auth.clickToAuthenticate": "დააწკაპუნეთ ავთენტიფიკაციისთვის", + "dialog.fork.empty": "განშტოებისთვის შეტყობინებები არ არის", + "dialog.directory.search.placeholder": "საქაღალდეების ძიება", + "dialog.directory.empty": "საქაღალდეები არ მოიძებნა", + "dialog.directory.action.selectFile": "აირჩიეთ ფაილი", + "dialog.directory.action.selectFolder": "აირჩიე საქაღალდე", + "dialog.directory.root": "ძირეული", + "dialog.directory.parent": "მშობელი", + "dialog.directory.readError": "ამ საქაღალდის წაკითხვა შეუძლებელია", + "app.server.unreachable": "ვერ მივაღწიე {{server}}", + "app.server.retrying": "ვცდი ავტომატურად...", + "app.server.otherServers": "სხვა სერვერები", + "dialog.server.title": "სერვერები", + "dialog.server.description": "გადართეთ რომელ OpenCode სერვერს უკავშირდება ეს აპი.", + "dialog.server.search.placeholder": "სერვერების ძებნა", + "dialog.server.empty": "სერვერები ჯერ არ არის", + "dialog.server.add.title": "სერვერის დამატება", + "dialog.server.add.url": "სერვერის მისამართი", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "სერვერთან დაკავშირება ვერ მოხერხდა", + "dialog.server.add.checking": "შემოწმება...", + "dialog.server.add.button": "სერვერის დამატება", + "dialog.server.add.name": "სერვერის სახელი (არასავალდებულო)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "მომხმარებლის სახელი (არასავალდებულო)", + "dialog.server.add.usernamePlaceholder": "მომხმარებლის სახელი", + "dialog.server.add.password": "პაროლი (არასავალდებულო)", + "dialog.server.add.passwordPlaceholder": "პაროლი", + "dialog.server.edit.title": "სერვერის რედაქტირება", + "dialog.server.default.title": "ნაგულისხმევი სერვერი", + "dialog.server.default.description": + "დაკავშირება ამ სერვერთან აპის გაშვებისას, ადგილობრივი სერვერის გაშვების ნაცვლად. საჭიროებს გადატვირთვას.", + "dialog.server.default.none": "სერვერი არ არის არჩეული", + "dialog.server.default.set": "მიმდინარე სერვერის დაყენება ნაგულისხმევად", + "dialog.server.default.clear": "გასუფთავება", + "dialog.server.action.remove": "სერვერის წაშლა", + "dialog.server.menu.edit": "რედაქტირება", + "dialog.server.menu.default": "დაყენება ნაგულისხმევად", + "dialog.server.menu.defaultRemove": "ნაგულისხმევის ამოღება", + "dialog.server.menu.delete": "წაშლა", + "dialog.server.current": "მიმდინარე სერვერი", + "dialog.server.status.default": "ნაგულისხმევი", + "wsl.server.add": "დაამატე WSL სერვერი", + "wsl.server.addShort": "დაამატე WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL სერვერი", + "wsl.server.retryStart": "სცადეთ ხელახლა დაწყება", + "wsl.server.updating": "განახლება...", + "wsl.onboarding.step.distro": "აირჩიე დისტრო", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "შემოწმებულია WSL...", + "wsl.onboarding.restartRequired": "Windows საჭიროებს გადატვირთვას WSL-ის ინსტალაციის დასასრულებლად.", + "wsl.onboarding.ready": "WSL მზად არის.", + "wsl.onboarding.required": "WSL საჭიროა გასაგრძელებლად.", + "wsl.onboarding.checkingDistros": "გავრცელების შემოწმება...", + "wsl.onboarding.installingDistro": "ინსტალაცია {{distro}}...", + "wsl.onboarding.checkingDistro": "შემოწმებულია {{distro}}...", + "wsl.onboarding.listingDistros": "Listing Distros...", + "wsl.onboarding.distroReady": "{{distro}} მზად არის.", + "wsl.onboarding.distroNotInstalled": "{{distro}} ჯერ არ არის დაინსტალირებული.", + "wsl.onboarding.openDistroOnce": "გახსენით {{distro}} ერთხელ დაყენების დასასრულებლად.", + "wsl.onboarding.finishingDistro": "{{distro}}-ისთვის დაყენების დასრულება.", + "wsl.onboarding.pickDistro": "აირჩიეთ დისტრო ან დააინსტალირეთ ერთი ქვემოთ.", + "wsl.onboarding.checkingOpencode": "შემოწმებულია OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "შემოწმებულია OpenCode-ში {{distro}}...", + "wsl.onboarding.updatingOpencode": "განახლება OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "OpenCode განახლება ხდება {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "განახლება OpenCode {{distro}}-ში.", + "wsl.onboarding.updateOpencode": "განახლება OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode მზად არის {{distro}}-ში.", + "wsl.onboarding.opencodeReady": "OpenCode მზად არის.", + "wsl.onboarding.installOpencodeIn": "დააინსტალირეთ OpenCode {{distro}}-ში.", + "wsl.onboarding.installOpencode": "დააინსტალირეთ OpenCode", + "wsl.onboarding.chooseDistroFirst": "აირჩიეთ ჯერ დისტრო.", + "wsl.onboarding.loadFailed": "ვერ ჩაიტვირთა WSL მდგომარეობა.", + "wsl.onboarding.loading": "იტვირთება...", + "wsl.onboarding.installedDistros": "დაინსტალირებული დისტროები", + "wsl.onboarding.checkAgain": "გადაამოწმეთ", + "wsl.onboarding.distroStatus.ready": "მზად", + "wsl.onboarding.distroStatus.checking": "შემოწმება...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode არ არის დაინსტალირებული", + "wsl.onboarding.distroStatus.missingTools": "აკლია bash და curl", + "wsl.onboarding.distroStatus.unsupported": "მხარდაუჭერელია · გამოიყენეთ WSL 2", + "wsl.onboarding.needAnotherDistro": "გჭირდებათ სხვა დისტრო?", + "wsl.onboarding.needAnotherDistroHint": "დააინსტალირეთ Linux დისტრიბუცია WSL კატალოგიდან", + "wsl.onboarding.wslNotInstalled.title": "WSL არ არის დაინსტალირებული", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows ქვესისტემა Linux-ისთვის) საჭიროა, სანამ OpenCode შეძლებს WSL სერვერის დამატებას", + "wsl.onboarding.wslUnavailable.title": "WSL მიუწვდომელია", + "wsl.onboarding.wslUnavailable.description": "OpenCode ვერ დაადასტურა WSL ამ მოწყობილობაზე.", + "wsl.onboarding.installWsl": "დააინსტალირეთ WSL", + "wsl.onboarding.windowsRestartRequired": + "გადატვირთეთ Windows WSL-ის ინსტალაციის დასასრულებლად, შემდეგ ხელახლა გახსენით OpenCode.", + "wsl.onboarding.next": "შემდეგი", + "wsl.onboarding.refresh": "განახლება", + "wsl.onboarding.allDistrosAdded": "ყველა დაინსტალირებული დისტრო უკვე დამატებულია.", + "wsl.onboarding.noDistros": "გავრცელება ჯერ არ არის გამოვლენილი.", + "wsl.onboarding.install": "ინსტალაცია", + "wsl.onboarding.installing": "ინსტალაცია...", + "wsl.onboarding.installDistro": "Install Distro", + "wsl.onboarding.searchDistros": "ძიების დისტრიბუციები", + "wsl.onboarding.wsl2Required": "WSL 2 საჭიროა.", + "wsl.onboarding.toolsRequired": "ამ დისტროს სჭირდება bash და curl.", + "wsl.onboarding.openTerminal": "ტერმინალის გახსნა", + "wsl.onboarding.path": "გზა: {{path}}", + "wsl.onboarding.notFound": "ვერ მოიძებნა", + "wsl.onboarding.version": "ვერსია: {{version}}", + "wsl.onboarding.unknown": "უცნობი", + "wsl.onboarding.desktopVersion": "დესკტოპის {{version}}", + "wsl.onboarding.versionMismatch": "დაინსტალირებული ვერსია არ ემთხვევა დესკტოპის აპის ვერსიას.", + "wsl.onboarding.adding": "დამატება...", + "help.tabs.toast.ariaLabel": "გაცნობა ჩანართებს. მოაწყეთ თქვენი სამუშაო და აქტიური სესიები ჩანართებით", + "help.tabs.toast.dismiss": "ჩანართების ინფორმაციის უარყოფა", + "help.tabs.title": "ჩანართების გაცნობა", + "help.tabs.description": "მოაწყვეთ თქვენი სამუშაო და აქტიური სესიები ჩანართებით", + "help.tabs.date": "14 ივლისი", + "help.tabs.introduction": "OpenCode სამუშაო მაგიდა ახლა აგებულია ჩანართების გარშემო.", + "help.tabs.sessions": + "დაიწყეთ ახალი სესია ჩანართში, ან გახსენით არსებული სესია თქვენი რომელიმე პროექტიდან. გახსენით ახალი ჩანართი, როდესაც რაიმე ახალს იწყებთ და დახურეთ, როდესაც დაასრულებთ.", + "help.tabs.organize": + "რამდენიმე ჩანართის გახსნა გაადვილებს თქვენი აქტიური სესიების ორგანიზებას. გადაარქვით ჩანართებს რაიმე დასამახსოვრებლად, თუ აპირებთ მათ შენარჩუნებას.", + "help.tabs.home": + "თქვენ იპოვით ყველა თქვენს სესიას და პროექტს ახალ მთავარ ეკრანზე. სესიის არჩევა ხსნის მას ჩანართში.", + "help.tabs.persistence": "აპის ხელახლა გახსნისას, თქვენი ჩანართები კვლავ ღიაა.", + "help.tabs.worktrees": + "ახალი დიზაინი ჯერ არ უჭერს მხარს Git Worktrees-ს, ის მალე გამოვა. ასე რომ, თუ გსურთ გააგრძელოთ წინა განლაგების გამოყენება, შეგიძლიათ გადართოთ განლაგებებს შორის პარამეტრებიდან. უბრალოდ გაითვალისწინეთ, რომ ახალი განლაგება მუდმივი გახდება რამდენიმე კვირაში.", + "server.row.noUsername": "მომხმარებლის სახელის გარეშე", + "dialog.project.edit.title": "პროექტის რედაქტირება", + "dialog.project.edit.name": "სახელი", + "dialog.project.edit.icon": "ხატულა", + "dialog.project.edit.icon.alt": "პროექტის ხატულა", + "dialog.project.edit.icon.hint": "დააწკაპუნეთ ან გადაიტანეთ სურათზე", + "dialog.project.edit.icon.recommended": "რეკომენდებულია: 128x128px", + "dialog.project.edit.color": "ფერი", + "dialog.project.edit.color.select": "აირჩიეთ {{color}} ფერი", + "dialog.project.edit.worktree.startup": "სამუშაო სივრცის გაშვების სკრიპტი", + "dialog.project.edit.worktree.startup.description": "იშვება ახალი სამუშაო სივრცის (სამუშაო ხის) შექმნის შემდეგ", + "dialog.project.edit.worktree.startup.placeholder": "მაგ. bun ინსტალაცია", + "dialog.releaseNotes.action.getStarted": "დაწყება", + "dialog.releaseNotes.action.next": "შემდეგი", + "dialog.releaseNotes.action.hideFuture": "მომავალში არ მაჩვენო", + "dialog.releaseNotes.media.alt": "გამოშვების გადახედვა", + "context.breakdown.title": "კონტექსტის დაშლა", + "context.breakdown.note": 'შეყვანის ნიშნების სავარაუდო დაშლა. "სხვა" მოიცავს ხელსაწყოს განმარტებებს და ზედნადებს.', + "context.breakdown.system": "სისტემა", + "context.breakdown.user": "მომხმარებელი", + "context.breakdown.assistant": "ასისტენტი", + "context.breakdown.tool": "ხელსაწყოს გამოძახებები", + "context.breakdown.other": "სხვა", + "context.systemPrompt.title": "სისტემის მოთხოვნა", + "context.rawMessages.title": "დაუმუშავებელი შეტყობინებები", + "context.export.session": "ექსპორტის სესია", + "context.stats.session": "სესია", + "context.stats.messages": "შეტყობინებები", + "context.stats.provider": "პროვაიდერი", + "context.stats.model": "მოდელი", + "context.stats.limit": "კონტექსტის ლიმიტი", + "context.stats.totalTokens": "სულ ტოკენები", + "context.stats.usage": "გამოყენება", + "context.stats.inputTokens": "შეყვანის ნიშნები", + "context.stats.outputTokens": "გამომავალი ტოკენები", + "context.stats.reasoningTokens": "მსჯელობის ტოკენები", + "context.stats.cacheTokens": "ქეში ტოკენები (წაკითხვა/ჩაწერა)", + "context.stats.userMessages": "მომხმარებლის შეტყობინებები", + "context.stats.assistantMessages": "ასისტენტის შეტყობინებები", + "context.stats.totalCost": "საერთო ღირებულება", + "context.stats.sessionCreated": "სესია შეიქმნა", + "context.stats.lastActivity": "ბოლო აქტივობა", + "context.usage.tokens": "ტოკენები", + "context.usage.usage": "გამოყენება", + "context.usage.cost": "ღირებულება", + "context.usage.clickToView": "დააწკაპუნეთ კონტექსტის სანახავად", + "context.usage.view": "კონტექსტის გამოყენების ნახვა", + "language.en": "ინგლისური", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "დანსკი", + "language.ja": "日本語", + "language.pl": "პოლსკი", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "ნორსკი", + "language.br": "პორტუგალია (ბრაზილია)", + "language.bs": "ბოსანსკი", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "ენა", + "toast.language.description": "გადართულია {{language}}", + "toast.theme.title": "თემა შეიცვალა", + "toast.scheme.title": "ფერთა სქემა", + "toast.workspace.enabled.title": "სამუშაო სივრცე ჩართულია", + "toast.workspace.enabled.description": "მრავალი სამუშაო ხე ახლა ნაჩვენებია გვერდითა ზოლში", + "toast.workspace.disabled.title": "სამუშაო სივრცე გამორთულია", + "toast.workspace.disabled.description": "მხოლოდ ძირითადი სამუშაო ხე ნაჩვენებია გვერდითა ზოლში", + "toast.permissions.autoaccept.on.title": "ნებართვების ავტომატური მიღება", + "toast.permissions.autoaccept.on.description": "ნებართვის მოთხოვნები ავტომატურად დამტკიცდება", + "toast.permissions.autoaccept.off.title": "შეწყდა ნებართვების ავტომატური მიღება", + "toast.permissions.autoaccept.off.description": "ნებართვის მოთხოვნა საჭიროებს დამტკიცებას", + "toast.model.none.title": "მოდელი არ არის არჩეული", + "toast.model.none.description": "დააკავშირეთ პროვაიდერი ამ სესიის შესაჯამებლად", + "toast.file.loadFailed.title": "ფაილის ჩატვირთვა ვერ მოხერხდა", + "toast.file.listFailed.title": "ფაილების სია ვერ მოხერხდა", + "toast.context.noLineSelection.title": "ხაზი არ არის მონიშნული", + "toast.context.noLineSelection.description": "აირჩიეთ ხაზის დიაპაზონი ჯერ ფაილის ჩანართში.", + "toast.session.share.copyFailed.title": "ვერ მოხერხდა URL-ის კოპირება ბუფერში", + "toast.session.share.success.title": "სესია გაზიარებულია", + "toast.session.share.success.description": "გააზიარეთ URL კოპირებულია ბუფერში!", + "toast.session.share.failed.title": "სესიის გაზიარება ვერ მოხერხდა", + "toast.session.share.failed.description": "მოხდა შეცდომა სესიის გაზიარებისას", + "toast.session.unshare.success.title": "სესია გაუზიარებელი", + "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": "ხელმისაწვდომია განახლება", + "toast.update.description": "OpenCode-ის ({{version}}) ახალი ვერსია ახლა ხელმისაწვდომია დასაინსტალირებლად.", + "toast.update.action.installRestart": "ინსტალაცია და გადატვირთვა", + "toast.update.action.notYet": "ჯერ არა", + "error.page.title": "რაღაც შეცდომა მოხდა", + "error.page.description": "მოხდა შეცდომა აპლიკაციის ჩატვირთვისას.", + "error.page.description.localServerStartup": "მოხდა შეცდომა ლოკალური სერვერის გაშვებისას.", + "error.page.details.label": "შეცდომის დეტალები", + "error.page.action.restart": "გადატვირთვა", + "error.page.action.report": "შეცდომის შესახებ შეტყობინება", + "error.page.action.reported": "მოხსენებული შეცდომა", + "error.page.action.exportLogs": "ჟურნალების ექსპორტი", + "error.page.action.checking": "შემოწმება...", + "error.page.action.checkUpdates": "შეამოწმეთ განახლებები", + "error.page.action.updateTo": "განახლება {{version}}", + "error.page.circular": "[Circular]", + "error.page.report.prefix": "გთხოვთ, შეატყობინოთ ეს შეცდომა OpenCode გუნდს", + "error.page.report.discord": "Discord", + "error.page.version": "ვერსია: {{version}}", + "error.dev.rootNotFound": + "ძირის ელემენტი ვერ მოიძებნა. დაგავიწყდათ მისი დამატება თქვენს index.html-ში? ან იქნებ id ატრიბუტი არასწორად არის დაწერილი?", + "error.serverSync.connectFailed": "სერვერთან დაკავშირება ვერ მოხერხდა. არის სერვერი გაშვებული `{{url}}`-ზე?", + "error.serverSDK.noServerAvailable": "სერვერი არ არის ხელმისაწვდომი", + "error.serverSDK.serverNotAvailable": "სერვერი მიუწვდომელია", + "error.childStore.persistedCacheCreateFailed": "შეუძლებელია ქეშის შექმნა", + "error.childStore.persistedProjectMetadataCreateFailed": "პროექტის მუდმივი მეტამონაცემების შექმნა ვერ მოხერხდა", + "error.childStore.persistedProjectIconCreateFailed": "პროექტის მუდმივი ხატის შექმნა ვერ მოხერხდა", + "error.childStore.storeCreateFailed": "მაღაზიის შექმნა ვერ მოხერხდა", + "directory.error.invalidUrl": "არასწორი დირექტორია URL-ში.", + "error.chain.unknown": "უცნობი შეცდომა", + "error.server.invalidConfiguration": "არასწორი კონფიგურაცია", + "error.chain.causedBy": "გამომწვევია:", + "error.chain.apiError": "API შეცდომა", + "error.chain.status": "სტატუსი: {{status}}", + "error.chain.retryable": "ხელახლა ცდა: {{retryable}}", + "error.chain.responseBody": "პასუხის სხეული:\n{{body}}", + "error.chain.didYouMean": "იგულისხმეთ: {{suggestions}}", + "error.chain.modelNotFound": "მოდელი ვერ მოიძებნა: {{provider}}/{{model}}", + "error.chain.checkConfig": "შეამოწმეთ თქვენი კონფიგურაცია (opencode.json) პროვაიდერის/მოდელების სახელები", + "error.chain.mcpFailed": + 'MCP სერვერი "{{name}}" ვერ მოხერხდა. შენიშვნა, OpenCode ჯერ არ უჭერს მხარს MCP ავთენტიფიკაციას.', + "error.chain.providerAuthFailed": "პროვაიდერის ავთენტიფიკაცია ვერ მოხერხდა ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + "ვერ მოხერხდა პროვაიდერის „{{provider}}“ ინიციალიზაცია. შეამოწმეთ რწმუნებათა სიგელები და კონფიგურაცია.", + "error.chain.configJsonInvalid": "კონფიგურაციის ფაილი {{path}}-ზე არ არის სწორი JSON(C)", + "error.chain.configJsonInvalidWithMessage": "კონფიგურაციის ფაილი {{path}}-ზე არ არის სწორი JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'ცნობარი "{{dir}}" {{path}}-ში არასწორია. დაარქვით დირექტორიას "{{suggestion}}" ან წაშალეთ იგი. ეს ჩვეულებრივი შეცდომაა.', + "error.chain.configFrontmatterError": "ვერ მოხერხდა ფრონტის მასალის გაანალიზება {{path}}-ში:\n{{message}}", + "error.chain.configInvalid": "კონფიგურაციის ფაილი {{path}}-ზე არასწორია", + "error.chain.configInvalidWithMessage": "კონფიგურაციის ფაილი {{path}}-ზე არასწორია: {{message}}", + "notification.permission.title": "ნებართვა საჭიროა", + "notification.permission.description": "{{sessionTitle}} {{projectName}}-ში საჭიროებს ნებართვას", + "notification.question.title": "კითხვა", + "notification.question.description": "{{sessionTitle}} {{projectName}}-ში აქვს შეკითხვა", + "notification.action.goToSession": "სესიაზე გადასვლა", + "notification.session.responseReady.title": "პასუხი მზად არის", + "notification.session.error.title": "სესიის შეცდომა", + "notification.session.error.fallbackDescription": "მოხდა შეცდომა", + "home.recentProjects": "ბოლო პროექტები", + "home.empty.title": "უახლესი პროექტები არ არის", + "home.empty.description": "დაიწყეთ ლოკალური პროექტის გახსნით", + "home.title": "მთავარი", + "home.projects": "პროექტები", + "home.project.add": "პროექტის დამატება", + "home.recentlyClosed": "ახლახან დაიხურა", + "home.server.collapse": "სერვერის პროექტების ჩაკეცვა", + "home.server.expand": "სერვერის პროექტების გაფართოება", + "home.sessions.search.placeholder": "ძიების სესიები", + "home.sessions.search.placeholder.scoped": "ძიების სესიები {{scope}}", + "home.sessions.search.sessions": "სესიები", + "home.sessions.search.noResults": "სესიები არ მოიძებნა {{query}}", + "home.sessions.empty": "აქ ჯერ არაფერი", + "home.sessions.empty.description": "შექმენით სესია დასაწყებად", + "home.sessions.group.today": "დღეს", + "home.sessions.group.yesterday": "გუშინ", + "home.sessions.group.older": "ძველი", + "home.providerTip": + "დაკავშირება 75+ პროვაიდერთან სხვა მოდელების გამოსაყენებლად, მათ შორის, Claude, GPT, Gemini და ა.შ.", + "session.tab.session": "სესია", + "session.tab.review": "მიმოხილვა", + "session.tab.context": "კონტექსტი", + "session.tab.unknown": "უცნობი სესია", + "session.panel.reviewAndFiles": "მიმოხილვა და ფაილები", + "session.error.notFound": "ეს სესია ვერ მოიძებნა", + "session.error.notFound.description": "ეს ჩანართი მიუთითებს სესიაზე, რომელიც აღარ არსებობს ამ სერვერზე.", + "session.error.notFound.closeTab": "ჩანართის დახურვა", + "session.error.serverConnection": "ამ სერვერთან დაკავშირება შეუძლებელია", + "session.review.filesChanged": "ფაილები შეიცვალა {{count}}", + "session.review.change.one": "შეცვლა", + "session.review.change.other": "ცვლილებები", + "session.review.loadingChanges": "ცვლილებების ჩატვირთვა...", + "session.review.empty": "ამ სესიაში ცვლილებები ჯერ არ არის", + "session.review.noVcs": "არ არის აღმოჩენილი Git ვერსიის კონტროლის სისტემა, ცვლილებები არ არის ნაჩვენები", + "session.review.noVcs.createGit.title": "შექმენით Git საცავი", + "session.review.noVcs.createGit.description": "თვალყური ადევნეთ, გადახედეთ და გააუქმეთ ცვლილებები ამ პროექტში", + "session.review.noVcs.createGit.actionLoading": "Git საცავის შექმნა...", + "session.review.noVcs.createGit.action": "შექმენით Git საცავი", + "session.review.noSnapshot": "Snapshot tracking გამორთულია კონფიგურაციაში, ამიტომ სესიის ცვლილებები მიუწვდომელია", + "session.review.noChanges": "ცვლილებები არ არის", + "session.review.noUncommittedChanges": "შეუსრულებელი ცვლილებები ჯერ არ არის", + "session.review.noBranchChanges": "ფილიალი ჯერ არ არის ცვლილებები", + "session.files.selectToOpen": "აირჩიეთ ფაილი გასახსნელად", + "session.files.all": "ყველა ფაილი", + "session.files.empty": "ფაილები არ არის", + "session.files.binaryContent": "ორობითი ფაილი (კონტენტის ჩვენება შეუძლებელია)", + "session.messages.renderEarlier": "ადრე შეტყობინებების რენდერი", + "session.messages.loadingEarlier": "ვიტვირთება ადრინდელი შეტყობინებები...", + "session.messages.loadEarlier": "ადრე შეტყობინებების ჩატვირთვა", + "session.messages.loading": "იტვირთება შეტყობინებები...", + "session.messages.jumpToLatest": "გადადით უახლესზე", + "session.context.addToContext": "დაამატე {{selection}} კონტექსტში", + "session.todo.title": "დავალებები", + "session.todo.collapse": "ჩაკეცვა", + "session.todo.expand": "გაფართოება", + "session.todo.progress": "{{done}} დასრულებული {{total}}-დან", + "session.question.progress": "{{current}} / {{total}} შეკითხვადან", + "session.question.minimize": "შეკითხვის მინიმიზაცია", + "session.question.restore": "კითხვის აღდგენა", + "session.question.pending.one": "{{count}} მომლოდინე შეკითხვა", + "session.question.pending.other": "{{count}} მომლოდინე შეკითხვა", + "session.followupDock.summary.one": "{{count}} რიგში დადებული შეტყობინება", + "session.followupDock.summary.other": "{{count}} რიგში შემავალი შეტყობინებები", + "session.followupDock.sendNow": "გაგზავნა ახლა", + "session.followupDock.edit": "რედაქტირება", + "session.followupDock.collapse": "შეტყობინებების რიგში ჩაკეცვა", + "session.followupDock.expand": "შეტყობინებების რიგში გაფართოება", + "session.revertDock.summary.one": "{{count}} შემობრუნებული შეტყობინება", + "session.revertDock.summary.other": "{{count}} შემობრუნებული შეტყობინებები", + "session.revertDock.collapse": "გაბრუნებული შეტყობინებების ჩაკეცვა", + "session.revertDock.expand": "გაფართოვებული შეტყობინებების გაფართოება", + "session.revertDock.restore": "წერილის აღდგენა", + "session.new.title": "აშენეთ რამე", + "session.new.project.new": "ახალი პროექტი", + "session.new.project.search": "პროექტების ძიება", + "session.new.project.add": "პროექტის დამატება", + "session.new.worktree.main": "მთავარი ფილიალი", + "session.new.worktree.mainWithBranch": "მთავარი ფილიალი ({{branch}})", + "session.new.worktree.create": "შექმენით ახალი სამუშაო ხე", + "session.new.workspace.runIn": "სესიის გაშვება", + "session.new.workspace.triggerLocal": "ადგილობრივი", + "session.new.workspace.local": "ადგილობრივი საცავი", + "session.new.workspace.existing": "სამუშაო სივრცე…", + "session.new.git.none": "Git-ის გარეშე", + "session.new.lastModified": "ბოლო ცვლილება", + "session.header.search.placeholder": "ძებნა {{project}}", + "session.header.searchFiles": "ფაილების ძიება", + "session.header.openIn": "გახსნა", + "session.header.open.action": "გახსენით {{app}}", + "session.header.open.ariaLabel": "გახსენით {{app}}", + "session.header.open.menu": "გახსნა პარამეტრები", + "session.header.open.copyPath": "ასლის ბილიკი", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "ფაილების მენეჯერი", + "session.header.open.fileManager": "ფაილ მენეჯერი", + "session.header.reveal.finder": "გამოვლენა Finder", + "session.header.reveal.fileExplorer": "გამოვლენა File Explorer-ში", + "session.header.reveal.containingFolder": "გახსენით შემცველი საქაღალდე", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "ტერმინალი", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "სტატუსი", + "status.popover.ariaLabel": "სერვერის კონფიგურაციები", + "status.popover.tab.servers": "სერვერები", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "დანამატები", + "status.popover.action.manageServers": "სერვერების მართვა", + "session.share.popover.title": "გამოქვეყნება ვებზე", + "session.share.popover.description.shared": "ეს სესია საჯაროა ინტერნეტში. ის ხელმისაწვდომია ყველასთვის ბმულით.", + "session.share.popover.description.unshared": + "სესიის საჯაროდ გაზიარება ვებში. ის ყველასთვის ხელმისაწვდომი იქნება ბმულით.", + "session.share.action.share": "გაზიარება", + "session.share.action.publish": "გამოქვეყნება", + "session.share.action.publishing": "გამოქვეყნება...", + "session.share.action.unpublish": "გამოქვეყნების გაუქმება", + "session.share.action.unpublishing": "გამოქვეყნების გაუქმება", + "session.share.action.view": "ნახვა", + "session.share.copy.copied": "კოპირებულია", + "session.share.copy.copyLink": "ბმულის კოპირება", + "lsp.tooltip.none": "LSP სერვერები არ არის", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "იტვირთება მოთხოვნა...", + "terminal.loading": "იტვირთება ტერმინალი...", + "terminal.title": "ტერმინალი", + "terminal.title.numbered": "ტერმინალი {{number}}", + "terminal.close": "ტერმინალის დახურვა", + "terminal.connectionLost.title": "კავშირი დაიკარგა", + "terminal.connectionLost.abnormalClose": "WebSocket დახურულია არანორმალურად: {{code}}", + "terminal.connectionLost.description": "ტერმინალის კავშირი შეწყდა. ეს შეიძლება მოხდეს სერვერის გადატვირთვისას.", + "terminal.connectTicket.csrfError": + "PTY დამაკავშირებელი ბილეთი უარყოფილია წარმოშობის ან CSRF ჩეკებით. შეამოწმეთ სერვერის CORS კონფიგურაცია.", + "terminal.connectTicket.statusError": "PTY დაკავშირების ბილეთი ვერ მოხერხდა {{status}}", + "titlebar.update": "განახლება", + "titlebar.updateVersion": "განახლება {{version}}", + "common.closeTab": "ჩანართის დახურვა", + "common.dismiss": "დატოვება", + "common.moreCountSuffix": " (+{{count}} მეტი)", + "common.requestFailed": "მოთხოვნა ვერ მოხერხდა", + "common.moreOptions": "მეტი პარამეტრები", + "common.learnMore": "შეიტყვეთ მეტი", + "common.rename": "გადარქმევა", + "common.export": "ექსპორტი", + "common.reset": "გადატვირთვა", + "common.archive": "არქივი", + "common.delete": "წაშლა", + "common.close": "დახურვა", + "common.edit": "რედაქტირება", + "common.loadMore": "ჩატვირთეთ მეტი", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "უცნობი", + "common.time.justNow": "მხოლოდ ახლა", + "common.time.minutesAgo.short": "{{count}}წთ წინ", + "common.time.hoursAgo.short": "{{count}}სთ წინ", + "common.time.daysAgo.short": "{{count}}დღის წინ", + "sidebar.menu.toggle": "გადართვა მენიუ", + "sidebar.nav.projectsAndSessions": "პროექტები და სესიები", + "sidebar.settings": "პარამეტრები", + "sidebar.help": "დახმარება", + "sidebar.workspaces.enable": "სამუშაო სივრცის ჩართვა", + "sidebar.workspaces.disable": "სამუშაო სივრცის გამორთვა", + "sidebar.gettingStarted.title": "დაწყება", + "sidebar.gettingStarted.line1": "OpenCode მოიცავს უფასო მოდელებს, ასე რომ თქვენ შეგიძლიათ დაუყოვნებლივ დაიწყოთ.", + "sidebar.gettingStarted.line2": + "დააკავშირეთ ნებისმიერი პროვაიდერი მოდელების გამოსაყენებლად, inc. Claude, GPT, Gemini და სხვ.", + "sidebar.project.recentSessions": "ბოლო სესიები", + "sidebar.project.viewAllSessions": "ყველა სესიის ნახვა", + "sidebar.project.clearNotifications": "შეტყობინებების გასუფთავება", + "sidebar.empty.title": "პროექტები არ არის გახსნილი", + "sidebar.empty.description": "გახსენით პროექტი დასაწყებად", + "debugBar.ariaLabel": "განვითარების შესრულების დიაგნოსტიკა", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "ბოლო დასრულებული მარშრუტის გადასვლა სესიის გვერდთან შეხებით, გაზომილი როუტერის დაწყებიდან პირველ შეღებვამდე მისი დალაგების შემდეგ.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "მოძრავი კადრები წამში ბოლო 5 წამის განმავლობაში.", + "debugBar.frame.label": "კადრი", + "debugBar.frame.tip": "ყველაზე ცუდი კადრების დრო ბოლო 5 წამის განმავლობაში.", + "debugBar.jank.label": "შეფერხება", + "debugBar.jank.tip": "ჩარჩოები 32 ms-ზე მეტი ბოლო 5 წამში.", + "debugBar.long.label": "ხანგრძლივი", + "debugBar.long.tip": "დაბლოკილი დრო და ხანგრძლივი ამოცანების რაოდენობა ბოლო 5 წამში. მაქსიმალური დავალება: {{max}}.", + "debugBar.delay.label": "დაყოვნება", + "debugBar.delay.tip": "შეყვანის ყველაზე უარესი შეფერხება ბოლო 5 წამში.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "ინტერაქციის სავარაუდო ხანგრძლივობა ბოლო 5 წამის განმავლობაში. ეს არის INP-ის მსგავსი და არა ოფიციალური Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "განლაგების კუმულატიური ცვლა აპლიკაციის მიმდინარე სიცოცხლის ხანგრძლივობისთვის.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "გამოყენებული JS heap vs heap limit. მხოლოდ Chromium.", + "debugBar.mem.tip": "გამოყენებული JS heap vs heap limit. {{used}} / {{limit}}-დან.", + "debugBar.focus.label": "ფოკუსი", + "debugBar.focus.tip": "აიძულებს ფოკუსირებას სტილებს ყველა ინტერაქტიულ ელემენტზე", + "debugBar.focus.on": "ჩართული", + "debugBar.focus.off": "გამორთული", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "აპის სრული განლაგების მიმართულების იძულება არჩეული ენის შეცვლის გარეშე", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode სამუშაო მაგიდა", + "settings.section.desktop": "დესკტოპი", + "settings.section.server": "სერვერი", + "settings.tab.general": "ზოგადი", + "settings.tab.shortcuts": "მალსახმობები", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL ინტეგრაცია", + "settings.desktop.wsl.description": "გაუშვით OpenCode სერვერი WSL-ში Windows-ზე.", + "settings.general.section.appearance": "გარეგნობა", + "settings.general.section.advanced": "დამატებითი", + "settings.general.section.notifications": "სისტემის შეტყობინებები", + "settings.general.section.updates": "განახლებები", + "settings.general.section.sounds": "ხმოვანი ეფექტები", + "settings.general.section.feed": "არხი", + "settings.general.section.display": "ჩვენება", + "settings.general.row.language.title": "ენა", + "settings.general.row.language.description": "შეცვალეთ ჩვენების ენა OpenCode", + "settings.general.row.shell.title": "ტერმინალის გარსი", + "settings.general.row.shell.description": "Shell გამოიყენება ტერმინალის და აგენტის ხელსაწყოების მიერ", + "settings.general.row.shell.autoDefault": "ავტომატური (ნაგულისხმევი)", + "settings.general.row.shell.terminalOnly": "მხოლოდ ტერმინალი", + "settings.general.row.appearance.title": "გარეგნობა", + "settings.general.row.appearance.description": "მორგეთ, როგორ გამოიყურება OpenCode თქვენს მოწყობილობაზე", + "settings.general.row.colorScheme.title": "ფერთა სქემა", + "settings.general.row.colorScheme.description": "აირჩიეთ, მიჰყვება თუ არა OpenCode სისტემას, ღია თუ ბნელ თემას", + "settings.general.row.theme.title": "თემა", + "settings.general.row.theme.description": "მორგება, თუ როგორ არის OpenCode თემატური.", + "settings.general.row.font.title": "კოდის შრიფტი", + "settings.general.row.font.description": "მორგეთ კოდის ბლოკებში გამოყენებული შრიფტი", + "settings.general.row.terminalFont.title": "ტერმინალის შრიფტი", + "settings.general.row.terminalFont.description": "მორგეთ ტერმინალში გამოყენებული შრიფტი", + "settings.general.row.uiFont.title": "UI ფონტი", + "settings.general.row.uiFont.description": "დააყენეთ ინტერფეისში გამოყენებული შრიფტის მორგება", + "settings.general.row.followup.title": "შემდეგი ქცევა", + "settings.general.row.followup.description": + "აირჩიეთ, შემდგომი დაკვირვების მოთხოვნა მყისიერად იხელმძღვანელეთ თუ დაელოდეთ რიგში", + "settings.general.row.followup.option.queue": "რიგი", + "settings.general.row.followup.option.steer": "მმართველი", + "settings.general.row.showFileTree.title": "ფაილის ხე", + "settings.general.row.showFileTree.description": "ფაილის ხის პანელის ჩვენება სესიებში", + "settings.general.row.showNavigation.title": "ნავიგაციის კონტროლი", + "settings.general.row.showNavigation.description": "უკანა და წინსვლის ღილაკების ჩვენება დესკტოპის სათაურის ზოლში", + "settings.general.row.showSearch.title": "ბრძანების პალიტრა", + "settings.general.row.showSearch.description": "ძებნისა და ბრძანების პალიტრის ღილაკის ჩვენება სათაურის ზოლში", + "settings.general.row.showTerminal.title": "ტერმინალი", + "settings.general.row.showTerminal.description": "ტერმინალის ღილაკის ჩვენება დესკტოპის სათაურის ზოლში", + "settings.general.row.showStatus.title": "სერვერის სტატუსი", + "settings.general.row.showStatus.description": "სერვერის სტატუსის ღილაკის ჩვენება სათაურის ზოლში", + "settings.general.row.mobileTitlebarBottom.title": "ქვედა ნავიგაცია", + "settings.general.row.mobileTitlebarBottom.description": + "მოათავსეთ სათაურის ზოლი და სესიის ჩანართები ეკრანის ბოლოში მობილურზე", + "settings.general.row.showCustomAgents.title": "აგენტის ჩვენება", + "settings.general.row.showCustomAgents.description": + "გადართვა კომპოზიტორში აგენტებს შორის. როდესაც დამალულია, ნაგულისხმევად არის Build აგენტი.", + "settings.general.row.reasoningSummaries.title": "მსჯელობის შეჯამების ჩვენება", + "settings.general.row.reasoningSummaries.description": "მოდელების მსჯელობის რეზიუმეების ჩვენება ვადებში", + "settings.general.row.shellToolPartsExpanded.title": "Shell ხელსაწყოს ნაწილების გაფართოება", + "settings.general.row.shellToolPartsExpanded.description": + "Shell ხელსაწყოს ნაწილების ნაგულისხმევად გაფართოებული ჩვენება ქრონოლოგიაში", + "settings.general.row.editToolPartsExpanded.title": "გაფართოვდეს ხელსაწყოს ნაწილები", + "settings.general.row.editToolPartsExpanded.description": + "აჩვენეთ რედაქტირების, ჩაწერის და პაჩის ხელსაწყოს ნაწილები, რომლებიც ნაგულისხმევად გაფართოვდა ვადებში", + "settings.general.row.newInterface.title": "ახალი განლაგება", + "settings.general.row.newInterface.badge": "ახალი", + "settings.general.row.newInterface.description": + "გამოიყენეთ ახალი ჩანართები და სახლის განლაგება. განლაგებებს შორის გადართვა შეზღუდული დროით.", + "settings.general.row.newInterfaceNotice.title": "თქვენ ახლა იყენებთ ახალ განლაგებას", + "settings.general.row.newInterfaceNotice.description": "წინა განლაგება აღარ არის ხელმისაწვდომი", + "settings.general.row.newInterfaceNotice.dismiss": "დატოვება", + "settings.general.row.pinchZoom.title": "დააჭირე გასადიდებლად", + "settings.general.row.pinchZoom.description": "Trackpad pinch და Ctrl-scroll ჟესტებით მასშტაბირება", + "settings.general.row.wayland.title": "გამოიყენე მშობლიური Wayland", + "settings.general.row.wayland.description": "X11-ის გამორთვა Wayland-ზე. საჭიროებს გადატვირთვას.", + "settings.general.row.wayland.tooltip": + "Linux-ზე განახლების სიჩქარის შერეული მონიტორებით, მშობლიური Wayland შეიძლება იყოს უფრო სტაბილური.", + "settings.general.row.releaseNotes.title": "გამოშვების შენიშვნები", + "settings.general.row.releaseNotes.description": "აჩვენე რა არის ახალი ამომხტარი ფანჯარა განახლებების შემდეგ", + "settings.updates.row.startup.title": "შეამოწმეთ განახლებები გაშვებისას", + "settings.updates.row.startup.description": "ავტომატურად შეამოწმეთ განახლებები, როდესაც OpenCode გაშვება", + "settings.updates.row.check.title": "შეამოწმეთ განახლებები", + "settings.updates.row.check.description": "ხელით შეამოწმეთ განახლებები და დააინსტალირეთ, თუ შესაძლებელია", + "settings.updates.action.checkNow": "შეამოწმეთ ახლა", + "settings.updates.action.checking": "შემოწმება...", + "settings.updates.action.downloading": "ჩამოტვირთვა...", + "settings.updates.action.installing": "ინსტალაცია...", + "settings.updates.toast.latest.title": "თქვენ განახლებული ხართ", + "settings.updates.toast.latest.description": "თქვენ იყენებთ OpenCode-ის უახლეს ვერსიას.", + "sound.option.none": "არცერთი", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "აგენტი", + "settings.general.notifications.agent.description": + "სისტემის შეტყობინების ჩვენება, როდესაც აგენტი დასრულებულია ან საჭიროებს ყურადღებას", + "settings.general.notifications.permissions.title": "ნებართვები", + "settings.general.notifications.permissions.description": "სისტემის შეტყობინების ჩვენება, როცა საჭიროა ნებართვა", + "settings.general.notifications.errors.title": "შეცდომები", + "settings.general.notifications.errors.description": "სისტემის შეტყობინების ჩვენება შეცდომის დადგომისას", + "settings.general.sounds.agent.title": "აგენტი", + "settings.general.sounds.agent.description": "ხმის დაკვრა, როცა აგენტი დასრულებულია ან საჭიროებს ყურადღებას", + "settings.general.sounds.permissions.title": "ნებართვები", + "settings.general.sounds.permissions.description": "ხმის დაკვრა როცა საჭიროა ნებართვა", + "settings.general.sounds.errors.title": "შეცდომები", + "settings.general.sounds.errors.description": "ხმის დაკვრა შეცდომის დროს", + "settings.shortcuts.title": "კლავიატურის მალსახმობები", + "settings.shortcuts.reset.button": "დაყენება ნაგულისხმევზე", + "settings.shortcuts.reset.toast.title": "მალსახმობები აღდგენილია", + "settings.shortcuts.reset.toast.description": "კლავიატურის მალსახმობები აღდგენილია ნაგულისხმევად.", + "settings.shortcuts.conflict.title": "მალსახი უკვე გამოიყენება", + "settings.shortcuts.conflict.description": "{{keybind}} უკვე მინიჭებულია {{titles}}-ს.", + "settings.shortcuts.unassigned": "მიუყენებელი", + "settings.shortcuts.pressKeys": "დააჭირე კლავიშებს", + "settings.shortcuts.search.placeholder": "ძებნის მალსახმობები", + "settings.shortcuts.search.empty": "მალსახები არ მოიძებნა", + "settings.shortcuts.group.general": "ზოგადი", + "settings.shortcuts.group.session": "სესია", + "settings.shortcuts.group.navigation": "ნავიგაცია", + "settings.shortcuts.group.modelAndAgent": "მოდელი და აგენტი", + "settings.shortcuts.group.terminal": "ტერმინალი", + "settings.shortcuts.group.prompt": "მოთხოვნა", + "settings.providers.title": "პროვაიდერები", + "settings.providers.description": "პროვაიდერის პარამეტრების კონფიგურაცია იქნება აქ.", + "settings.providers.section.connected": "დაკავშირებული პროვაიდერები", + "settings.providers.connected.empty": "დაკავშირებული პროვაიდერები არ არის", + "settings.providers.connected.environmentDescription": "დაკავშირებულია თქვენი გარემოს ცვლადებიდან", + "settings.providers.section.popular": "პოპულარული პროვაიდერები", + "settings.providers.custom.description": "დაამატე OpenAI-თავსებადი პროვაიდერი ბაზის მიხედვით: URL.", + "settings.providers.tag.environment": "გარემო", + "settings.providers.tag.config": "კონფიგურაცია", + "settings.providers.tag.custom": "მორგებული", + "settings.providers.tag.other": "სხვა", + "settings.models.title": "მოდელები", + "settings.models.description": "მოდელის პარამეტრების კონფიგურაცია იქნება აქ.", + "settings.agents.title": "აგენტები", + "settings.agents.description": "აგენტის პარამეტრების კონფიგურაცია იქნება აქ.", + "settings.commands.title": "ბრძანებები", + "settings.commands.description": "ბრძანების პარამეტრების კონფიგურაცია იქნება აქ.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP პარამეტრების კონფიგურაცია აქ იქნება შესაძლებელი.", + "settings.permissions.title": "ნებართვები", + "settings.permissions.description": "აკონტროლეთ, რომელი ხელსაწყოების გამოყენება შეუძლია სერვერს ნაგულისხმევად.", + "settings.permissions.section.tools": "ხელსაწყოები", + "settings.permissions.toast.updateFailed.title": "ნებართვების განახლება ვერ მოხერხდა", + "settings.permissions.action.allow": "დაშვება", + "settings.permissions.action.ask": "კითხვა", + "settings.permissions.action.deny": "უარი", + "settings.permissions.tool.read.title": "წაიკითხე", + "settings.permissions.tool.read.description": "ფაილის კითხვა (შეესაბამება ფაილის გზას)", + "settings.permissions.tool.edit.title": "რედაქტირება", + "settings.permissions.tool.edit.description": "ფაილების შეცვლა, რედაქტირების, ჩაწერებისა და პაჩების ჩათვლით", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "ფაილების შესატყვისი გლობალური შაბლონების გამოყენებით", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "ფაილის შიგთავსის ძიება რეგულარული გამონათქვამების გამოყენებით", + "settings.permissions.tool.list.title": "სია", + "settings.permissions.tool.list.description": "ფაილების სია დირექტორიაში", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "გარსის ბრძანებების გაშვება", + "settings.permissions.tool.task.title": "ამოცანა", + "settings.permissions.tool.task.description": "ქვე-აგენტების გაშვება", + "settings.permissions.tool.skill.title": "უნარი", + "settings.permissions.tool.skill.description": "უნარის ჩატვირთვა სახელით", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "ენის სერვერის მოთხოვნების გაშვება", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "განაახლე სამუშაოების სია", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "კონტენტის მიღება URL", + "settings.permissions.tool.websearch.title": "ვებ ძიება", + "settings.permissions.tool.websearch.description": "მოძებნე ვებში", + "settings.permissions.tool.external_directory.title": "გარე დირექტორია", + "settings.permissions.tool.external_directory.description": "წვდომა ფაილებზე პროექტის დირექტორიას გარეთ", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "იდენტური შეყვანით ხელსაწყოს განმეორებითი გამოძახებების აღმოჩენა", + "session.delete.failed.title": "სესიის წაშლა ვერ მოხერხდა", + "session.delete.title": "სესიის წაშლა", + "session.delete.confirm": "წაშალე სესია „{{name}}“?", + "session.delete.button": "სესიის წაშლა", + "workspace.new": "ახალი სამუშაო სივრცე", + "workspace.type.local": "ადგილობრივი", + "workspace.type.sandbox": "Sandbox", + "workspace.create.failed.title": "სამუშაო სივრცის შექმნა ვერ მოხერხდა", + "workspace.delete.failed.title": "სამუშაო სივრცის წაშლა ვერ მოხერხდა", + "workspace.resetting.title": "სამუშაო სივრცის გადაყენება", + "workspace.resetting.description": "ამას შეიძლება ერთი წუთი დასჭირდეს.", + "workspace.reset.failed.title": "სამუშაო სივრცის გადაყენება ვერ მოხერხდა", + "workspace.reset.success.title": "სამუშაო სივრცის გადატვირთვა", + "workspace.reset.success.description": "სამუშაო სივრცე ახლა ემთხვევა ნაგულისხმევ ფილიალს.", + "workspace.error.stillPreparing": "სამუშაო სივრცე ჯერ კიდევ მზადდება", + "workspace.status.checking": "შეუერთებელი ცვლილებების შემოწმება...", + "workspace.status.error": "git სტატუსის გადამოწმება შეუძლებელია.", + "workspace.status.clean": "გაუერთებელი ცვლილებები არ არის აღმოჩენილი.", + "workspace.status.dirty": "აღმოჩენილი ცვლილებები აღმოჩენილია ამ სამუშაო სივრცეში.", + "workspace.delete.title": "სამუშაო სივრცის წაშლა", + "workspace.delete.confirm": "სამუშაო სივრცის „{{name}}“ წაშლა?", + "workspace.delete.button": "სამუშაო სივრცის წაშლა", + "workspace.reset.title": "სამუშაო სივრცის გადატვირთვა", + "workspace.reset.confirm": 'სამუშაო სივრცის "{{name}}" გადატვირთვა?', + "workspace.reset.button": "სამუშაო სივრცის გადატვირთვა", + "workspace.reset.archived.none": "აქტიური სესიები არ იქნება დაარქივებული.", + "workspace.reset.archived.one": "1 სესია დაარქივდება.", + "workspace.reset.archived.many": "{{count}} სესია დაარქივდება.", + "workspace.reset.note": "ეს აღადგენს სამუშაო სივრცეს ნაგულისხმევი ფილიალის შესატყვისად.", + "dialog.usageExceeded.dontShowAgain": "აღარ მაჩვენო", +} diff --git a/packages/app/src/i18n/km.ts b/packages/app/src/i18n/km.ts new file mode 100644 index 0000000000000000000000000000000000000000..7f1b61fe3667cd533087971cb7edfb8524c82e57 --- /dev/null +++ b/packages/app/src/i18n/km.ts @@ -0,0 +1,1151 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ឯកសារ", + "desktop.menu.edit": "កែសម្រួល", + "desktop.menu.view": "មើល", + "desktop.menu.go": "ទៅ", + "desktop.menu.window": "បង្អួច", + "desktop.menu.help": "ជំនួយ", + "desktop.menu.checkForUpdates": "ពិនិត្យមើលបច្ចុប្បន្នភាព...", + "desktop.menu.settings": "ការកំណត់", + "desktop.menu.reloadWebview": "ផ្ទុក Webview ឡើងវិញ", + "desktop.menu.restart": "ចាប់ផ្ដើមឡើងវិញ", + "desktop.menu.exportLogs": "នាំចេញកំណត់ហេតុ...", + "desktop.menu.newSession": "សម័យថ្មី។", + "desktop.menu.openProject": "បើកគម្រោង...", + "desktop.menu.newWindow": "បង្អួចថ្មី។", + "desktop.menu.closeWindow": "បិទបង្អួច", + "desktop.menu.undo": "មិនធ្វើវិញ", + "desktop.menu.redo": "ធ្វើឡើងវិញ", + "desktop.menu.cut": "កាត់", + "desktop.menu.copy": "ចម្លង", + "desktop.menu.paste": "បិទភ្ជាប់", + "desktop.menu.delete": "លុប", + "desktop.menu.selectAll": "ជ្រើសរើសទាំងអស់។", + "desktop.menu.toggleSidebar": "បិទ/បើករបារចំហៀង", + "desktop.menu.toggleTerminal": "បិទ/បើកស្ថានីយ", + "desktop.menu.toggleFileTree": "បិទបើកឯកសារមែកធាង", + "desktop.menu.reload": "ផ្ទុកឡើងវិញ", + "desktop.menu.toggleDeveloperTools": "បិទបើកឧបករណ៍អ្នកអភិវឌ្ឍន៍", + "desktop.menu.actualSize": "ទំហំពិត", + "desktop.menu.zoomIn": "ពង្រីក", + "desktop.menu.zoomOut": "ពង្រីក", + "desktop.menu.toggleFullScreen": "បិទ/បើកពេញអេក្រង់", + "desktop.menu.back": "ត្រឡប់មកវិញ", + "desktop.menu.forward": "ទៅមុខ", + "desktop.menu.previousSession": "សម័យមុន", + "desktop.menu.nextSession": "សម័យបន្ទាប់", + "desktop.menu.previousProject": "គម្រោងពីមុន", + "desktop.menu.nextProject": "គម្រោងបន្ទាប់", + "desktop.menu.minimize": "បង្រួមអប្បបរមា", + "desktop.menu.maximize": "អតិបរមា", + "desktop.menu.documentation": "ឯកសារ OpenCode", + "desktop.menu.supportForum": "វេទិកាគាំទ្រ", + "desktop.menu.shareFeedback": "ចែករំលែកមតិកែលម្អ", + "desktop.menu.reportBug": "រាយការណ៍អំពីបញ្ហា", + "desktop.menu.ariaLabel": "ម៉ឺនុយ OpenCode", + "desktop.updater.dialog.checkFailed.message": "ការត្រួតពិនិត្យការអាប់ដេតបានបរាជ័យ។", + "desktop.updater.dialog.checkFailed.title": "កំហុសក្នុងការអាប់ដេត", + "desktop.updater.dialog.upToDate.message": "អ្នកទាន់សម័យហើយ។", + "desktop.updater.dialog.upToDate.title": "គ្មានការអាប់ដេតទេ។", + "desktop.updater.dialog.ready.message": "អាប់ដេត {{version}} ត្រូវបានទាញយក។ ចាប់ផ្តើមឡើងវិញឥឡូវនេះ?", + "desktop.updater.dialog.ready.title": "ធ្វើបច្ចុប្បន្នភាពរួចរាល់", + "desktop.updater.dialog.restart": "ចាប់ផ្ដើមឡើងវិញ", + "desktop.updater.dialog.later": "ពេលក្រោយ", + "desktop.recovery.action.relaunch": "ចាប់ផ្តើមឡើងវិញ", + "desktop.recovery.action.exportLogs": "នាំចេញកំណត់ហេតុ", + "desktop.recovery.action.keepWaiting": "បន្តរង់ចាំ", + "desktop.recovery.action.quit": "ឈប់", + "desktop.recovery.loadFailed": "OpenCode បានបរាជ័យក្នុងការផ្ទុក", + "desktop.recovery.terminated": "បង្អួច OpenCode ត្រូវបានបញ្ចប់ដោយមិននឹកស្មានដល់", + "desktop.recovery.unresponsive": "OpenCode មិនឆ្លើយតបទេ។", + "desktop.recovery.unresponsive.detail": "អ្នកអាចបើកកម្មវិធីឡើងវិញ បើកកំណត់ហេតុ ឬបន្តរង់ចាំ។", + "desktop.recovery.loadFailed.detail": "បង្អួច៖ {{window}}\nURL៖ {{url}}\nកំហុស៖ {{code}} {{description}}", + "desktop.recovery.terminated.detail": "បង្អួច៖ {{window}}\nហេតុផល៖ {{reason}}\nលេខកូដ៖ {{code}}", + "desktop.recovery.unknown": "<មិនស្គាល់>", + "desktop.dialog.chooseFolder": "ជ្រើសរើសថតឯកសារ", + "desktop.dialog.chooseFile": "ជ្រើសរើសឯកសារ", + "desktop.dialog.saveFile": "រក្សាទុកឯកសារ", + "desktop.dialog.files": "ឯកសារ", + "desktop.server.local": "ម៉ាស៊ីនមេក្នុងស្រុក", + "desktop.wsl.error.windowsOnly": "WSL មាននៅលើ Windows ប៉ុណ្ណោះ។", + "desktop.wsl.error.unavailable": "WSL មិនអាចប្រើបានទេ។", + "desktop.wsl.error.listInstalled": "បានបរាជ័យក្នុងការរាយបញ្ជីការចែកចាយ WSL ដែលបានដំឡើង", + "desktop.wsl.error.listOnline": "បានបរាជ័យក្នុងការរាយបញ្ជីការចែកចាយ WSL តាមអ៊ីនធឺណិត", + "desktop.wsl.error.executeDistro": "មិនអាចប្រតិបត្តិពាក្យបញ្ជានៅក្នុង distro បានទេ។", + "desktop.wsl.error.installWsl": "ការដំឡើង WSL បានបរាជ័យ", + "desktop.wsl.error.installDistro": "បរាជ័យក្នុងការដំឡើង distro៖ {{distro}}", + "desktop.wsl.error.installOpencode": "ការដំឡើង OpenCode បានបរាជ័យ", + "desktop.wsl.error.alreadyAdded": "{{distro}} ត្រូវបានបន្ថែមរួចហើយ", + "desktop.wsl.error.opencodeMissing": "opencode មិនត្រូវបានដំឡើងនៅក្នុង distro នេះទេ។", + "desktop.wsl.error.opencodeCannotRun": "opencode ត្រូវបានដំឡើង ប៉ុន្តែមិនអាចដំណើរការបានទេ។", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode មិនត្រូវបានដំឡើងនៅក្នុង {{distro}} ទេ។", + "desktop.wsl.error.updateVersion": + "ការធ្វើបច្ចុប្បន្នភាព OpenCode បានបញ្ចប់ ប៉ុន្តែ {{distro}} នៅតែរាយការណ៍ {{installed}}; {{expected}} រំពឹងទុក", + "desktop.wsl.error.noVersion": "គ្មានកំណែ", + "desktop.wsl.error.serverExited": "ម៉ាស៊ីនមេ WSL បានចាកចេញបន្ទាប់ពីការចាប់ផ្ដើម (កូដ={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "ម៉ាស៊ីនមេ WSL បានចេញមុនពេលមានសុខភាពល្អ (កូដ={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Sidecar សម្រាប់ការពិនិត្យសុខភាព {{distro}} អស់ពេលបន្ទាប់ពី {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} អស់ពេលបន្ទាប់ពី {{timeout}}ms", + "desktop.wsl.error.failedPort": "បរាជ័យក្នុងការទទួលបានច្រក", + "desktop.picker.error.notSelected": "ឯកសារមិនត្រូវបានជ្រើសរើសដោយអ្នកជ្រើសរើសទេ។", + "desktop.picker.error.sizeLimit": "ឯកសារភ្ជាប់ដែលបានជ្រើសរើសលើសពីដែនកំណត់ {{limit}} MB", + "command.category.suggested": "បានស្នើ", + "command.category.view": "មើល", + "command.category.project": "គម្រោង", + "command.category.provider": "អ្នកផ្តល់សេវា", + "command.category.server": "ម៉ាស៊ីនមេ", + "command.category.session": "សម័យ", + "command.category.theme": "ប្រធានបទ", + "command.category.language": "ភាសា", + "command.category.file": "ឯកសារ", + "command.category.context": "បរិបទ", + "command.category.terminal": "ស្ថានីយ", + "command.category.model": "ម៉ូដែល", + "command.category.mcp": "MCP", + "command.category.agent": "ភ្នាក់ងារ", + "command.category.permissions": "ការអនុញ្ញាត", + "command.category.workspace": "កន្លែងធ្វើការ", + "command.category.settings": "ការកំណត់", + "command.logs.export": "នាំចេញកំណត់ហេតុ", + "theme.scheme.system": "ប្រព័ន្ធ", + "theme.scheme.light": "ពន្លឺ", + "theme.scheme.dark": "ងងឹត", + "command.sidebar.toggle": "បិទ/បើករបារចំហៀង", + "command.project.open": "គម្រោងបើក", + "command.project.previous": "គម្រោងមុន។", + "command.project.next": "គម្រោងបន្ទាប់", + "command.project.index": "ប្តូរទៅគម្រោង {{index}}", + "command.provider.connect": "ភ្ជាប់អ្នកផ្តល់សេវា", + "command.server.switch": "ប្តូរម៉ាស៊ីនមេ", + "command.settings.open": "បើកការកំណត់", + "command.session.previous": "សម័យមុន។", + "command.session.next": "សម័យបន្ទាប់", + "command.session.previous.unseen": "សម័យមិនទាន់អានពីមុន", + "command.session.next.unseen": "សម័យមិនទាន់អានបន្ទាប់", + "command.session.archive": "សម័យបណ្ណសារ", + "command.palette": "ក្ដារលាយពាក្យបញ្ជា", + "command.theme.cycle": "ប្រធានបទវដ្ត", + "command.theme.set": "ប្រើប្រធានបទ៖ {{theme}}", + "command.theme.scheme.cycle": "ពណ៌ចម្រុះ", + "command.theme.scheme.set": "ប្រើពណ៌ចម្រុះ៖ {{scheme}}", + "command.language.cycle": "ភាសាវដ្ត", + "command.language.set": "ប្រើភាសា៖ {{language}}", + "command.session.new": "សម័យថ្មី។", + "command.file.open": "បើកឯកសារ", + "command.tab.close": "បិទផ្ទាំង", + "command.tab.reopenClosed": "បើកផ្ទាំងបិទឡើងវិញ", + "command.context.addSelection": "បន្ថែមការជ្រើសរើសទៅបរិបទ", + "command.context.addSelection.description": "បន្ថែមបន្ទាត់ដែលបានជ្រើសរើសពីឯកសារបច្ចុប្បន្ន", + "command.input.focus": "ការបញ្ចូលការផ្តោតអារម្មណ៍", + "command.terminal.toggle": "បិទ/បើកស្ថានីយ", + "command.fileTree.toggle": "បិទបើកមែកធាងឯកសារ", + "command.review.toggle": "បិទ/បើកការពិនិត្យឡើងវិញ", + "command.terminal.new": "ស្ថានីយថ្មី។", + "command.terminal.new.description": "បង្កើតផ្ទាំងស្ថានីយថ្មី។", + "command.steps.toggle": "បិទ/បើកជំហាន", + "command.steps.toggle.description": "បង្ហាញ ឬលាក់ជំហានសម្រាប់សារបច្ចុប្បន្ន", + "command.message.previous": "សារពីមុន", + "command.message.previous.description": "ទៅកាន់សារអ្នកប្រើប្រាស់មុន។", + "command.message.next": "សារបន្ទាប់", + "command.message.next.description": "ទៅកាន់សារអ្នកប្រើប្រាស់បន្ទាប់", + "command.model.choose": "ជ្រើសរើសម៉ូដែល", + "command.model.choose.description": "ជ្រើសរើសម៉ូដែលផ្សេង", + "command.mcp.toggle": "បិទ/បើក MCPs", + "command.mcp.toggle.description": "បិទ/បើក MCPs", + "command.agent.cycle": "ភ្នាក់ងារវដ្ត", + "command.agent.cycle.description": "ប្តូរទៅភ្នាក់ងារបន្ទាប់", + "command.agent.cycle.reverse": "ភ្នាក់ងាររង្វង់ថយក្រោយ", + "command.agent.cycle.reverse.description": "ប្តូរទៅភ្នាក់ងារមុន។", + "command.model.variant.cycle": "ការខិតខំប្រឹងប្រែងគិតជារង្វង់", + "command.model.variant.cycle.description": "ប្តូរទៅកម្រិតនៃកិច្ចខិតខំប្រឹងប្រែងបន្ទាប់", + "command.prompt.mode.shell": "សែល", + "command.prompt.mode.normal": "ប្រអប់បញ្ចូល", + "command.permissions.autoaccept.enable": "ការអនុញ្ញាតដោយស្វ័យប្រវត្តិ", + "command.permissions.autoaccept.disable": "បញ្ឈប់ការអនុញ្ញាតដោយស្វ័យប្រវត្តិ", + "command.workspace.toggle": "បិទ/បើកកន្លែងធ្វើការ", + "command.workspace.toggle.description": "បើក ឬបិទកន្លែងធ្វើការច្រើននៅក្នុងរបារចំហៀង", + "command.session.undo": "មិនធ្វើវិញ", + "command.session.undo.description": "មិនធ្វើសារចុងក្រោយ", + "command.session.redo": "ធ្វើឡើងវិញ", + "command.session.redo.description": "ធ្វើសារឡើងវិញនូវសារដែលមិនបានធ្វើចុងក្រោយ", + "command.session.compact": "សម័យបង្រួម", + "command.session.compact.description": "សង្ខេបសម័យដើម្បីកាត់បន្ថយទំហំបរិបទ", + "command.session.fork": "បំបែកចេញពីសារ", + "command.session.fork.description": "បង្កើតសម័យថ្មីពីសារមុន។", + "command.session.share": "សម័យចែករំលែក", + "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": "រកមិនឃើញលទ្ធផលទេ។", + "palette.group.commands": "ពាក្យបញ្ជា", + "palette.group.files": "ឯកសារ", + "dialog.provider.search.placeholder": "អ្នកផ្តល់សេវាស្វែងរក", + "dialog.provider.empty": "រកមិនឃើញអ្នកផ្តល់សេវាទេ។", + "dialog.provider.group.popular": "ពេញនិយម", + "dialog.provider.group.other": "ផ្សេងទៀត។", + "dialog.provider.custom.label": "អ្នកផ្តល់សេវាដែលត្រូវគ្នានឹង OpenAI ផ្ទាល់ខ្លួន", + "dialog.provider.tag.recommended": "បានណែនាំ", + "dialog.provider.opencode.note": "ម៉ូដែលដែលបានជ្រើសរើសរួមមាន Claude, GPT, Gemini និងច្រើនទៀត", + "dialog.provider.opencode.tagline": "ម៉ូដែលដែលបានកែលម្អដែលអាចទុកចិត្តបាន។", + "dialog.provider.opencodeGo.tagline": "ការជាវតម្លៃទាបសម្រាប់អ្នករាល់គ្នា", + "dialog.provider.anthropic.note": "ការចូលប្រើដោយផ្ទាល់ទៅកាន់ម៉ូដែល Claude រួមទាំង Pro និង Max", + "dialog.provider.copilot.note": "ម៉ូដែល AI សម្រាប់ជំនួយការសរសេរកូដតាមរយៈ GitHub Copilot", + "dialog.provider.openai.note": "ម៉ូដែល GPT សម្រាប់កិច្ចការ AI ទូទៅដែលមានល្បឿនលឿន និងមានសមត្ថភាព", + "dialog.provider.google.note": "ម៉ូដែល Gemini សម្រាប់ការឆ្លើយតបរហ័ស និងមានរចនាសម្ព័ន្ធ", + "dialog.provider.openrouter.note": "ចូលប្រើម៉ូដែលដែលបានគាំទ្រទាំងអស់ពីអ្នកផ្តល់សេវាមួយ។", + "dialog.provider.vercel.note": "ការចូលប្រើប្រាស់រួមទៅកាន់ម៉ូដែល AI ជាមួយនឹងការកំណត់ផ្លូវឆ្លាតវៃ", + "dialog.model.select.title": "ជ្រើសរើសម៉ូដែល", + "dialog.model.search.placeholder": "ស្វែងរកម៉ូដែល", + "dialog.model.empty": "រកមិនឃើញម៉ូដែល", + "dialog.model.manage": "គ្រប់គ្រងម៉ូដែល", + "dialog.model.manage.description": "ប្ដូរម៉ូដែលដែលបង្ហាញក្នុងឧបករណ៍ជ្រើសរើសម៉ូដែលតាមបំណង។", + "dialog.model.manage.provider.toggle": "បិទ/បើកម៉ូដែល {{provider}} ទាំងអស់។", + "dialog.model.unpaid.freeModels.title": "ម៉ូដែលឥតគិតថ្លៃដែលផ្តល់ដោយ OpenCode", + "dialog.model.unpaid.addMore.title": "បន្ថែមម៉ូដែលជាច្រើនទៀតពីអ្នកផ្តល់សេវាពេញនិយម", + "dialog.model.unpaid.viewMoreProviders": "មើលអ្នកផ្តល់សេវា 70+ ទៀត។", + "dialog.provider.viewAll": "បង្ហាញអ្នកផ្តល់សេវាបន្ថែម", + "provider.connect.title": "ភ្ជាប់ {{provider}}", + "provider.connect.title.anthropicProMax": "ចូលដោយប្រើ Claude Pro/Max", + "provider.connect.selectMethod": "ជ្រើសរើសវិធីសាស្ត្រចូលសម្រាប់ {{provider}} ។", + "provider.connect.method.apiKey": "សោ API", + "provider.connect.method.browser": "កម្មវិធីរុករក", + "provider.connect.method.headless": "គ្មានក្បាល", + "provider.connect.status.inProgress": "ការអនុញ្ញាតកំពុងដំណើរការ...", + "provider.connect.status.waiting": "រង់ចាំការអនុញ្ញាត...", + "provider.connect.status.failed": "ការអនុញ្ញាតបានបរាជ័យ៖ {{error}}", + "provider.connect.apiKey.description": + "បញ្ចូល {{provider}} API key របស់អ្នក ដើម្បីភ្ជាប់គណនីរបស់អ្នក ហើយប្រើម៉ូដែល {{provider}} នៅក្នុង OpenCode។", + "provider.connect.apiKey.label": "សោ API {{provider}}", + "provider.connect.apiKey.placeholder": "សោ API", + "provider.connect.apiKey.required": "ត្រូវការសោ API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ផ្តល់ឱ្យអ្នកនូវម៉ូដែលដែលបានជ្រើសរើស និងកែលម្អសម្រាប់ភ្នាក់ងារសរសេរកូដ។", + "provider.connect.opencodeZen.line2": + "ជាមួយនឹងសោ API តែមួយ អ្នកនឹងទទួលបានការចូលប្រើម៉ូដែលដូចជា Claude, GPT, Gemini, GLM និងច្រើនទៀត។", + "provider.connect.opencodeZen.visit.prefix": "ទស្សនា", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "ដើម្បីប្រមូលសោ API របស់អ្នក។", + "provider.connect.oauth.code.visit.prefix": "ទស្សនា", + "provider.connect.oauth.code.visit.link": "តំណនេះ។", + "provider.connect.oauth.code.visit.suffix": + "ដើម្បីប្រមូលលេខកូដអនុញ្ញាតរបស់អ្នកដើម្បីភ្ជាប់គណនីរបស់អ្នក និងប្រើម៉ូដែល {{provider}} នៅក្នុង OpenCode ។", + "provider.connect.oauth.code.label": "លេខកូដអនុញ្ញាត {{method}}", + "provider.connect.oauth.code.placeholder": "លេខកូដអនុញ្ញាត", + "provider.connect.oauth.code.required": "ទាមទារលេខកូដអនុញ្ញាត", + "provider.connect.oauth.code.invalid": "លេខកូដអនុញ្ញាតមិនត្រឹមត្រូវ", + "provider.connect.oauth.auto.visit.prefix": "ទស្សនា", + "provider.connect.oauth.auto.visit.link": "តំណនេះ។", + "provider.connect.oauth.auto.visit.suffix": + "ហើយបញ្ចូលលេខកូដខាងក្រោមដើម្បីភ្ជាប់គណនីរបស់អ្នក ហើយប្រើម៉ូដែល {{provider}} នៅក្នុង OpenCode។", + "provider.connect.oauth.auto.confirmationCode": "លេខកូដបញ្ជាក់", + "provider.connect.toast.connected.title": "{{provider}} បានភ្ជាប់", + "provider.connect.toast.connected.description": "ម៉ូដែល {{provider}} ឥឡូវអាចប្រើបាន។", + "provider.custom.title": "អ្នកផ្តល់សេវាផ្ទាល់ខ្លួន", + "provider.custom.unavailable": "អ្នកផ្តល់សេវាផ្ទាល់ខ្លួនមិនមាននៅលើម៉ាស៊ីនមេនេះទេ។", + "provider.custom.description.prefix": "កំណត់រចនាសម្ព័ន្ធអ្នកផ្តល់សេវាដែលត្រូវគ្នាជាមួយ OpenAI ។ សូមមើល", + "provider.custom.description.link": "ឯកសារកំណត់រចនាសម្ព័ន្ធអ្នកផ្តល់សេវា", + "provider.custom.description.suffix": "។", + "provider.custom.field.providerID.label": "លេខសម្គាល់អ្នកផ្តល់សេវា", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "អក្សរតូច លេខ សហសញ្ញា ឬសញ្ញាគូសក្រោម", + "provider.custom.field.name.label": "ឈ្មោះបង្ហាញ", + "provider.custom.field.name.placeholder": "អ្នកផ្តល់សេវា AI របស់ខ្ញុំ", + "provider.custom.field.baseURL.label": "URL មូលដ្ឋាន", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "សោ API", + "provider.custom.field.apiKey.placeholder": "សោ API", + "provider.custom.field.apiKey.description": "ស្រេចចិត្ត។ ទុកឱ្យទទេប្រសិនបើអ្នកគ្រប់គ្រងការផ្ទៀងផ្ទាត់តាមរយៈបឋមកថា។", + "provider.custom.models.label": "ម៉ូដែល", + "provider.custom.models.id.label": "លេខសម្គាល់", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "ឈ្មោះ", + "provider.custom.models.name.placeholder": "ឈ្មោះបង្ហាញ", + "provider.custom.models.remove": "យកម៉ូដែលចេញ", + "provider.custom.models.add": "បន្ថែមម៉ូដែល", + "provider.custom.headers.label": "បឋមកថា (ជាជម្រើស)", + "provider.custom.headers.key.label": "បឋមកថា", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "តម្លៃ", + "provider.custom.headers.value.placeholder": "តម្លៃ", + "provider.custom.headers.remove": "លុបបឋមកថា", + "provider.custom.headers.add": "បន្ថែមបឋមកថា", + "provider.custom.error.providerID.required": "ទាមទារលេខសម្គាល់អ្នកផ្តល់សេវា", + "provider.custom.error.providerID.format": "ប្រើអក្សរតូច លេខ សហសញ្ញា ឬសញ្ញាគូសក្រោម", + "provider.custom.error.providerID.exists": "លេខសម្គាល់អ្នកផ្តល់សេវានោះមានរួចហើយ", + "provider.custom.error.name.required": "ទាមទារឈ្មោះបង្ហាញ", + "provider.custom.error.baseURL.required": "ទាមទារ URL មូលដ្ឋាន", + "provider.custom.error.baseURL.format": "ត្រូវតែចាប់ផ្តើមជាមួយ http:// ឬ https://", + "provider.custom.error.required": "ទាមទារ", + "provider.custom.error.duplicate": "ស្ទួន", + "provider.disconnect.toast.disconnected.title": "{{provider}} ត្រូវបានផ្តាច់", + "provider.disconnect.toast.disconnected.description": "ម៉ូដែល {{provider}} លែងមានទៀតហើយ។", + "model.tag.free": "ឥតគិតថ្លៃ", + "model.tag.latest": "ចុងក្រោយ", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "អត្ថបទ\nរូបភាព", + "model.input.image": "រូបភាព", + "model.input.audio": "សំឡេង", + "model.input.video": "វីដេអូ", + "model.input.pdf": "pdf", + "model.tooltip.allows": "អនុញ្ញាត៖ {{inputs}}", + "model.tooltip.context.label": "បរិបទ", + "model.tooltip.inputs": "ធាតុបញ្ចូល", + "model.tooltip.model": "ម៉ូដែល", + "model.tooltip.provider": "អ្នកផ្តល់សេវា", + "model.tooltip.reasoning": "ហេតុផល", + "model.tooltip.reasoning.allowed": "អនុញ្ញាតឱ្យមានហេតុផល", + "model.tooltip.reasoning.none": "គ្មានហេតុផល", + "model.tooltip.context": "ដែនកំណត់បរិបទ {{limit}}", + "common.search.placeholder": "ស្វែងរក", + "common.goBack": "រុករកត្រឡប់មកវិញ", + "common.goForward": "រុករកទៅមុខ", + "common.loading": "កំពុងផ្ទុក", + "common.loading.ellipsis": "...", + "common.cancel": "បោះបង់", + "common.clear": "ច្បាស់", + "common.open": "បើក", + "common.connect": "ភ្ជាប់", + "common.disconnect": "ផ្តាច់", + "common.continue": "បន្ត", + "common.submit": "ដាក់ស្នើ", + "common.save": "រក្សាទុក", + "common.saving": "កំពុងរក្សាទុក...", + "common.default": "លំនាំដើម\nឯកសារភ្ជាប់", + "common.attachment": "ឯកសារភ្ជាប់", + "prompt.placeholder.shell": "បញ្ចូលពាក្យបញ្ជាសែល... {{example}}", + "prompt.placeholder.normal": 'សួរអ្វីក៏បាន... "{{example}}"', + "prompt.placeholder.simple": "សួរអ្វី...", + "prompt.placeholder.summarizeComments": "សង្ខេបមតិ...", + "prompt.placeholder.summarizeComment": "សង្ខេបមតិ...", + "prompt.mode.shell": "សែល", + "prompt.mode.normal": "ប្រអប់បញ្ចូល", + "prompt.mode.shell.exit": "esc ដើម្បីចេញ", + "session.child.promptDisabled": "សម័យ Subagent មិនអាចត្រូវបានសួរ។", + "session.child.backToParent": "ត្រឡប់ទៅសម័យមេ។", + "prompt.example.1": "ជួសជុល TODO នៅក្នុងមូលដ្ឋានកូដ", + "prompt.example.2": "តើអ្វីជាជង់បច្ចេកវិទ្យានៃគម្រោងនេះ?", + "prompt.example.3": "ជួសជុលការធ្វើតេស្តដែលខូច", + "prompt.example.4": "ពន្យល់ពីរបៀបដែលការផ្ទៀងផ្ទាត់ដំណើរការ", + "prompt.example.5": "ស្វែងរក និងជួសជុលភាពងាយរងគ្រោះផ្នែកសុវត្ថិភាព", + "prompt.example.6": "បន្ថែមការធ្វើតេស្តឯកតាសម្រាប់សេវាកម្មអ្នកប្រើប្រាស់", + "prompt.example.7": "កែប្រែមុខងារនេះឱ្យកាន់តែអាចអានបាន។", + "prompt.example.8": "តើកំហុសនេះមានន័យដូចម្តេច?", + "prompt.example.9": "ជួយខ្ញុំដោះស្រាយបញ្ហានេះ។", + "prompt.example.10": "បង្កើតឯកសារ API", + "prompt.example.11": "បង្កើនប្រសិទ្ធភាពសំណួរមូលដ្ឋានទិន្នន័យ", + "prompt.example.12": "បន្ថែមសុពលភាពបញ្ចូល", + "prompt.example.13": "បង្កើតសមាសធាតុថ្មីសម្រាប់...", + "prompt.example.14": "តើខ្ញុំដាក់ពង្រាយគម្រោងនេះដោយរបៀបណា?", + "prompt.example.15": "ពិនិត្យមើលកូដរបស់ខ្ញុំសម្រាប់ការអនុវត្តល្អបំផុត", + "prompt.example.16": "បន្ថែមការដោះស្រាយកំហុសទៅមុខងារនេះ។", + "prompt.example.17": "ពន្យល់ពីលំនាំ regex នេះ។", + "prompt.example.18": "បម្លែងវាទៅជា TypeScript", + "prompt.example.19": "បន្ថែមការកត់ត្រាពេញមូលដ្ឋានកូដ", + "prompt.example.20": "តើភាពអាស្រ័យអ្វីហួសសម័យ?", + "prompt.example.21": "ជួយខ្ញុំសរសេរស្គ្រីបការធ្វើចំណាកស្រុក", + "prompt.example.22": "អនុវត្តឃ្លាំងសម្ងាត់សម្រាប់ចំណុចបញ្ចប់នេះ។", + "prompt.example.23": "បន្ថែមលេខទំព័រទៅក្នុងបញ្ជីនេះ។", + "prompt.example.24": "បង្កើតពាក្យបញ្ជា CLI សម្រាប់...", + "prompt.example.25": "តើអថេរបរិស្ថានដំណើរការនៅទីនេះដោយរបៀបណា?", + "prompt.popover.emptyResults": "គ្មានលទ្ធផលដែលត្រូវគ្នា។", + "prompt.popover.emptyCommands": "គ្មានពាក្យបញ្ជាដែលត្រូវគ្នាទេ។", + "prompt.dropzone.label": "ទម្លាក់រូបភាព PDFs ឬឯកសារអត្ថបទនៅទីនេះ", + "prompt.dropzone.file.label": "ទម្លាក់ទៅឯកសារ @mention", + "prompt.slash.badge.custom": "ផ្ទាល់ខ្លួន", + "prompt.slash.badge.skill": "ជំនាញ", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "សកម្ម", + "prompt.context.includeActiveFile": "រួមបញ្ចូលឯកសារសកម្ម", + "prompt.context.removeActiveFile": "លុបឯកសារសកម្មចេញពីបរិបទ", + "prompt.context.removeFile": "លុបឯកសារចេញពីបរិបទ", + "prompt.action.attachFile": "បន្ថែមឯកសារ", + "prompt.menu.addImagesAndFiles": "បន្ថែមឯកសារ និងច្រើនទៀត", + "prompt.menu.imagesAndFiles": "រូបភាព និងឯកសារ", + "prompt.menu.commands": "ពាក្យបញ្ជា", + "prompt.menu.context": "បរិបទ", + "prompt.menu.shellCommand": "ពាក្យបញ្ជា Shell", + "prompt.attachment.remove": "លុបឯកសារភ្ជាប់ចេញ", + "prompt.action.send": "ផ្ញើ", + "prompt.action.stop": "ឈប់", + "prompt.toast.pasteUnsupported.title": "ឯកសារភ្ជាប់ដែលមិនគាំទ្រ", + "prompt.toast.pasteUnsupported.description": "មានតែរូបភាព PDF ឬឯកសារអត្ថបទប៉ុណ្ណោះដែលអាចភ្ជាប់មកទីនេះបាន។", + "prompt.toast.attachmentDuplicate.title": "ឯកសារនេះត្រូវបានផ្ទុកឡើងរួចហើយ", + "prompt.toast.modelAgentRequired.title": "ជ្រើសរើសភ្នាក់ងារ និងម៉ូដែល", + "prompt.toast.modelAgentRequired.description": "ជ្រើសរើសភ្នាក់ងារ និងម៉ូដែលមុនពេលផ្ញើប្រអប់បញ្ចូល។", + "prompt.toast.worktreeCreateFailed.title": "បរាជ័យក្នុងការបង្កើត Git worktree", + "prompt.toast.sessionCreateFailed.title": "បរាជ័យក្នុងការបង្កើតសម័យ", + "prompt.toast.shellSendFailed.title": "បរាជ័យក្នុងការផ្ញើពាក្យបញ្ជាសែល", + "prompt.toast.commandSendFailed.title": "បរាជ័យក្នុងការផ្ញើពាក្យបញ្ជា", + "prompt.toast.promptSendFailed.title": "បរាជ័យក្នុងការផ្ញើប្រអប់បញ្ចូល", + "prompt.toast.promptSendFailed.description": "មិនអាចទាញយកសម័យបានទេ។", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} នៃ {{total}} ត្រូវបានបើក", + "dialog.mcp.empty": "មិនបានកំណត់រចនាសម្ព័ន្ធ MCPs ទេ។", + "dialog.lsp.empty": "LSPs រកឃើញដោយស្វ័យប្រវត្តិពីប្រភេទឯកសារ", + "dialog.plugins.empty": "កម្មវិធីជំនួយដែលបានកំណត់រចនាសម្ព័ន្ធនៅក្នុង opencode.json", + "mcp.status.connected": "បានភ្ជាប់", + "mcp.status.failed": "បរាជ័យ", + "mcp.status.needs_auth": "ត្រូវការសិទ្ធិ", + "mcp.status.disabled": "បានបិទ", + "mcp.auth.clickToAuthenticate": "ចុចដើម្បីផ្ទៀងផ្ទាត់", + "dialog.fork.empty": "គ្មានសារដែលត្រូវបំបែកពី", + "dialog.directory.search.placeholder": "ស្វែងរកថត", + "dialog.directory.empty": "រកមិនឃើញថតឯកសារទេ។", + "dialog.directory.action.selectFile": "ជ្រើសរើសឯកសារ", + "dialog.directory.action.selectFolder": "ជ្រើសរើសថត", + "dialog.directory.root": "ឫស", + "dialog.directory.parent": "មាតាបិតា", + "dialog.directory.readError": "មិនអាចអានថតឯកសារនេះបានទេ។", + "app.server.unreachable": "មិនអាចទៅដល់ {{server}} បានទេ។", + "app.server.retrying": "កំពុងព្យាយាមម្តងទៀតដោយស្វ័យប្រវត្តិ...", + "app.server.otherServers": "ម៉ាស៊ីនមេផ្សេងទៀត។", + "dialog.server.title": "ម៉ាស៊ីនមេ", + "dialog.server.description": "ប្តូរម៉ាស៊ីនមេ OpenCode ដែលកម្មវិធីនេះភ្ជាប់ទៅ។", + "dialog.server.search.placeholder": "ម៉ាស៊ីនមេស្វែងរក", + "dialog.server.empty": "មិនទាន់មានម៉ាស៊ីនមេទេ។", + "dialog.server.add.title": "បន្ថែមម៉ាស៊ីនមេ", + "dialog.server.add.url": "អាសយដ្ឋានម៉ាស៊ីនមេ", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "មិនអាចភ្ជាប់ទៅម៉ាស៊ីនមេបានទេ។", + "dialog.server.add.checking": "កំពុងពិនិត្យ...", + "dialog.server.add.button": "បន្ថែមម៉ាស៊ីនមេ", + "dialog.server.add.name": "ឈ្មោះម៉ាស៊ីនមេ (ជាជម្រើស)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "ឈ្មោះអ្នកប្រើប្រាស់ (ជាជម្រើស)", + "dialog.server.add.usernamePlaceholder": "ឈ្មោះអ្នកប្រើ", + "dialog.server.add.password": "ពាក្យសម្ងាត់ (ជាជម្រើស)\nពាក្យសម្ងាត់", + "dialog.server.add.passwordPlaceholder": "ពាក្យសម្ងាត់", + "dialog.server.edit.title": "កែសម្រួលម៉ាស៊ីនមេ", + "dialog.server.default.title": "ម៉ាស៊ីនមេលំនាំដើម", + "dialog.server.default.description": + "ភ្ជាប់ទៅម៉ាស៊ីនមេនេះនៅពេលចាប់ផ្តើមកម្មវិធីជំនួសឱ្យការចាប់ផ្តើមម៉ាស៊ីនមេមូលដ្ឋាន។ ទាមទារការចាប់ផ្តើមឡើងវិញ។", + "dialog.server.default.none": "គ្មានម៉ាស៊ីនមេត្រូវបានជ្រើសរើសទេ។", + "dialog.server.default.set": "កំណត់ម៉ាស៊ីនមេបច្ចុប្បន្នជាលំនាំដើម", + "dialog.server.default.clear": "ច្បាស់", + "dialog.server.action.remove": "លុបម៉ាស៊ីនមេ", + "dialog.server.menu.edit": "កែសម្រួល", + "dialog.server.menu.default": "កំណត់ជាលំនាំដើម", + "dialog.server.menu.defaultRemove": "លុបលំនាំដើមចេញ", + "dialog.server.menu.delete": "លុប", + "dialog.server.current": "ម៉ាស៊ីនមេបច្ចុប្បន្ន", + "dialog.server.status.default": "លំនាំដើម", + "wsl.server.add": "បន្ថែមម៉ាស៊ីនមេ WSL", + "wsl.server.addShort": "បន្ថែម WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "ម៉ាស៊ីនមេ WSL", + "wsl.server.retryStart": "ព្យាយាមចាប់ផ្តើមឡើងវិញ", + "wsl.server.updating": "កំពុងអាប់ដេត...", + "wsl.onboarding.step.distro": "ជ្រើសរើស distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "កំពុងពិនិត្យ WSL...", + "wsl.onboarding.restartRequired": "វីនដូត្រូវការការចាប់ផ្តើមឡើងវិញដើម្បីបញ្ចប់ការដំឡើង WSL ។", + "wsl.onboarding.ready": "WSL រួចរាល់ហើយ។", + "wsl.onboarding.required": "WSL ត្រូវបានទាមទារដើម្បីបន្ត។", + "wsl.onboarding.checkingDistros": "កំពុងពិនិត្យមើលការចែកចាយ...", + "wsl.onboarding.installingDistro": "កំពុងដំឡើង {{distro}}...", + "wsl.onboarding.checkingDistro": "កំពុងពិនិត្យ {{distro}}...", + "wsl.onboarding.listingDistros": "កំពុងរាយបញ្ជីការចែកចាយ...", + "wsl.onboarding.distroReady": "{{distro}} រួចរាល់ហើយ។", + "wsl.onboarding.distroNotInstalled": "{{distro}} មិនទាន់ដំឡើងនៅឡើយទេ។", + "wsl.onboarding.openDistroOnce": "បើក {{distro}} ម្តង ដើម្បីបញ្ចប់ការដំឡើង។", + "wsl.onboarding.finishingDistro": "បញ្ចប់ការដំឡើងសម្រាប់ {{distro}} ។", + "wsl.onboarding.pickDistro": "ជ្រើសរើស distro ឬដំឡើងមួយខាងក្រោម។", + "wsl.onboarding.checkingOpencode": "កំពុងពិនិត្យមើលកូដ OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "កំពុងពិនិត្យកូដ OpenCode ក្នុង {{distro}}...", + "wsl.onboarding.updatingOpencode": "កំពុងអាប់ដេត OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "កំពុងអាប់ដេត OpenCode ក្នុង {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "ធ្វើបច្ចុប្បន្នភាព OpenCode ក្នុង {{distro}} ។", + "wsl.onboarding.updateOpencode": "ធ្វើបច្ចុប្បន្នភាព OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode គឺរួចរាល់នៅក្នុង {{distro}} ។", + "wsl.onboarding.opencodeReady": "OpenCode គឺរួចរាល់។", + "wsl.onboarding.installOpencodeIn": "ដំឡើង OpenCode ក្នុង {{distro}} ។", + "wsl.onboarding.installOpencode": "ដំឡើង OpenCode", + "wsl.onboarding.chooseDistroFirst": "ជ្រើសរើសការចែកចាយជាមុនសិន។", + "wsl.onboarding.loadFailed": "បានបរាជ័យក្នុងការផ្ទុកស្ថានភាព WSL ។", + "wsl.onboarding.loading": "កំពុងផ្ទុក...", + "wsl.onboarding.installedDistros": "បានដំឡើងការចែកចាយ", + "wsl.onboarding.checkAgain": "ពិនិត្យម្តងទៀត", + "wsl.onboarding.distroStatus.ready": "រួចរាល់", + "wsl.onboarding.distroStatus.checking": "កំពុងពិនិត្យ...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode មិនត្រូវបានដំឡើងទេ។", + "wsl.onboarding.distroStatus.missingTools": "បាត់ bash, curl", + "wsl.onboarding.distroStatus.unsupported": "មិនគាំទ្រ · ប្រើ WSL 2", + "wsl.onboarding.needAnotherDistro": "ត្រូវការអ្នកចែកចាយផ្សេងទៀតទេ?", + "wsl.onboarding.needAnotherDistroHint": "ដំឡើងការចែកចាយលីនុចពីកាតាឡុក WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL មិនត្រូវបានដំឡើងទេ។", + "wsl.onboarding.wslNotInstalled.description": + "WSL (ប្រព័ន្ធរងវីនដូសម្រាប់លីនុច) ត្រូវបានទាមទារ មុនពេល OpenCode អាចបន្ថែមម៉ាស៊ីនមេ WSL", + "wsl.onboarding.wslUnavailable.title": "WSL មិនអាចប្រើបានទេ។", + "wsl.onboarding.wslUnavailable.description": "OpenCode មិនអាចផ្ទៀងផ្ទាត់ WSL នៅលើម៉ាស៊ីននេះបានទេ។", + "wsl.onboarding.installWsl": "ដំឡើង WSL", + "wsl.onboarding.windowsRestartRequired": + "ចាប់ផ្ដើម Windows ឡើងវិញ ដើម្បីបញ្ចប់ការដំឡើង WSL បន្ទាប់មកបើក OpenCode ឡើងវិញ។", + "wsl.onboarding.next": "បន្ទាប់", + "wsl.onboarding.refresh": "ធ្វើឱ្យស្រស់", + "wsl.onboarding.allDistrosAdded": "ការចែកចាយដែលបានដំឡើងទាំងអស់ត្រូវបានបន្ថែមរួចហើយ។", + "wsl.onboarding.noDistros": "មិនបានរកឃើញការចែកចាយនៅឡើយទេ។", + "wsl.onboarding.install": "ដំឡើង", + "wsl.onboarding.installing": "កំពុងដំឡើង...", + "wsl.onboarding.installDistro": "ដំឡើង distro", + "wsl.onboarding.searchDistros": "ស្វែងរកការចែកចាយ", + "wsl.onboarding.wsl2Required": "WSL 2 ត្រូវបានទាមទារ។", + "wsl.onboarding.toolsRequired": "ការចែកចាយនេះត្រូវការ bash និង curl ។", + "wsl.onboarding.openTerminal": "បើកស្ថានីយ", + "wsl.onboarding.path": "ផ្លូវ៖ {{path}}", + "wsl.onboarding.notFound": "រកមិនឃើញទេ។", + "wsl.onboarding.version": "កំណែ៖ {{version}}", + "wsl.onboarding.unknown": "មិនស្គាល់", + "wsl.onboarding.desktopVersion": "ផ្ទៃតុ {{version}}", + "wsl.onboarding.versionMismatch": "កំណែដែលបានដំឡើងមិនត្រូវគ្នានឹងកំណែកម្មវិធីកុំព្យូទ័រលើតុទេ។", + "wsl.onboarding.adding": "កំពុងបន្ថែម...", + "help.tabs.toast.ariaLabel": "ការណែនាំផ្ទាំង។ រៀបចំការងារ និងសម័យសកម្មរបស់អ្នកដោយប្រើផ្ទាំង", + "help.tabs.toast.dismiss": "ច្រានចោលព័ត៌មានផ្ទាំង", + "help.tabs.title": "ការណែនាំផ្ទាំង", + "help.tabs.description": "រៀបចំការងារ និងសម័យសកម្មរបស់អ្នកជាមួយផ្ទាំង", + "help.tabs.date": "ថ្ងៃទី 14 ខែកក្កដា", + "help.tabs.introduction": "OpenCode Desktop ឥឡូវនេះត្រូវបានបង្កើតឡើងនៅជុំវិញផ្ទាំង។", + "help.tabs.sessions": + "ចាប់ផ្តើមសម័យថ្មីនៅក្នុងផ្ទាំងមួយ ឬបើកសម័យដែលមានស្រាប់ពីគម្រោងណាមួយរបស់អ្នក។ បើកផ្ទាំងថ្មីនៅពេលអ្នកចាប់ផ្តើមអ្វីថ្មី ហើយបិទវានៅពេលអ្នករួចរាល់។", + "help.tabs.organize": + "ការបន្តបើកផ្ទាំងមួយចំនួនធ្វើឱ្យវាកាន់តែងាយស្រួលក្នុងការរៀបចំសម័យសកម្មរបស់អ្នក។ ប្តូរឈ្មោះផ្ទាំងទៅជាអ្វីមួយដែលគួរឱ្យចងចាំ ប្រសិនបើអ្នកមានគម្រោងរក្សាពួកវាឱ្យនៅជាប់។", + "help.tabs.home": + "អ្នកនឹងរកឃើញសម័យ និងគម្រោងរបស់អ្នកទាំងអស់នៅលើអេក្រង់ដើមថ្មី។ ការជ្រើសរើសសម័យបើកវានៅក្នុងផ្ទាំងមួយ។", + "help.tabs.persistence": "នៅពេលអ្នកបើកកម្មវិធីឡើងវិញ ផ្ទាំងរបស់អ្នកនៅតែបើក។", + "help.tabs.worktrees": + "ការរចនាថ្មីមិនទាន់គាំទ្រ Git Worktrees នៅឡើយទេ វានឹងមកដល់ក្នុងពេលឆាប់ៗនេះ។ ដូច្នេះប្រសិនបើអ្នកចង់បន្តប្រើប្លង់ពីមុន អ្នកអាចប្តូររវាងប្លង់នៅក្នុងការកំណត់។ សូមចងចាំថាប្លង់ថ្មីនឹងក្លាយទៅជាអចិន្ត្រៃយ៍ក្នុងរយៈពេលពីរបីសប្តាហ៍។", + "server.row.noUsername": "គ្មានឈ្មោះអ្នកប្រើប្រាស់", + "dialog.project.edit.title": "កែសម្រួលគម្រោង", + "dialog.project.edit.name": "ឈ្មោះ", + "dialog.project.edit.icon": "រូបតំណាង", + "dialog.project.edit.icon.alt": "រូបតំណាងគម្រោង", + "dialog.project.edit.icon.hint": "ចុច ឬអូសរូបភាព", + "dialog.project.edit.icon.recommended": "បានណែនាំ៖ 128x128px", + "dialog.project.edit.color": "ពណ៌", + "dialog.project.edit.color.select": "ជ្រើសរើសពណ៌ {{color}}", + "dialog.project.edit.worktree.startup": "ស្គ្រីបចាប់ផ្តើមកន្លែងធ្វើការ", + "dialog.project.edit.worktree.startup.description": "ដំណើរការបន្ទាប់ពីបង្កើតកន្លែងធ្វើការថ្មី (Git worktree)។", + "dialog.project.edit.worktree.startup.placeholder": "ឧ. ដំឡើងប៊ុន", + "dialog.releaseNotes.action.getStarted": "ចាប់ផ្តើម", + "dialog.releaseNotes.action.next": "បន្ទាប់", + "dialog.releaseNotes.action.hideFuture": "កុំបង្ហាញទាំងនេះនៅពេលអនាគត", + "dialog.releaseNotes.media.alt": "ចេញផ្សាយការមើលជាមុន", + "context.breakdown.title": "ការបំបែកបរិបទ", + "context.breakdown.note": 'ការបំបែកប្រហាក់ប្រហែលនៃនិមិត្តសញ្ញាបញ្ចូល។ "ផ្សេងទៀត" រួមបញ្ចូលនិយមន័យឧបករណ៍ និងតម្លៃលើស។', + "context.breakdown.system": "ប្រព័ន្ធ", + "context.breakdown.user": "អ្នកប្រើប្រាស់", + "context.breakdown.assistant": "ជំនួយការ", + "context.breakdown.tool": "ការហៅឧបករណ៍", + "context.breakdown.other": "ផ្សេងទៀត។", + "context.systemPrompt.title": "ប្រអប់បញ្ចូលប្រព័ន្ធ", + "context.rawMessages.title": "សារឆៅ", + "context.export.session": "សម័យនាំចេញ", + "context.stats.session": "សម័យ", + "context.stats.messages": "សារ", + "context.stats.provider": "អ្នកផ្តល់សេវា", + "context.stats.model": "ម៉ូដែល", + "context.stats.limit": "ដែនកំណត់បរិបទ", + "context.stats.totalTokens": "ថូខឹនសរុប", + "context.stats.usage": "ការប្រើប្រាស់", + "context.stats.inputTokens": "បញ្ចូលសញ្ញាសម្ងាត់", + "context.stats.outputTokens": "Output Token", + "context.stats.reasoningTokens": "សញ្ញាសម្គាល់ហេតុផល", + "context.stats.cacheTokens": "Cache Token (អាន/សរសេរ)", + "context.stats.userMessages": "សារអ្នកប្រើប្រាស់", + "context.stats.assistantMessages": "សារជំនួយ", + "context.stats.totalCost": "តម្លៃសរុប", + "context.stats.sessionCreated": "សម័យត្រូវបានបង្កើត", + "context.stats.lastActivity": "សកម្មភាពចុងក្រោយ", + "context.usage.tokens": "សញ្ញាសម្ងាត់", + "context.usage.usage": "ការប្រើប្រាស់", + "context.usage.cost": "តម្លៃ", + "context.usage.clickToView": "ចុចដើម្បីមើលបរិបទ", + "context.usage.view": "មើលការប្រើប្រាស់បរិបទ", + "language.en": "ភាសាអង់គ្លេស", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "អេស្ប៉ាណុល", + "language.fr": "បារាំង", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "ប៉ូលស្គី", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "ព័រទុយហ្គាល់ (ប្រេស៊ីល)", + "language.bs": "បូសានស្គី", + "language.uk": "Українська", + "language.th": "ថៃ", + "language.tr": "Türkçe", + "toast.language.title": "ភាសា", + "toast.language.description": "បានប្តូរទៅ {{language}}", + "toast.theme.title": "បានប្តូររូបរាង", + "toast.scheme.title": "ពណ៌ចម្រុះ", + "toast.workspace.enabled.title": "បានបើកកន្លែងធ្វើការ", + "toast.workspace.enabled.description": "ឥឡូវនេះ Git worktree ជាច្រើនត្រូវបានបង្ហាញក្នុងរបារចំហៀង", + "toast.workspace.disabled.title": "កន្លែងធ្វើការត្រូវបានបិទ", + "toast.workspace.disabled.description": "មានតែ Git worktree ចម្បងប៉ុណ្ណោះដែលត្រូវបានបង្ហាញក្នុងរបារចំហៀង", + "toast.permissions.autoaccept.on.title": "ការអនុញ្ញាតដោយស្វ័យប្រវត្តិ", + "toast.permissions.autoaccept.on.description": "សំណើការអនុញ្ញាតនឹងត្រូវបានអនុម័តដោយស្វ័យប្រវត្តិ", + "toast.permissions.autoaccept.off.title": "បានបញ្ឈប់ការអនុញ្ញាតដោយស្វ័យប្រវត្តិ", + "toast.permissions.autoaccept.off.description": "សំណើសុំការអនុញ្ញាតនឹងទាមទារការយល់ព្រម", + "toast.model.none.title": "មិនបានជ្រើសរើសម៉ូដែលទេ។", + "toast.model.none.description": "ភ្ជាប់អ្នកផ្តល់សេវាដើម្បីសង្ខេបសម័យនេះ។", + "toast.file.loadFailed.title": "បរាជ័យក្នុងការផ្ទុកឯកសារ", + "toast.file.listFailed.title": "បរាជ័យក្នុងបញ្ជីឯកសារ", + "toast.context.noLineSelection.title": "គ្មានការជ្រើសរើសបន្ទាត់ទេ។", + "toast.context.noLineSelection.description": "ជ្រើសរើសជួរបន្ទាត់ក្នុងផ្ទាំងឯកសារជាមុនសិន។", + "toast.session.share.copyFailed.title": "បរាជ័យក្នុងការចម្លង URL ទៅក្ដារតម្បៀតខ្ទាស់", + "toast.session.share.success.title": "សម័យដែលបានចែករំលែក", + "toast.session.share.success.description": "ចែករំលែក URL ដែលបានចម្លងទៅក្ដារតម្បៀតខ្ទាស់!", + "toast.session.share.failed.title": "បរាជ័យក្នុងការចែករំលែកសម័យ", + "toast.session.share.failed.description": "កំហុសបានកើតឡើងខណៈពេលចែករំលែកសម័យ", + "toast.session.unshare.success.title": "សម័យមិនត្រូវបានចែករំលែក", + "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": "មានបច្ចុប្បន្នភាព", + "toast.update.description": "កំណែថ្មីនៃ OpenCode ({{version}}) ឥឡូវនេះមានសម្រាប់ដំឡើងហើយ។", + "toast.update.action.installRestart": "ដំឡើង និងចាប់ផ្តើមឡើងវិញ", + "toast.update.action.notYet": "មិនទាន់", + "error.page.title": "មានអ្វីមួយខុសប្រក្រតី", + "error.page.description": "កំហុសបានកើតឡើងខណៈពេលកំពុងផ្ទុកកម្មវិធី។", + "error.page.description.localServerStartup": "កំហុសបានកើតឡើងខណៈពេលចាប់ផ្តើមម៉ាស៊ីនមេមូលដ្ឋាន។", + "error.page.details.label": "ព័ត៌មានលម្អិតអំពីកំហុស", + "error.page.action.restart": "ចាប់ផ្ដើមឡើងវិញ", + "error.page.action.report": "រាយការណ៍កំហុស", + "error.page.action.reported": "កំហុសត្រូវបានរាយការណ៍", + "error.page.action.exportLogs": "នាំចេញកំណត់ហេតុ", + "error.page.action.checking": "កំពុងពិនិត្យ...", + "error.page.action.checkUpdates": "ពិនិត្យមើលបច្ចុប្បន្នភាព", + "error.page.action.updateTo": "ធ្វើបច្ចុប្បន្នភាពទៅ {{version}}", + "error.page.circular": "[សារាចរ]", + "error.page.report.prefix": "សូមរាយការណ៍ពីកំហុសនេះទៅកាន់ក្រុម OpenCode", + "error.page.report.discord": "នៅលើ Discord", + "error.page.version": "កំណែ៖ {{version}}", + "error.dev.rootNotFound": + "រកមិនឃើញធាតុឫសទេ។ តើអ្នកភ្លេចបន្ថែមវាទៅ index.html របស់អ្នកទេ? ឬប្រហែលជាគុណលក្ខណៈលេខសម្គាល់ត្រូវបានសរសេរខុស?", + "error.serverSync.connectFailed": "មិនអាចភ្ជាប់ទៅម៉ាស៊ីនមេបានទេ។ តើមានម៉ាស៊ីនមេដែលកំពុងដំណើរការនៅ `{{url}}` ទេ?", + "error.serverSDK.noServerAvailable": "មិនមានម៉ាស៊ីនមេទេ។", + "error.serverSDK.serverNotAvailable": "ម៉ាស៊ីនមេមិនមានទេ។", + "error.childStore.persistedCacheCreateFailed": "បានបរាជ័យក្នុងការបង្កើតឃ្លាំងសម្ងាត់ដែលបន្ត", + "error.childStore.persistedProjectMetadataCreateFailed": "បានបរាជ័យក្នុងការបង្កើតទិន្នន័យមេតាគម្រោងដែលជាប់", + "error.childStore.persistedProjectIconCreateFailed": "បានបរាជ័យក្នុងការបង្កើតរូបតំណាងគម្រោងដែលបន្ត", + "error.childStore.storeCreateFailed": "បរាជ័យក្នុងការបង្កើតហាង", + "directory.error.invalidUrl": "ថតមិនត្រឹមត្រូវនៅក្នុង URL ។", + "error.chain.unknown": "កំហុសមិនស្គាល់", + "error.server.invalidConfiguration": "ការកំណត់មិនត្រឹមត្រូវ", + "error.chain.causedBy": "បង្កឡើងដោយ៖", + "error.chain.apiError": "កំហុស API", + "error.chain.status": "ស្ថានភាព៖ {{status}}", + "error.chain.retryable": "អាចសាកល្បងបាន៖ {{retryable}}", + "error.chain.responseBody": "តួការឆ្លើយតប៖\n{{body}}", + "error.chain.didYouMean": "តើអ្នកមានន័យថា៖ {{suggestions}}", + "error.chain.modelNotFound": "រកមិនឃើញម៉ូដែល៖ {{provider}}/{{model}}", + "error.chain.checkConfig": "ពិនិត្យការកំណត់រចនាសម្ព័ន្ធរបស់អ្នក (opencode.json) អ្នកផ្តល់សេវា/ឈ្មោះម៉ូដែល", + "error.chain.mcpFailed": 'ម៉ាស៊ីនមេ MCP "{{name}}" បានបរាជ័យ។ ចំណាំ OpenCode មិនគាំទ្រការផ្ទៀងផ្ទាត់ MCP នៅឡើយទេ។', + "error.chain.providerAuthFailed": "ការផ្ទៀងផ្ទាត់អ្នកផ្តល់សេវាបានបរាជ័យ ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'បានបរាជ័យក្នុងការចាប់ផ្តើមអ្នកផ្តល់សេវា "{{provider}}" ។ ពិនិត្យអត្តសញ្ញាណ និងការកំណត់រចនាសម្ព័ន្ធ។', + "error.chain.configJsonInvalid": "ឯកសារកំណត់រចនាសម្ព័ន្ធនៅ {{path}} មិនត្រឹមត្រូវ JSON(C)", + "error.chain.configJsonInvalidWithMessage": "ឯកសារកំណត់រចនាសម្ព័ន្ធនៅ {{path}} មិនត្រឹមត្រូវទេ JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'ថត "{{dir}}" នៅក្នុង {{path}} មិនត្រឹមត្រូវទេ។ ប្តូរឈ្មោះថតទៅជា "{{suggestion}}" ឬលុបវាចេញ។ នេះ​ជា​ការ​វាយ​ខុស​ធម្មតា។', + "error.chain.configFrontmatterError": "បរាជ័យក្នុងការញែកបញ្ហាមុខនៅក្នុង {{path}}៖\n{{message}}", + "error.chain.configInvalid": "ឯកសារកំណត់រចនាសម្ព័ន្ធនៅ {{path}} មិនត្រឹមត្រូវទេ។", + "error.chain.configInvalidWithMessage": "ឯកសារកំណត់រចនាសម្ព័ន្ធនៅ {{path}} មិនត្រឹមត្រូវទេ៖ {{message}}", + "notification.permission.title": "ទាមទារការអនុញ្ញាត", + "notification.permission.description": "{{sessionTitle}} ក្នុង {{projectName}} ត្រូវការការអនុញ្ញាត", + "notification.question.title": "សំណួរ", + "notification.question.description": "{{sessionTitle}} នៅក្នុង {{projectName}} មានសំណួរមួយ។", + "notification.action.goToSession": "ចូលទៅកាន់សម័យ", + "notification.session.responseReady.title": "ការឆ្លើយតបរួចរាល់", + "notification.session.error.title": "កំហុសសម័យ", + "notification.session.error.fallbackDescription": "កំហុសបានកើតឡើង", + "home.recentProjects": "គម្រោងថ្មីៗ", + "home.empty.title": "មិនមានគម្រោងថ្មីៗទេ។", + "home.empty.description": "ចាប់ផ្តើមដោយការបើកគម្រោងក្នុងស្រុក", + "home.title": "ទំព័រដើម", + "home.projects": "គម្រោង", + "home.project.add": "បន្ថែមគម្រោង", + "home.recentlyClosed": "ថ្មីៗនេះបានបិទ", + "home.server.collapse": "បង្រួមគម្រោងម៉ាស៊ីនមេ", + "home.server.expand": "ពង្រីកគម្រោងម៉ាស៊ីនមេ", + "home.sessions.search.placeholder": "សម័យស្វែងរក", + "home.sessions.search.placeholder.scoped": "សម័យស្វែងរកក្នុង {{scope}}", + "home.sessions.search.sessions": "សម័យ", + "home.sessions.search.noResults": "រកមិនឃើញសម័យសម្រាប់ {{query}} ទេ។", + "home.sessions.empty": "គ្មានអ្វីនៅទីនេះទេ។", + "home.sessions.empty.description": "បង្កើតសម័យដើម្បីចាប់ផ្តើម", + "home.sessions.group.today": "ថ្ងៃនេះ", + "home.sessions.group.yesterday": "ម្សិលមិញ", + "home.sessions.group.older": "ចាស់ជាង", + "home.providerTip": "ភ្ជាប់ជាមួយអ្នកផ្តល់សេវា 75+ ដើម្បីប្រើម៉ូដែលផ្សេងទៀត រួមទាំង Claude, GPT, Gemini ជាដើម។", + "session.tab.session": "សម័យ", + "session.tab.review": "ពិនិត្យ", + "session.tab.context": "បរិបទ", + "session.tab.unknown": "សម័យមិនស្គាល់", + "session.panel.reviewAndFiles": "ពិនិត្យ និងឯកសារ", + "session.error.notFound": "សម័យនេះមិនអាចរកឃើញទេ។", + "session.error.notFound.description": "ផ្ទាំងនេះចង្អុលទៅសម័យដែលលែងមាននៅលើម៉ាស៊ីនមេនេះ។", + "session.error.notFound.closeTab": "បិទផ្ទាំង", + "session.error.serverConnection": "មិនអាចភ្ជាប់ទៅម៉ាស៊ីនមេនេះទេ។", + "session.review.filesChanged": "ឯកសារបានផ្លាស់ប្តូរ {{count}}", + "session.review.change.one": "ការផ្លាស់ប្តូរ", + "session.review.change.other": "ការផ្លាស់ប្តូរ", + "session.review.loadingChanges": "កំពុងផ្ទុកការផ្លាស់ប្តូរ...", + "session.review.empty": "មិនមានការផ្លាស់ប្តូរនៅក្នុងសម័យនេះនៅឡើយទេ", + "session.review.noVcs": "គ្មានប្រព័ន្ធគ្រប់គ្រងកំណែ Git ត្រូវបានរកឃើញទេ ការផ្លាស់ប្តូរមិនត្រូវបានបង្ហាញទេ។", + "session.review.noVcs.createGit.title": "បង្កើតឃ្លាំង Git", + "session.review.noVcs.createGit.description": "តាមដាន ពិនិត្យ និងបោះបង់ការផ្លាស់ប្ដូរនៅក្នុងគម្រោងនេះ។", + "session.review.noVcs.createGit.actionLoading": "ការបង្កើតឃ្លាំង Git...", + "session.review.noVcs.createGit.action": "បង្កើតឃ្លាំង Git", + "session.review.noSnapshot": "ការតាមដានរូបថតត្រូវបានបិទនៅក្នុងការកំណត់ ដូច្នេះការផ្លាស់ប្តូរវេនមិនអាចប្រើបានទេ", + "session.review.noChanges": "គ្មានការផ្លាស់ប្តូរទេ។", + "session.review.noUncommittedChanges": "មិនទាន់មានការផ្លាស់ប្តូរដែលមិនទាន់បានកំណត់នៅឡើយ", + "session.review.noBranchChanges": "មិនទាន់មានការផ្លាស់ប្តូរសាខានៅឡើយទេ", + "session.files.selectToOpen": "ជ្រើសរើសឯកសារដើម្បីបើក", + "session.files.all": "ឯកសារទាំងអស់។", + "session.files.empty": "គ្មានឯកសារទេ។", + "session.files.binaryContent": "ឯកសារគោលពីរ (មាតិកាមិនអាចត្រូវបានបង្ហាញ)", + "session.messages.renderEarlier": "បង្ហាញសារមុន។", + "session.messages.loadingEarlier": "កំពុងផ្ទុកសារមុន...", + "session.messages.loadEarlier": "ផ្ទុកសារមុន។", + "session.messages.loading": "កំពុងផ្ទុកសារ...", + "session.messages.jumpToLatest": "លោតទៅចុងក្រោយបំផុត។", + "session.context.addToContext": "បន្ថែម {{selection}} ទៅបរិបទ", + "session.todo.title": "អ្វីដែលត្រូវធ្វើ", + "session.todo.collapse": "ដួលរលំ", + "session.todo.expand": "ពង្រីក", + "session.todo.progress": "{{done}} នៃ {{total}} ការងារត្រូវធ្វើបានបញ្ចប់", + "session.question.progress": "{{current}} នៃសំណួរ {{total}}", + "session.question.minimize": "បង្រួមសំណួរ", + "session.question.restore": "ស្ដារសំណួរ", + "session.question.pending.one": "{{count}} សំណួរដែលមិនទាន់សម្រេច", + "session.question.pending.other": "{{count}} សំណួរមិនទាន់សម្រេច", + "session.followupDock.summary.one": "សារដែលបានតម្រង់ជួរ {{count}}", + "session.followupDock.summary.other": "{{count}} សារដែលបានដាក់ជាជួរ", + "session.followupDock.sendNow": "ផ្ញើឥឡូវនេះ", + "session.followupDock.edit": "កែសម្រួល", + "session.followupDock.collapse": "បង្រួមសារដែលបានដាក់ជាជួរ", + "session.followupDock.expand": "ពង្រីកសារដែលបានដាក់ជាជួរ", + "session.revertDock.summary.one": "{{count}} ត្រឡប់សារវិញ។", + "session.revertDock.summary.other": "{{count}} ត្រឡប់សារវិញ។", + "session.revertDock.collapse": "បង្រួមសារដែលបានវិលត្រឡប់មកវិញ", + "session.revertDock.expand": "ពង្រីកសារត្រឡប់", + "session.revertDock.restore": "ស្តារសារឡើងវិញ", + "session.new.title": "សាងសង់អ្វីក៏បាន", + "session.new.project.new": "គម្រោងថ្មី។", + "session.new.project.search": "គម្រោងស្វែងរក", + "session.new.project.add": "បន្ថែមគម្រោង", + "session.new.worktree.main": "សាខាចម្បង", + "session.new.worktree.mainWithBranch": "សាខាចម្បង ({{branch}})", + "session.new.worktree.create": "បង្កើត Git worktree ថ្មី", + "session.new.workspace.runIn": "ដំណើរការសម័យក្នុង", + "session.new.workspace.triggerLocal": "ក្នុងស្រុក", + "session.new.workspace.local": "ឃ្លាំងមូលដ្ឋាន", + "session.new.workspace.existing": "កន្លែងធ្វើការ…", + "session.new.git.none": "គ្មាន Git", + "session.new.lastModified": "កែប្រែចុងក្រោយ", + "session.header.search.placeholder": "ស្វែងរក {{project}}", + "session.header.searchFiles": "ស្វែងរកឯកសារ", + "session.header.openIn": "បើកចូល", + "session.header.open.action": "បើក {{app}}", + "session.header.open.ariaLabel": "បើកក្នុង {{app}}", + "session.header.open.menu": "ជម្រើសបើក", + "session.header.open.copyPath": "ចម្លងផ្លូវ", + "session.header.open.finder": "អ្នកស្វែងរក", + "session.header.open.fileExplorer": "កម្មវិធីរុករកឯកសារ", + "session.header.open.fileManager": "កម្មវិធីគ្រប់គ្រងឯកសារ", + "session.header.reveal.finder": "បង្ហាញនៅក្នុង Finder", + "session.header.reveal.fileExplorer": "បង្ហាញនៅក្នុង File Explorer", + "session.header.reveal.containingFolder": "បើកថតដែលមាន", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "ស្ថានភាព", + "status.popover.ariaLabel": "ការកំណត់រចនាសម្ព័ន្ធម៉ាស៊ីនមេ", + "status.popover.tab.servers": "ម៉ាស៊ីនមេ", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "កម្មវិធីជំនួយ", + "status.popover.action.manageServers": "គ្រប់គ្រងម៉ាស៊ីនមេ", + "session.share.popover.title": "បោះផ្សាយនៅលើគេហទំព័រ", + "session.share.popover.description.shared": + "សម័យនេះគឺសាធារណៈនៅលើគេហទំព័រ។ វា​អាច​ចូល​ប្រើ​បាន​សម្រាប់​អ្នក​ណា​ដែល​មាន​តំណ។", + "session.share.popover.description.unshared": + "ចែករំលែកសម័យជាសាធារណៈនៅលើគេហទំព័រ។ វានឹងអាចចូលប្រើបានសម្រាប់អ្នកដែលមានតំណ។", + "session.share.action.share": "ចែករំលែក", + "session.share.action.publish": "បោះពុម្ព", + "session.share.action.publishing": "ការបោះពុម្ព...", + "session.share.action.unpublish": "ឈប់បោះផ្សាយ", + "session.share.action.unpublishing": "កំពុងឈប់បោះពុម្ព...", + "session.share.action.view": "មើល", + "session.share.copy.copied": "ចម្លង", + "session.share.copy.copyLink": "ចម្លងតំណ", + "lsp.tooltip.none": "គ្មានម៉ាស៊ីនមេ LSP ទេ។", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "កំពុង​ផ្ទុក​ប្រអប់បញ្ចូល...", + "terminal.loading": "កំពុងផ្ទុកស្ថានីយ...", + "terminal.title": "ស្ថានីយ", + "terminal.title.numbered": "ស្ថានីយ {{number}}", + "terminal.close": "បិទស្ថានីយ", + "terminal.connectionLost.title": "ការតភ្ជាប់បាត់", + "terminal.connectionLost.abnormalClose": "WebSocket បានបិទមិនធម្មតា៖ {{code}}", + "terminal.connectionLost.description": "ការតភ្ជាប់ស្ថានីយត្រូវបានរំខាន។ វាអាចកើតឡើងនៅពេលដែលម៉ាស៊ីនមេចាប់ផ្តើមឡើងវិញ។", + "terminal.connectTicket.csrfError": + "សំបុត្រភ្ជាប់ PTY ត្រូវបានបដិសេធដោយប្រភពដើម ឬការត្រួតពិនិត្យ CSRF ។ ពិនិត្យការកំណត់រចនាសម្ព័ន្ធ CORS ម៉ាស៊ីនមេ។", + "terminal.connectTicket.statusError": "សំបុត្រភ្ជាប់ PTY បានបរាជ័យជាមួយ {{status}}", + "titlebar.update": "អាប់ដេត", + "titlebar.updateVersion": "ធ្វើបច្ចុប្បន្នភាព {{version}}", + "common.closeTab": "បិទផ្ទាំង", + "common.dismiss": "ច្រានចោល", + "common.moreCountSuffix": "(+{{count}} ច្រើនទៀត)", + "common.requestFailed": "សំណើបានបរាជ័យ", + "common.moreOptions": "ជម្រើសច្រើនទៀត", + "common.learnMore": "ស្វែងយល់បន្ថែម", + "common.rename": "ប្តូរឈ្មោះ", + "common.export": "នាំចេញ", + "common.reset": "កំណត់ឡើងវិញ", + "common.archive": "បណ្ណសារ", + "common.delete": "លុប", + "common.close": "បិទ", + "common.edit": "កែសម្រួល", + "common.loadMore": "ផ្ទុកច្រើនទៀត", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "មិនស្គាល់", + "common.time.justNow": "ឥឡូវនេះ", + "common.time.minutesAgo.short": "{{count}}m មុន។", + "common.time.hoursAgo.short": "{{count}}h កន្លងទៅ", + "common.time.daysAgo.short": "{{count}}d មុន។", + "sidebar.menu.toggle": "បិទ/បើកម៉ឺនុយ", + "sidebar.nav.projectsAndSessions": "គម្រោង និងសម័យ", + "sidebar.settings": "ការកំណត់", + "sidebar.help": "ជំនួយ", + "sidebar.workspaces.enable": "បើកកន្លែងធ្វើការ", + "sidebar.workspaces.disable": "បិទកន្លែងធ្វើការ", + "sidebar.gettingStarted.title": "ការចាប់ផ្តើម", + "sidebar.gettingStarted.line1": "OpenCode រួមបញ្ចូលម៉ូដែលឥតគិតថ្លៃ ដូច្នេះអ្នកអាចចាប់ផ្តើមភ្លាមៗ។", + "sidebar.gettingStarted.line2": "ភ្ជាប់អ្នកផ្តល់សេវាណាមួយដើម្បីប្រើម៉ូដែល, inc ។ Claude, GPT, Gemini ជាដើម។", + "sidebar.project.recentSessions": "សម័យថ្មីៗ", + "sidebar.project.viewAllSessions": "មើលសម័យទាំងអស់។", + "sidebar.project.clearNotifications": "ជម្រះការជូនដំណឹង", + "sidebar.empty.title": "គ្មានគម្រោងបើកទេ។", + "sidebar.empty.description": "បើកគម្រោងដើម្បីចាប់ផ្តើម", + "debugBar.ariaLabel": "ការវិនិច្ឆ័យដំណើរការអភិវឌ្ឍន៍", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "ការផ្លាស់ប្តូរផ្លូវដែលបានបញ្ចប់ចុងក្រោយដោយប៉ះទំព័រសម័យមួយ ដែលវាស់វែងពីការចាប់ផ្តើមរ៉ោតទ័រ រហូតដល់ការលាបពណ៌ដំបូងបន្ទាប់ពីវាដោះស្រាយ។", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "ស៊ុមវិលក្នុងមួយវិនាទីក្នុងរយៈពេល 5 វិនាទីចុងក្រោយ។", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "ពេលវេលាស៊ុមអាក្រក់បំផុតក្នុងរយៈពេល 5 វិនាទីចុងក្រោយ។", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "ស៊ុមលើសពី 32ms ក្នុងរយៈពេល 5 វិនាទីចុងក្រោយ។", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "ពេលវេលាដែលបានទប់ស្កាត់ និងចំនួនកិច្ចការយូរក្នុងរយៈពេល 5 វិនាទីចុងក្រោយ។ ភារកិច្ចអតិបរមា៖ {{max}} ។", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "ការពន្យាពេលបញ្ចូលដែលបានសង្កេតឃើញអាក្រក់បំផុតក្នុងរយៈពេល 5 វិនាទីចុងក្រោយ។", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "រយៈពេលអន្តរកម្មប្រហាក់ប្រហែលក្នុងរយៈពេល 5 វិនាទីចុងក្រោយ។ នេះគឺដូច INP មិនមែន Web Vitals INP ផ្លូវការទេ។", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "ការផ្លាស់ប្តូរប្លង់សរុបសម្រាប់អាយុកាលកម្មវិធីបច្ចុប្បន្ន។", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "បានប្រើ JS heap ទល់នឹង heap limit ។ Chromium តែប៉ុណ្ណោះ។", + "debugBar.mem.tip": "បានប្រើ JS heap ទល់នឹង heap limit ។ {{used}} នៃ {{limit}} ។", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "បង្ខំរចនាប័ទ្មផ្តោតទៅលើធាតុអន្តរកម្មទាំងអស់។", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "បង្ខំទិសដៅប្លង់កម្មវិធីពេញលេញដោយមិនផ្លាស់ប្តូរភាសាដែលបានជ្រើសរើស", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "ផ្ទៃតុ", + "settings.section.server": "ម៉ាស៊ីនមេ", + "settings.tab.general": "ទូទៅ", + "settings.tab.shortcuts": "ផ្លូវកាត់", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "ការរួមបញ្ចូល WSL", + "settings.desktop.wsl.description": "ដំណើរការម៉ាស៊ីនមេ OpenCode នៅខាងក្នុង WSL នៅលើ Windows ។", + "settings.general.section.appearance": "រូបរាង", + "settings.general.section.advanced": "កម្រិតខ្ពស់", + "settings.general.section.notifications": "ការជូនដំណឹងប្រព័ន្ធ", + "settings.general.section.updates": "បច្ចុប្បន្នភាព", + "settings.general.section.sounds": "បែបផែនសំឡេង", + "settings.general.section.feed": "មតិព័ត៌មាន", + "settings.general.section.display": "បង្ហាញ", + "settings.general.row.language.title": "ភាសា", + "settings.general.row.language.description": "ប្តូរភាសាបង្ហាញសម្រាប់ OpenCode", + "settings.general.row.shell.title": "សែលស្ថានីយ", + "settings.general.row.shell.description": "សែលប្រើដោយឧបករណ៍ស្ថានីយ និងភ្នាក់ងារ", + "settings.general.row.shell.autoDefault": "ស្វ័យប្រវត្តិ (លំនាំដើម)\nស្ថានីយ", + "settings.general.row.shell.terminalOnly": "ប៉ុណ្ណោះ។", + "settings.general.row.appearance.title": "រូបរាង", + "settings.general.row.appearance.description": "ប្ដូរតាមបំណងពីរបៀបដែល OpenCode មើលទៅលើឧបករណ៍របស់អ្នក។", + "settings.general.row.colorScheme.title": "ពណ៌ចម្រុះ", + "settings.general.row.colorScheme.description": "ជ្រើសរើសថាតើ OpenCode ធ្វើតាមប្រព័ន្ធ ពន្លឺ ឬរូបរាងងងឹត", + "settings.general.row.theme.title": "ស្បែក", + "settings.general.row.theme.description": "ប្ដូរតាមបំណងពីរបៀបដែល OpenCode មានប្រធានបទ។", + "settings.general.row.font.title": "ពុម្ពអក្សរកូដ", + "settings.general.row.font.description": "ប្ដូរពុម្ពអក្សរដែលប្រើក្នុងប្លុកកូដតាមបំណង", + "settings.general.row.terminalFont.title": "ពុម្ពអក្សរស្ថានីយ", + "settings.general.row.terminalFont.description": "ប្ដូរពុម្ពអក្សរដែលប្រើក្នុងស្ថានីយតាមបំណង", + "settings.general.row.uiFont.title": "ពុម្ពអក្សរ UI", + "settings.general.row.uiFont.description": "ប្ដូរពុម្ពអក្សរតាមបំណងដែលបានប្រើទូទាំងចំណុចប្រទាក់", + "settings.general.row.followup.title": "ឥរិយាបថតាមដាន", + "settings.general.row.followup.description": "ជ្រើសរើសថាតើការតាមដានជំរុញភ្លាមៗ ឬរង់ចាំជាជួរ", + "settings.general.row.followup.option.queue": "ជួរ", + "settings.general.row.followup.option.steer": "ចង្កូត", + "settings.general.row.showFileTree.title": "មែកធាងឯកសារ", + "settings.general.row.showFileTree.description": "បង្ហាញបន្ទះមែកធាងឯកសារនៅក្នុងសម័យ", + "settings.general.row.showNavigation.title": "ការគ្រប់គ្រងការរុករក", + "settings.general.row.showNavigation.description": "បង្ហាញប៊ូតុងថយក្រោយ និងបញ្ជូនបន្តនៅក្នុងរបារចំណងជើងផ្ទៃតុ", + "settings.general.row.showSearch.title": "ក្ដារលាយពាក្យបញ្ជា", + "settings.general.row.showSearch.description": "បង្ហាញប៊ូតុងស្វែងរក និងក្ដារលាយពាក្យបញ្ជានៅក្នុងរបារចំណងជើង", + "settings.general.row.showTerminal.title": "ស្ថានីយ", + "settings.general.row.showTerminal.description": "បង្ហាញប៊ូតុងស្ថានីយនៅក្នុងរបារចំណងជើងផ្ទៃតុ", + "settings.general.row.showStatus.title": "ស្ថានភាពម៉ាស៊ីនមេ", + "settings.general.row.showStatus.description": "បង្ហាញប៊ូតុងស្ថានភាពម៉ាស៊ីនមេនៅក្នុងរបារចំណងជើង", + "settings.general.row.mobileTitlebarBottom.title": "ការរុករកខាងក្រោម", + "settings.general.row.mobileTitlebarBottom.description": "ដាក់របារចំណងជើង និងផ្ទាំងសម័យនៅខាងក្រោមអេក្រង់នៅលើទូរសព្ទ", + "settings.general.row.showCustomAgents.title": "បង្ហាញភ្នាក់ងារ", + "settings.general.row.showCustomAgents.description": + "ប្តូររវាងភ្នាក់ងារនៅក្នុងកម្មវិធីតែង។ នៅពេលលាក់ វាកំណត់លំនាំដើមទៅភ្នាក់ងារបង្កើត។", + "settings.general.row.reasoningSummaries.title": "បង្ហាញការសង្ខេបហេតុផល", + "settings.general.row.reasoningSummaries.description": "បង្ហាញសេចក្តីសង្ខេបហេតុផលរបស់ម៉ូដែលក្នុងបន្ទាត់ពេលវេលា", + "settings.general.row.shellToolPartsExpanded.title": "ពង្រីកផ្នែកឧបករណ៍សែល", + "settings.general.row.shellToolPartsExpanded.description": + "បង្ហាញផ្នែកឧបករណ៍សែលដែលបានពង្រីកតាមលំនាំដើមនៅក្នុងបន្ទាត់ពេលវេលា", + "settings.general.row.editToolPartsExpanded.title": "ពង្រីកផ្នែកឧបករណ៍កែសម្រួល", + "settings.general.row.editToolPartsExpanded.description": + "បង្ហាញការកែសម្រួល សរសេរ និងផ្នែកឧបករណ៍បំណះដែលបានពង្រីកតាមលំនាំដើមនៅក្នុងបន្ទាត់ពេលវេលា", + "settings.general.row.newInterface.title": "ប្លង់ថ្មី។", + "settings.general.row.newInterface.badge": "ថ្មី។", + "settings.general.row.newInterface.description": "ប្រើផ្ទាំងថ្មី និងប្លង់ផ្ទះ។ ប្តូររវាងប្លង់សម្រាប់រយៈពេលកំណត់។", + "settings.general.row.newInterfaceNotice.title": "ឥឡូវនេះ អ្នកកំពុងប្រើប្លង់ថ្មី។", + "settings.general.row.newInterfaceNotice.description": "ប្លង់ពីមុនលែងមានទៀតហើយ", + "settings.general.row.newInterfaceNotice.dismiss": "ច្រានចោល", + "settings.general.row.pinchZoom.title": "ខ្ទាស់ដើម្បីពង្រីក", + "settings.general.row.pinchZoom.description": "អនុញ្ញាត​ឱ្យ​ចុច​បន្ទះ​បន្ទះ​និង​កាយវិការ​បញ្ជា​-​រមូរ​ដើម្បី​ពង្រីក", + "settings.general.row.wayland.title": "ប្រើ Wayland ដើម", + "settings.general.row.wayland.description": "បិទដំណើរការ X11 fallback នៅលើ Wayland។ ទាមទារការចាប់ផ្តើមឡើងវិញ។", + "settings.general.row.wayland.tooltip": + "នៅលើលីនុចដែលមានម៉ូនីទ័រអត្រាធ្វើឱ្យស្រស់ចម្រុះ វេយលែនដើមអាចមានស្ថេរភាពជាងមុន។", + "settings.general.row.releaseNotes.title": "កំណត់ចំណាំចេញផ្សាយ", + "settings.general.row.releaseNotes.description": "បង្ហាញអ្វីដែលថ្មីលេចឡើងបន្ទាប់ពីការអាប់ដេត", + "settings.updates.row.startup.title": "ពិនិត្យមើលការអាប់ដេតនៅពេលចាប់ផ្តើម", + "settings.updates.row.startup.description": "ពិនិត្យមើលការអាប់ដេតដោយស្វ័យប្រវត្តិនៅពេលដែល OpenCode ចាប់ផ្តើម", + "settings.updates.row.check.title": "ពិនិត្យមើលបច្ចុប្បន្នភាព", + "settings.updates.row.check.description": "ពិនិត្យមើលបច្ចុប្បន្នភាពដោយដៃ ហើយដំឡើងប្រសិនបើមាន", + "settings.updates.action.checkNow": "ពិនិត្យឥឡូវនេះ", + "settings.updates.action.checking": "កំពុងពិនិត្យ...", + "settings.updates.action.downloading": "កំពុងទាញយក...", + "settings.updates.action.installing": "កំពុងដំឡើង...", + "settings.updates.toast.latest.title": "អ្នកទាន់សម័យហើយ។", + "settings.updates.toast.latest.description": "អ្នកកំពុងដំណើរការកំណែចុងក្រោយបំផុតនៃ OpenCode ។", + "sound.option.none": "គ្មានទេ។", + "sound.option.alert01": "ដាស់តឿន ០១", + "sound.option.alert02": "ដាស់តឿន ០២", + "sound.option.alert03": "ដាស់តឿន ០៣", + "sound.option.alert04": "ដាស់តឿន ០៤", + "sound.option.alert05": "ដាស់តឿន ០៥", + "sound.option.alert06": "ដាស់តឿន ០៦", + "sound.option.alert07": "ដាស់តឿន ០៧", + "sound.option.alert08": "ដាស់តឿន ០៨", + "sound.option.alert09": "ដាស់តឿន ០៩", + "sound.option.alert10": "ការជូនដំណឹង ១០", + "sound.option.bipbop01": "ប៊ីប-បប ០១", + "sound.option.bipbop02": "បុប្ផាបុប្ផា ០២", + "sound.option.bipbop03": "ប៊ីប-បូប ០៣", + "sound.option.bipbop04": "បុប្ផាព្រៃភ្នំ ០៤", + "sound.option.bipbop05": "ប៊ីប-បូប ០៥", + "sound.option.bipbop06": "ប៊ីប-បប ០៦", + "sound.option.bipbop07": "បុប្ផាបុប្ផា ០៧", + "sound.option.bipbop08": "បុប្ផាបុប្ផា ០៨", + "sound.option.bipbop09": "បុប្ផាផ្កាស្មៅ ០៩", + "sound.option.bipbop10": "ប៊ីបបប ១០", + "sound.option.staplebops01": "Staplebops ០១", + "sound.option.staplebops02": "Staplebops ០២", + "sound.option.staplebops03": "Staplebops ០៣", + "sound.option.staplebops04": "Staplebops ០៤", + "sound.option.staplebops05": "Staplebops ០៥", + "sound.option.staplebops06": "Staplebops ០៦", + "sound.option.staplebops07": "Staplebops ០៧", + "sound.option.nope01": "ទេ ០១", + "sound.option.nope02": "ទេ ០២", + "sound.option.nope03": "ទេ ០៣", + "sound.option.nope04": "ទេ ០៤", + "sound.option.nope05": "ទេ ០៥", + "sound.option.nope06": "គ្មានទេ ០៦", + "sound.option.nope07": "គ្មានទេ ០៧", + "sound.option.nope08": "ទេ ០៨", + "sound.option.nope09": "ទេ ០៩", + "sound.option.nope10": "ទេ ១០", + "sound.option.nope11": "ទេ ១១", + "sound.option.nope12": "ទេ ១២", + "sound.option.yup01": "បាទ ០១", + "sound.option.yup02": "បាទ ០២", + "sound.option.yup03": "បាទ ០៣", + "sound.option.yup04": "បាទ ០៤", + "sound.option.yup05": "បាទ ០៥", + "sound.option.yup06": "បាទ ០៦", + "settings.general.notifications.agent.title": "ភ្នាក់ងារ", + "settings.general.notifications.agent.description": + "បង្ហាញការជូនដំណឹងប្រព័ន្ធ នៅពេលដែលភ្នាក់ងារបញ្ចប់ ឬត្រូវការការយកចិត្តទុកដាក់", + "settings.general.notifications.permissions.title": "ការអនុញ្ញាត", + "settings.general.notifications.permissions.description": "បង្ហាញការជូនដំណឹងប្រព័ន្ធនៅពេលដែលត្រូវការការអនុញ្ញាត", + "settings.general.notifications.errors.title": "កំហុស", + "settings.general.notifications.errors.description": "បង្ហាញការជូនដំណឹងប្រព័ន្ធនៅពេលដែលមានកំហុសកើតឡើង", + "settings.general.sounds.agent.title": "ភ្នាក់ងារ", + "settings.general.sounds.agent.description": "ចាក់សំឡេងនៅពេលដែលភ្នាក់ងារបញ្ចប់ ឬត្រូវការការយកចិត្តទុកដាក់", + "settings.general.sounds.permissions.title": "ការអនុញ្ញាត", + "settings.general.sounds.permissions.description": "ចាក់សំឡេងនៅពេលដែលត្រូវការការអនុញ្ញាត", + "settings.general.sounds.errors.title": "កំហុស", + "settings.general.sounds.errors.description": "ចាក់សំឡេងនៅពេលមានកំហុសកើតឡើង", + "settings.shortcuts.title": "ផ្លូវកាត់ក្តារចុច", + "settings.shortcuts.reset.button": "កំណត់ឡើងវិញទៅលំនាំដើម", + "settings.shortcuts.reset.toast.title": "កំណត់ផ្លូវកាត់ឡើងវិញ", + "settings.shortcuts.reset.toast.description": "ផ្លូវកាត់ក្តារចុចត្រូវបានកំណត់ឡើងវិញទៅលំនាំដើម។", + "settings.shortcuts.conflict.title": "ផ្លូវកាត់ត្រូវបានប្រើប្រាស់រួចហើយ", + "settings.shortcuts.conflict.description": "{{keybind}} ត្រូវបានចាត់ឱ្យទៅ {{titles}} រួចហើយ។", + "settings.shortcuts.unassigned": "មិនបានកំណត់", + "settings.shortcuts.pressKeys": "ចុចគ្រាប់ចុច", + "settings.shortcuts.search.placeholder": "ផ្លូវកាត់ស្វែងរក", + "settings.shortcuts.search.empty": "រកមិនឃើញផ្លូវកាត់ទេ។", + "settings.shortcuts.group.general": "ទូទៅ", + "settings.shortcuts.group.session": "សម័យ", + "settings.shortcuts.group.navigation": "ការរុករក", + "settings.shortcuts.group.modelAndAgent": "ម៉ូដែល និងភ្នាក់ងារ", + "settings.shortcuts.group.terminal": "ស្ថានីយ", + "settings.shortcuts.group.prompt": "ប្រអប់បញ្ចូល", + "settings.providers.title": "អ្នកផ្តល់សេវា", + "settings.providers.description": "ការកំណត់អ្នកផ្តល់សេវានឹងអាចកំណត់បាននៅទីនេះ។", + "settings.providers.section.connected": "អ្នកផ្តល់សេវាដែលបានភ្ជាប់", + "settings.providers.connected.empty": "គ្មានអ្នកផ្តល់សេវាដែលបានតភ្ជាប់ទេ។", + "settings.providers.connected.environmentDescription": "បានភ្ជាប់ពីអថេរបរិស្ថានរបស់អ្នក។", + "settings.providers.section.popular": "អ្នកផ្តល់សេវាពេញនិយម", + "settings.providers.custom.description": "បន្ថែមអ្នកផ្តល់សេវាដែលត្រូវគ្នាជាមួយ OpenAI ដោយ URL មូលដ្ឋាន។", + "settings.providers.tag.environment": "បរិស្ថាន", + "settings.providers.tag.config": "កំណត់រចនាសម្ព័ន្ធ", + "settings.providers.tag.custom": "ផ្ទាល់ខ្លួន", + "settings.providers.tag.other": "ផ្សេងទៀត។", + "settings.models.title": "ម៉ូដែល", + "settings.models.description": "ការកំណត់ម៉ូដែលនឹងអាចកំណត់បាននៅទីនេះ។", + "settings.agents.title": "ភ្នាក់ងារ", + "settings.agents.description": "ការកំណត់ភ្នាក់ងារនឹងអាចកំណត់បាននៅទីនេះ។", + "settings.commands.title": "ពាក្យបញ្ជា", + "settings.commands.description": "ការកំណត់ពាក្យបញ្ជានឹងអាចកំណត់បាននៅទីនេះ។", + "settings.mcp.title": "MCP", + "settings.mcp.description": "ការកំណត់ MCP នឹងអាចកំណត់បាននៅទីនេះ។", + "settings.permissions.title": "ការអនុញ្ញាត", + "settings.permissions.description": "គ្រប់គ្រងឧបករណ៍អ្វីដែលម៉ាស៊ីនមេអាចប្រើតាមលំនាំដើម។", + "settings.permissions.section.tools": "ឧបករណ៍", + "settings.permissions.toast.updateFailed.title": "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពការអនុញ្ញាត", + "settings.permissions.action.allow": "អនុញ្ញាត", + "settings.permissions.action.ask": "សួរ", + "settings.permissions.action.deny": "បដិសេធ", + "settings.permissions.tool.read.title": "អាន", + "settings.permissions.tool.read.description": "ការអានឯកសារ (ត្រូវនឹងផ្លូវឯកសារ)", + "settings.permissions.tool.edit.title": "កែសម្រួល", + "settings.permissions.tool.edit.description": "កែប្រែឯកសារ រួមទាំងការកែសម្រួល សរសេរ និងបំណះ", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "ផ្គូផ្គងឯកសារដោយប្រើលំនាំ glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "ស្វែងរកមាតិកាឯកសារដោយប្រើកន្សោមធម្មតា។", + "settings.permissions.tool.list.title": "បញ្ជី", + "settings.permissions.tool.list.description": "រាយបញ្ជីឯកសារនៅក្នុងថតមួយ។", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "ដំណើរការពាក្យបញ្ជាសែល", + "settings.permissions.tool.task.title": "កិច្ចការ", + "settings.permissions.tool.task.description": "បើកដំណើរការភ្នាក់ងាររង", + "settings.permissions.tool.skill.title": "ជំនាញ", + "settings.permissions.tool.skill.description": "ផ្ទុកជំនាញតាមឈ្មោះ", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "ដំណើរការសំណួរម៉ាស៊ីនមេភាសា", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "ធ្វើបច្ចុប្បន្នភាពបញ្ជីការងារត្រូវធ្វើ", + "settings.permissions.tool.webfetch.title": "ការទាញយកគេហទំព័រ", + "settings.permissions.tool.webfetch.description": "ទាញយកមាតិកាពី URL", + "settings.permissions.tool.websearch.title": "ការស្វែងរកតាមអ៊ីនធឺណិត", + "settings.permissions.tool.websearch.description": "ស្វែងរកគេហទំព័រ", + "settings.permissions.tool.external_directory.title": "ថតខាងក្រៅ", + "settings.permissions.tool.external_directory.description": "ចូលប្រើឯកសារនៅខាងក្រៅថតគម្រោង", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "រកឃើញការហៅឧបករណ៍ម្តងហើយម្តងទៀតជាមួយនឹងការបញ្ចូលដូចគ្នា។", + "session.delete.failed.title": "បរាជ័យក្នុងការលុបសម័យ", + "session.delete.title": "លុបសម័យ", + "session.delete.confirm": 'លុបសម័យ "{{name}}"?', + "session.delete.button": "លុបសម័យ", + "workspace.new": "កន្លែងធ្វើការថ្មី។", + "workspace.type.local": "ក្នុងស្រុក", + "workspace.type.sandbox": "ប្រអប់ខ្សាច់", + "workspace.create.failed.title": "បរាជ័យក្នុងការបង្កើតកន្លែងធ្វើការ", + "workspace.delete.failed.title": "បរាជ័យក្នុងការលុបកន្លែងធ្វើការ", + "workspace.resetting.title": "កំណត់កន្លែងធ្វើការឡើងវិញ", + "workspace.resetting.description": "វាអាចចំណាយពេលមួយនាទី។", + "workspace.reset.failed.title": "បានបរាជ័យក្នុងការកំណត់កន្លែងធ្វើការឡើងវិញ", + "workspace.reset.success.title": "កំណត់កន្លែងធ្វើការឡើងវិញ", + "workspace.reset.success.description": "កន្លែងធ្វើការឥឡូវនេះត្រូវគ្នានឹងសាខាលំនាំដើម។", + "workspace.error.stillPreparing": "កន្លែងធ្វើការកំពុងរៀបចំនៅឡើយ", + "workspace.status.checking": "កំពុងពិនិត្យមើលការផ្លាស់ប្តូរដែលមិនរួមបញ្ចូលគ្នា...", + "workspace.status.error": "មិនអាចផ្ទៀងផ្ទាត់ស្ថានភាព git បានទេ។", + "workspace.status.clean": "រកមិនឃើញការផ្លាស់ប្តូរដែលមិនរួមបញ្ចូលគ្នាទេ។", + "workspace.status.dirty": "បានរកឃើញការផ្លាស់ប្តូរដែលមិនរួមបញ្ចូលគ្នានៅក្នុងកន្លែងធ្វើការនេះ។", + "workspace.delete.title": "លុបកន្លែងធ្វើការ", + "workspace.delete.confirm": 'លុបកន្លែងធ្វើការ "{{name}}"?', + "workspace.delete.button": "លុបកន្លែងធ្វើការ", + "workspace.reset.title": "កំណត់កន្លែងធ្វើការឡើងវិញ", + "workspace.reset.confirm": 'កំណត់ទំហំការងារ "{{name}}" ឡើងវិញ?', + "workspace.reset.button": "កំណត់ទំហំការងារឡើងវិញ", + "workspace.reset.archived.none": "គ្មានសម័យសកម្មនឹងត្រូវបានទុកក្នុងប័ណ្ណសារទេ។", + "workspace.reset.archived.one": "សម័យ 1 នឹងត្រូវបានទុកក្នុងប័ណ្ណសារ។", + "workspace.reset.archived.many": "សម័យ {{count}} នឹងត្រូវបានទុកក្នុងប័ណ្ណសារ។", + "workspace.reset.note": "វានឹងកំណត់កន្លែងធ្វើការឡើងវិញដើម្បីផ្គូផ្គងសាខាលំនាំដើម។", + "dialog.usageExceeded.dontShowAgain": "កុំបង្ហាញម្តងទៀត", +} diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts new file mode 100644 index 0000000000000000000000000000000000000000..b96fb4c59471a14ace8cda9d22d9f2f7a57c5270 --- /dev/null +++ b/packages/app/src/i18n/ko.ts @@ -0,0 +1,1167 @@ +export const dict = { + "command.category.suggested": "추천", + "command.category.view": "보기", + "command.category.project": "프로젝트", + "command.category.provider": "공급자", + "command.category.server": "서버", + "command.category.session": "세션", + "command.category.theme": "테마", + "command.category.language": "언어", + "command.category.file": "파일", + "command.category.context": "컨텍스트", + "command.category.terminal": "터미널", + "command.category.model": "모델", + "command.category.mcp": "MCP", + "command.category.agent": "에이전트", + "command.category.permissions": "권한", + "command.category.workspace": "작업 공간", + "command.category.settings": "설정", + "theme.scheme.system": "시스템", + "theme.scheme.light": "라이트", + "theme.scheme.dark": "다크", + "command.sidebar.toggle": "사이드바 표시/숨기기", + "command.project.open": "프로젝트 열기", + "command.provider.connect": "공급자 연결", + "command.server.switch": "서버 전환", + "command.settings.open": "설정 열기", + "command.session.previous": "이전 세션", + "command.session.next": "다음 세션", + "command.session.previous.unseen": "이전 읽지 않은 세션", + "command.session.next.unseen": "다음 읽지 않은 세션", + "command.session.archive": "세션 보관", + "command.palette": "명령 팔레트", + "command.theme.cycle": "테마 순환", + "command.theme.set": "테마 사용: {{theme}}", + "command.theme.scheme.cycle": "색상 테마 순환", + "command.theme.scheme.set": "색상 테마 사용: {{scheme}}", + "command.language.cycle": "언어 순환", + "command.language.set": "언어 사용: {{language}}", + "command.session.new": "새 세션", + "command.file.open": "파일 열기", + "command.tab.close": "탭 닫기", + "command.context.addSelection": "선택 영역을 컨텍스트에 추가", + "command.context.addSelection.description": "현재 파일에서 선택한 줄을 추가", + "command.input.focus": "입력창 포커스", + "command.terminal.toggle": "터미널 표시/숨기기", + "command.fileTree.toggle": "파일 트리 표시/숨기기", + "command.review.toggle": "검토 표시/숨기기", + "command.terminal.new": "새 터미널", + "command.terminal.new.description": "새 터미널 탭 생성", + "command.steps.toggle": "단계 표시/숨기기", + "command.steps.toggle.description": "현재 메시지의 단계 표시/숨기기", + "command.message.previous": "이전 메시지", + "command.message.previous.description": "이전 사용자 메시지로 이동", + "command.message.next": "다음 메시지", + "command.message.next.description": "다음 사용자 메시지로 이동", + "command.model.choose": "모델 선택", + "command.model.choose.description": "다른 모델 선택", + "command.mcp.toggle": "MCP 전환", + "command.mcp.toggle.description": "MCP 전환", + "command.agent.cycle": "에이전트 순환", + "command.agent.cycle.description": "다음 에이전트로 전환", + "command.agent.cycle.reverse": "에이전트 역순환", + "command.agent.cycle.reverse.description": "이전 에이전트로 전환", + "command.model.variant.cycle": "생각 수준 순환", + "command.model.variant.cycle.description": "다음 생각 수준으로 전환", + "command.prompt.mode.shell": "셸", + "command.prompt.mode.normal": "프롬프트", + "command.permissions.autoaccept.enable": "권한 자동 수락", + "command.permissions.autoaccept.disable": "권한 자동 수락 중지", + "command.workspace.toggle": "작업 공간 전환", + "command.workspace.toggle.description": "사이드바에서 다중 작업 공간 활성화 또는 비활성화", + "command.session.undo": "실행 취소", + "command.session.undo.description": "마지막 메시지 실행 취소", + "command.session.redo": "다시 실행", + "command.session.redo.description": "마지막 실행 취소된 메시지 다시 실행", + "command.session.compact": "세션 압축", + "command.session.compact.description": "컨텍스트 크기를 줄이기 위해 세션 요약", + "command.session.fork": "메시지에서 분기", + "command.session.fork.description": "이전 메시지에서 새 세션 생성", + "command.session.share": "세션 공유", + "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": "결과 없음", + "palette.group.commands": "명령어", + "palette.group.files": "파일", + "dialog.provider.search.placeholder": "공급자 검색", + "dialog.provider.empty": "공급자 없음", + "dialog.provider.group.popular": "인기", + "dialog.provider.group.other": "기타", + "dialog.provider.custom.label": "OpenAI 호환 사용자 지정 공급자", + "dialog.provider.tag.recommended": "추천", + "dialog.provider.opencode.note": "Claude, GPT, Gemini 등을 포함한 엄선된 모델", + "dialog.provider.opencode.tagline": "신뢰할 수 있는 최적화 모델", + "dialog.provider.opencodeGo.tagline": "모두를 위한 저렴한 구독", + "dialog.provider.anthropic.note": "Pro 및 Max를 포함한 Claude 모델에 직접 액세스", + "dialog.provider.copilot.note": "GitHub Copilot을 통한 코딩 지원용 AI 모델", + "dialog.provider.openai.note": "빠르고 강력한 범용 AI 작업을 위한 GPT 모델", + "dialog.provider.google.note": "빠르고 구조화된 응답을 위한 Gemini 모델", + "dialog.provider.openrouter.note": "모든 지원 모델을 단일 공급자에서 액세스", + "dialog.provider.vercel.note": "스마트 라우팅을 통한 AI 모델 통합 액세스", + "dialog.model.select.title": "모델 선택", + "dialog.model.search.placeholder": "모델 검색", + "dialog.model.empty": "모델 결과 없음", + "dialog.model.manage": "모델 관리", + "dialog.model.manage.description": "모델 선택기에 표시할 모델 사용자 지정", + "dialog.model.manage.provider.toggle": "모든 {{provider}} 모델 토글", + "dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델", + "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가", + "dialog.model.unpaid.viewMoreProviders": "70개 이상의 공급자 더 보기", + "dialog.provider.viewAll": "더 많은 공급자 보기", + "provider.connect.title": "{{provider}} 연결", + "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인", + "provider.connect.selectMethod": "{{provider}} 로그인 방법 선택", + "provider.connect.method.apiKey": "API 키", + "provider.connect.method.browser": "브라우저", + "provider.connect.method.headless": "헤드리스 모드", + "provider.connect.status.inProgress": "인증 진행 중...", + "provider.connect.status.waiting": "인증 대기 중...", + "provider.connect.status.failed": "인증 실패: {{error}}", + "provider.connect.apiKey.description": + "{{provider}} API 키를 입력하여 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", + "provider.connect.apiKey.label": "{{provider}} API 키", + "provider.connect.apiKey.placeholder": "API 키", + "provider.connect.apiKey.required": "API 키가 필요합니다", + "provider.connect.opencodeZen.line1": + "OpenCode Zen은 코딩 에이전트를 위해 최적화된 신뢰할 수 있는 엄선된 모델에 대한 액세스를 제공합니다.", + "provider.connect.opencodeZen.line2": "단일 API 키로 Claude, GPT, Gemini, GLM 등 다양한 모델에 액세스할 수 있습니다.", + "provider.connect.opencodeZen.visit.prefix": "다음 ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "을 방문하여 API 키를 받으세요.", + "provider.connect.oauth.code.visit.prefix": "다음 ", + "provider.connect.oauth.code.visit.link": "이 링크", + "provider.connect.oauth.code.visit.suffix": + "를 방문하여 인증 코드를 받아 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", + "provider.connect.oauth.code.label": "{{method}} 인증 코드", + "provider.connect.oauth.code.placeholder": "인증 코드", + "provider.connect.oauth.code.required": "인증 코드가 필요합니다", + "provider.connect.oauth.code.invalid": "유효하지 않은 인증 코드", + "provider.connect.oauth.auto.visit.prefix": "다음 ", + "provider.connect.oauth.auto.visit.link": "이 링크", + "provider.connect.oauth.auto.visit.suffix": + "를 방문하고 아래 코드를 입력하여 계정을 연결하고 OpenCode에서 {{provider}} 모델을 사용하세요.", + "provider.connect.oauth.auto.confirmationCode": "확인 코드", + "provider.connect.toast.connected.title": "{{provider}} 연결됨", + "provider.connect.toast.connected.description": "이제 {{provider}} 모델을 사용할 수 있습니다.", + "provider.custom.title": "사용자 지정 공급자", + "provider.custom.unavailable": "이 서버에서는 사용자 지정 공급자를 사용할 수 없습니다", + "provider.custom.description.prefix": "OpenAI 호환 공급자를 구성합니다. ", + "provider.custom.description.link": "공급자 구성 문서", + "provider.custom.description.suffix": "를 참조하세요.", + "provider.custom.field.providerID.label": "공급자 ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "소문자, 숫자, 하이픈 또는 밑줄", + "provider.custom.field.name.label": "표시 이름", + "provider.custom.field.name.placeholder": "내 AI 공급자", + "provider.custom.field.baseURL.label": "기본 URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API 키", + "provider.custom.field.apiKey.placeholder": "API 키", + "provider.custom.field.apiKey.description": "선택 사항입니다. 헤더를 통해 인증을 관리하는 경우 비워 두세요.", + "provider.custom.models.label": "모델", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "이름", + "provider.custom.models.name.placeholder": "표시 이름", + "provider.custom.models.remove": "모델 제거", + "provider.custom.models.add": "모델 추가", + "provider.custom.headers.label": "헤더 (선택 사항)", + "provider.custom.headers.key.label": "헤더", + "provider.custom.headers.key.placeholder": "헤더 이름", + "provider.custom.headers.value.label": "값", + "provider.custom.headers.value.placeholder": "값", + "provider.custom.headers.remove": "헤더 제거", + "provider.custom.headers.add": "헤더 추가", + "provider.custom.error.providerID.required": "공급자 ID가 필요합니다", + "provider.custom.error.providerID.format": "소문자, 숫자, 하이픈 또는 밑줄을 사용하세요", + "provider.custom.error.providerID.exists": "해당 공급자 ID가 이미 존재합니다", + "provider.custom.error.name.required": "표시 이름이 필요합니다", + "provider.custom.error.baseURL.required": "기본 URL이 필요합니다", + "provider.custom.error.baseURL.format": "http:// 또는 https://로 시작해야 합니다", + "provider.custom.error.required": "필수", + "provider.custom.error.duplicate": "중복", + "provider.disconnect.toast.disconnected.title": "{{provider}} 연결 해제됨", + "provider.disconnect.toast.disconnected.description": "{{provider}} 모델을 더 이상 사용할 수 없습니다.", + "model.tag.free": "무료", + "model.tag.latest": "최신", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "텍스트", + "model.input.image": "이미지", + "model.input.audio": "오디오", + "model.input.video": "비디오", + "model.input.pdf": "PDF", + "model.tooltip.allows": "지원: {{inputs}}", + "model.tooltip.reasoning.allowed": "추론 허용", + "model.tooltip.reasoning.none": "추론 없음", + "model.tooltip.context": "컨텍스트 제한 {{limit}}", + "common.search.placeholder": "검색", + "common.goBack": "뒤로 가기", + "common.goForward": "앞으로 가기", + "common.loading": "로딩 중", + "common.loading.ellipsis": "...", + "common.cancel": "취소", + "common.connect": "연결", + "common.disconnect": "연결 해제", + "common.continue": "계속", + "common.submit": "제출", + "common.save": "저장", + "common.saving": "저장 중...", + "common.default": "기본값", + "common.attachment": "첨부 파일", + "prompt.placeholder.shell": "셸 명령어 입력... {{example}}", + "prompt.placeholder.normal": '무엇이든 물어보세요... "{{example}}"', + "prompt.placeholder.simple": "무엇이든 물어보세요...", + "prompt.placeholder.summarizeComments": "댓글 요약…", + "prompt.placeholder.summarizeComment": "댓글 요약…", + "prompt.mode.shell": "셸", + "prompt.mode.normal": "프롬프트", + "prompt.mode.shell.exit": "종료하려면 esc", + "prompt.example.1": "코드베이스의 TODO 수정", + "prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?", + "prompt.example.3": "고장 난 테스트 수정", + "prompt.example.4": "인증 작동 방식 설명", + "prompt.example.5": "보안 취약점 찾기 및 수정", + "prompt.example.6": "사용자 서비스에 단위 테스트 추가", + "prompt.example.7": "이 함수를 더 읽기 쉽게 리팩터링", + "prompt.example.8": "이 오류는 무엇을 의미하나요?", + "prompt.example.9": "이 문제 디버깅 도와줘", + "prompt.example.10": "API 문서 생성", + "prompt.example.11": "데이터베이스 쿼리 최적화", + "prompt.example.12": "입력 유효성 검사 추가", + "prompt.example.13": "...를 위한 새 컴포넌트 생성", + "prompt.example.14": "이 프로젝트를 어떻게 배포하나요?", + "prompt.example.15": "모범 사례를 기준으로 내 코드 검토", + "prompt.example.16": "이 함수에 오류 처리 추가", + "prompt.example.17": "이 정규식 패턴 설명", + "prompt.example.18": "이것을 TypeScript로 변환", + "prompt.example.19": "코드베이스 전체에 로깅 추가", + "prompt.example.20": "오래된 종속성은 무엇인가요?", + "prompt.example.21": "마이그레이션 스크립트 작성 도와줘", + "prompt.example.22": "이 엔드포인트에 캐싱 구현", + "prompt.example.23": "이 목록에 페이지네이션 추가", + "prompt.example.24": "...를 위한 CLI 명령어 생성", + "prompt.example.25": "여기서 환경 변수는 어떻게 작동하나요?", + "prompt.popover.emptyResults": "일치하는 결과 없음", + "prompt.popover.emptyCommands": "일치하는 명령어 없음", + "prompt.dropzone.label": "이미지, PDF 또는 텍스트 파일을 이곳에 드롭하세요", + "prompt.dropzone.file.label": "드롭하여 파일 @멘션 추가", + "prompt.slash.badge.custom": "사용자 지정", + "prompt.slash.badge.skill": "스킬", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "활성", + "prompt.context.includeActiveFile": "활성 파일 포함", + "prompt.context.removeActiveFile": "컨텍스트에서 활성 파일 제거", + "prompt.context.removeFile": "컨텍스트에서 파일 제거", + "prompt.action.attachFile": "파일 추가", + "prompt.menu.addImagesAndFiles": "파일 및 기타 항목 추가", + "prompt.menu.imagesAndFiles": "이미지 및 파일", + "prompt.menu.commands": "명령어", + "prompt.menu.context": "컨텍스트", + "prompt.menu.shellCommand": "셸 명령", + "prompt.attachment.remove": "첨부 파일 제거", + "prompt.action.send": "전송", + "prompt.action.stop": "중지", + "prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일", + "prompt.toast.attachmentDuplicate.title": "이 파일은 이미 업로드되었습니다", + "prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.", + "prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택", + "prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.", + "prompt.toast.worktreeCreateFailed.title": "작업 트리 생성 실패", + "prompt.toast.sessionCreateFailed.title": "세션 생성 실패", + "prompt.toast.shellSendFailed.title": "셸 명령 전송 실패", + "prompt.toast.commandSendFailed.title": "명령 전송 실패", + "prompt.toast.promptSendFailed.title": "프롬프트 전송 실패", + "prompt.toast.promptSendFailed.description": "세션을 가져올 수 없습니다", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{total}}개 중 {{enabled}}개 활성화됨", + "dialog.mcp.empty": "구성된 MCP 없음", + "dialog.lsp.empty": "파일 유형에서 자동 감지된 LSP", + "dialog.plugins.empty": "opencode.json에 구성된 플러그인", + "mcp.status.connected": "연결됨", + "mcp.status.failed": "실패", + "mcp.status.needs_auth": "인증 필요", + "mcp.auth.clickToAuthenticate": "클릭하여 인증", + "mcp.status.disabled": "비활성화됨", + "dialog.fork.empty": "분기할 메시지 없음", + "dialog.directory.search.placeholder": "폴더 검색", + "dialog.directory.empty": "폴더 없음", + "dialog.directory.action.selectFile": "파일 선택", + "dialog.directory.action.selectFolder": "폴더 선택", + "dialog.directory.root": "루트", + "dialog.directory.parent": "상위 폴더", + "dialog.directory.readError": "이 폴더를 읽을 수 없습니다", + "dialog.server.title": "서버", + "dialog.server.description": "이 앱이 연결할 OpenCode 서버를 전환합니다.", + "dialog.server.search.placeholder": "서버 검색", + "dialog.server.empty": "서버 없음", + "dialog.server.add.title": "서버 추가", + "dialog.server.add.url": "서버 URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "서버에 연결할 수 없습니다", + "dialog.server.add.checking": "확인 중...", + "dialog.server.add.button": "서버 추가", + "dialog.server.add.name": "서버 이름 (선택 사항)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "사용자 이름 (선택 사항)", + "dialog.server.add.password": "비밀번호 (선택 사항)", + "dialog.server.edit.title": "서버 편집", + "dialog.server.default.title": "기본 서버", + "dialog.server.default.description": + "로컬 서버를 시작하는 대신 앱 실행 시 이 서버에 연결합니다. 다시 시작해야 합니다.", + "dialog.server.default.none": "선택된 서버 없음", + "dialog.server.default.set": "현재 서버를 기본값으로 설정", + "dialog.server.default.clear": "지우기", + "dialog.server.action.remove": "서버 제거", + "dialog.server.menu.edit": "편집", + "dialog.server.menu.default": "기본값으로 설정", + "dialog.server.menu.defaultRemove": "기본값 제거", + "dialog.server.menu.delete": "삭제", + "dialog.server.current": "현재 서버", + "dialog.server.status.default": "기본값", + "dialog.project.edit.title": "프로젝트 편집", + "dialog.project.edit.name": "이름", + "dialog.project.edit.icon": "아이콘", + "dialog.project.edit.icon.alt": "프로젝트 아이콘", + "dialog.project.edit.icon.hint": "이미지를 클릭하거나 드래그하세요", + "dialog.project.edit.icon.recommended": "권장: 128x128px", + "dialog.project.edit.color": "색상", + "dialog.project.edit.color.select": "{{color}} 색상 선택", + "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": "시스템", + "context.breakdown.user": "사용자", + "context.breakdown.assistant": "어시스턴트", + "context.breakdown.tool": "도구 호출", + "context.breakdown.other": "기타", + "context.systemPrompt.title": "시스템 프롬프트", + "context.rawMessages.title": "원시 메시지", + "context.export.session": "세션 내보내기", + + "context.stats.session": "세션", + "context.stats.messages": "메시지", + "context.stats.provider": "공급자", + "context.stats.model": "모델", + "context.stats.limit": "컨텍스트 제한", + "context.stats.totalTokens": "총 토큰", + "context.stats.usage": "사용량", + "context.stats.inputTokens": "입력 토큰", + "context.stats.outputTokens": "출력 토큰", + "context.stats.reasoningTokens": "추론 토큰", + "context.stats.cacheTokens": "캐시 토큰 (읽기/쓰기)", + "context.stats.userMessages": "사용자 메시지", + "context.stats.assistantMessages": "어시스턴트 메시지", + "context.stats.totalCost": "총 비용", + "context.stats.sessionCreated": "세션 생성됨", + "context.stats.lastActivity": "최근 활동", + "context.usage.tokens": "토큰", + "context.usage.usage": "사용량", + "context.usage.cost": "비용", + "context.usage.clickToView": "컨텍스트를 보려면 클릭", + "context.usage.view": "컨텍스트 사용량 보기", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "언어", + "toast.language.description": "{{language}}(으)로 전환됨", + "toast.theme.title": "테마 전환됨", + "toast.scheme.title": "색상 테마", + "toast.workspace.enabled.title": "작업 공간 활성화됨", + "toast.workspace.enabled.description": "이제 사이드바에 여러 작업 트리가 표시됩니다", + "toast.workspace.disabled.title": "작업 공간 비활성화됨", + "toast.workspace.disabled.description": "사이드바에 메인 작업 트리만 표시됩니다", + "toast.permissions.autoaccept.on.title": "권한 자동 수락 중", + "toast.permissions.autoaccept.on.description": "권한 요청이 자동으로 승인됩니다", + "toast.permissions.autoaccept.off.title": "권한 자동 수락 중지됨", + "toast.permissions.autoaccept.off.description": "권한 요청에 승인이 필요합니다", + "toast.model.none.title": "선택된 모델 없음", + "toast.model.none.description": "이 세션을 요약하려면 공급자를 연결하세요", + "toast.file.loadFailed.title": "파일 로드 실패", + "toast.file.listFailed.title": "파일 목록을 불러오지 못했습니다", + "toast.context.noLineSelection.title": "줄 선택 없음", + "toast.context.noLineSelection.description": "먼저 파일 탭에서 줄 범위를 선택하세요.", + "toast.session.share.copyFailed.title": "URL 클립보드 복사 실패", + "toast.session.share.success.title": "세션 공유됨", + "toast.session.share.success.description": "공유 URL이 클립보드에 복사되었습니다!", + "toast.session.share.failed.title": "세션 공유 실패", + "toast.session.share.failed.description": "세션을 공유하는 동안 오류가 발생했습니다", + "toast.session.unshare.success.title": "세션 공유 해제됨", + "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}})을 설치할 수 있습니다.", + "toast.update.action.installRestart": "설치 및 다시 시작", + "toast.update.action.notYet": "나중에", + "error.page.title": "문제가 발생했습니다", + "error.page.description": "애플리케이션을 로드하는 동안 오류가 발생했습니다.", + "error.page.details.label": "오류 세부 정보", + "error.page.action.restart": "다시 시작", + "error.page.action.report": "오류 신고", + "error.page.action.reported": "오류가 신고됨", + "error.page.action.checking": "확인 중...", + "error.page.action.checkUpdates": "업데이트 확인", + "error.page.action.updateTo": "{{version}} 버전으로 업데이트", + "error.page.report.prefix": "이 오류를 OpenCode 팀에 제보해 주세요: ", + "error.page.report.discord": "Discord", + "error.page.version": "버전: {{version}}", + "error.dev.rootNotFound": + "루트 요소를 찾을 수 없습니다. index.html에 추가하는 것을 잊으셨나요? 또는 id 속성의 철자가 틀렸을 수 있습니다.", + "error.serverSync.connectFailed": "서버에 연결할 수 없습니다. `{{url}}`에서 서버가 실행 중인가요?", + "directory.error.invalidUrl": "URL에 유효하지 않은 디렉터리가 있습니다.", + "error.chain.unknown": "알 수 없는 오류", + "error.chain.causedBy": "원인:", + "error.chain.apiError": "API 오류", + "error.chain.status": "상태: {{status}}", + "error.chain.retryable": "재시도 가능: {{retryable}}", + "error.chain.responseBody": "응답 본문:\n{{body}}", + "error.chain.didYouMean": "혹시 {{suggestions}}을(를) 의미하셨나요?", + "error.chain.modelNotFound": "모델을 찾을 수 없음: {{provider}}/{{model}}", + "error.chain.checkConfig": "구성(opencode.json)의 공급자/모델 이름을 확인하세요", + "error.chain.mcpFailed": 'MCP 서버 "{{name}}" 실패. 참고: OpenCode는 아직 MCP 인증을 지원하지 않습니다.', + "error.chain.providerAuthFailed": "공급자 인증 실패 ({{provider}}): {{message}}", + "error.chain.providerInitFailed": '공급자 "{{provider}}" 초기화 실패. 자격 증명과 구성을 확인하세요.', + "error.chain.configJsonInvalid": "{{path}}의 구성 파일이 유효한 JSON(C)가 아닙니다", + "error.chain.configJsonInvalidWithMessage": "{{path}}의 구성 파일이 유효한 JSON(C)가 아닙니다: {{message}}", + "error.chain.configDirectoryTypo": + '{{path}}의 "{{dir}}" 디렉터리가 유효하지 않습니다. 디렉터리 이름을 "{{suggestion}}"으로 변경하거나 제거하세요. 이는 흔한 오타입니다.', + "error.chain.configFrontmatterError": "{{path}}의 frontmatter 파싱 실패:\n{{message}}", + "error.chain.configInvalid": "{{path}}의 구성 파일이 유효하지 않습니다", + "error.chain.configInvalidWithMessage": "{{path}}의 구성 파일이 유효하지 않습니다: {{message}}", + "notification.permission.title": "권한 필요", + "notification.permission.description": "{{projectName}}의 {{sessionTitle}}에서 권한이 필요합니다", + "notification.question.title": "질문", + "notification.question.description": "{{projectName}}의 {{sessionTitle}}에서 질문이 있습니다", + "notification.action.goToSession": "세션으로 이동", + "notification.session.responseReady.title": "응답 준비됨", + "notification.session.error.title": "세션 오류", + "notification.session.error.fallbackDescription": "오류가 발생했습니다", + "home.recentProjects": "최근 프로젝트", + "home.empty.title": "최근 프로젝트 없음", + "home.empty.description": "로컬 프로젝트를 열어 시작하세요", + "session.tab.session": "세션", + "session.tab.review": "검토", + "session.tab.context": "컨텍스트", + "session.panel.reviewAndFiles": "검토 및 파일", + "session.review.filesChanged": "{{count}}개 파일 변경됨", + "session.review.change.one": "변경", + "session.review.change.other": "변경", + "session.review.loadingChanges": "변경 사항 로드 중...", + "session.review.empty": "이 세션에 변경 사항이 아직 없습니다", + "session.review.noVcs": "Git 버전 관리 시스템이 감지되지 않아 변경 사항이 표시되지 않습니다", + "session.review.noSnapshot": "구성에서 스냅샷 추적이 비활성화되어 있어 세션 변경 사항을 사용할 수 없습니다", + "session.review.noChanges": "변경 없음", + "session.files.selectToOpen": "열 파일을 선택하세요", + "session.files.all": "모든 파일", + "session.files.empty": "파일 없음", + "session.files.binaryContent": "바이너리 파일 (내용을 표시할 수 없음)", + "session.messages.renderEarlier": "이전 메시지 렌더링", + "session.messages.loadingEarlier": "이전 메시지 로드 중...", + "session.messages.loadEarlier": "이전 메시지 로드", + "session.messages.loading": "메시지 로드 중...", + "session.messages.jumpToLatest": "최신으로 이동", + "session.context.addToContext": "컨텍스트에 {{selection}} 추가", + "session.todo.title": "할 일", + "session.todo.collapse": "접기", + "session.todo.expand": "펼치기", + "session.followupDock.summary.one": "{{count}}개의 대기 중인 메시지", + "session.followupDock.summary.other": "{{count}}개의 대기 중인 메시지", + "session.followupDock.sendNow": "지금 전송", + "session.followupDock.edit": "편집", + "session.followupDock.collapse": "대기 중인 메시지 접기", + "session.followupDock.expand": "대기 중인 메시지 펼치기", + "session.revertDock.summary.one": "{{count}}개의 롤백된 메시지", + "session.revertDock.summary.other": "{{count}}개의 롤백된 메시지", + "session.revertDock.collapse": "롤백된 메시지 접기", + "session.revertDock.expand": "롤백된 메시지 펼치기", + "session.revertDock.restore": "메시지 복원", + "session.new.title": "무엇이든 만들기", + "session.new.worktree.main": "메인 브랜치", + "session.new.worktree.mainWithBranch": "메인 브랜치 ({{branch}})", + "session.new.worktree.create": "새 작업 트리 생성", + "session.new.lastModified": "최근 수정", + "session.header.search.placeholder": "{{project}} 검색", + "session.header.searchFiles": "파일 검색", + "session.header.openIn": "다음에서 열기", + "session.header.open.action": "{{app}} 열기", + "session.header.open.ariaLabel": "{{app}}에서 열기", + "session.header.open.menu": "열기 옵션", + "session.header.open.copyPath": "경로 복사", + "status.popover.trigger": "상태", + "status.popover.ariaLabel": "서버 구성", + "status.popover.tab.servers": "서버", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "플러그인", + "status.popover.action.manageServers": "서버 관리", + "session.share.popover.title": "웹에 게시", + "session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.", + "session.share.popover.description.unshared": + "세션을 웹에 공개적으로 공유합니다. 링크가 있는 누구나 액세스할 수 있습니다.", + "session.share.action.share": "공유", + "session.share.action.publish": "게시", + "session.share.action.publishing": "게시 중...", + "session.share.action.unpublish": "게시 취소", + "session.share.action.unpublishing": "게시 취소 중...", + "session.share.action.view": "보기", + "session.share.copy.copied": "복사됨", + "session.share.copy.copyLink": "링크 복사", + "lsp.tooltip.none": "LSP 서버 없음", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "프롬프트 로드 중...", + "terminal.loading": "터미널 로드 중...", + "terminal.title": "터미널", + "terminal.title.numbered": "터미널 {{number}}", + "terminal.close": "터미널 닫기", + "terminal.connectionLost.title": "연결 끊김", + "terminal.connectionLost.description": + "터미널 연결이 중단되었습니다. 서버가 재시작하면 이런 일이 발생할 수 있습니다.", + "common.closeTab": "탭 닫기", + "common.dismiss": "닫기", + "common.requestFailed": "요청 실패", + "common.moreOptions": "더 많은 옵션", + "common.learnMore": "더 알아보기", + "common.rename": "이름 바꾸기", + "common.export": "내보내기", + "common.reset": "초기화", + "common.archive": "보관", + "common.delete": "삭제", + "common.close": "닫기", + "common.edit": "편집", + "common.loadMore": "더 불러오기", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "메뉴 토글", + "sidebar.nav.projectsAndSessions": "프로젝트 및 세션", + "sidebar.settings": "설정", + "sidebar.help": "도움말", + "sidebar.workspaces.enable": "작업 공간 활성화", + "sidebar.workspaces.disable": "작업 공간 비활성화", + "sidebar.gettingStarted.title": "시작하기", + "sidebar.gettingStarted.line1": "OpenCode에는 무료 모델이 포함되어 있어 즉시 시작할 수 있습니다.", + "sidebar.gettingStarted.line2": "Claude, GPT, Gemini 등을 포함한 모델을 사용하려면 공급자를 연결하세요.", + "sidebar.project.recentSessions": "최근 세션", + "sidebar.project.viewAllSessions": "모든 세션 보기", + "sidebar.project.clearNotifications": "알림 지우기", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "데스크톱", + "settings.section.server": "서버", + "settings.tab.general": "일반", + "settings.tab.shortcuts": "단축키", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL 통합", + "settings.desktop.wsl.description": "Windows의 WSL 내부에서 OpenCode 서버를 실행합니다.", + "settings.general.section.appearance": "모양", + "settings.general.section.notifications": "시스템 알림", + "settings.general.section.updates": "업데이트", + "settings.general.section.sounds": "효과음", + "settings.general.section.feed": "피드", + "settings.general.section.display": "디스플레이", + "settings.general.row.language.title": "언어", + "settings.general.row.language.description": "OpenCode 표시 언어 변경", + "settings.general.row.appearance.title": "모양", + "settings.general.row.appearance.description": "기기에서 OpenCode가 보이는 방식 사용자 지정", + "settings.general.row.colorScheme.title": "색상 테마", + "settings.general.row.colorScheme.description": "OpenCode가 시스템, 라이트 또는 다크 테마를 따를지 선택하세요", + "settings.general.row.theme.title": "테마", + "settings.general.row.theme.description": "OpenCode 테마 사용자 지정", + "settings.general.row.font.title": "코드 글꼴", + "settings.general.row.font.description": "코드 블록에 사용되는 글꼴을 사용자 지정", + "settings.general.row.terminalFont.title": "터미널 글꼴", + "settings.general.row.terminalFont.description": "터미널에서 사용할 글꼴을 설정합니다", + "settings.general.row.uiFont.title": "UI 글꼴", + "settings.general.row.uiFont.description": "인터페이스 전반에 사용되는 글꼴을 사용자 지정", + "settings.general.row.followup.title": "후속 조치 동작", + "settings.general.row.followup.description": "후속 프롬프트를 즉시 실행할지 대기열에 넣을지 선택하세요", + "settings.general.row.followup.option.queue": "대기열", + "settings.general.row.followup.option.steer": "즉시 반영", + "settings.general.row.reasoningSummaries.title": "추론 요약 표시", + "settings.general.row.reasoningSummaries.description": "타임라인에 모델 추론 요약 표시", + "settings.general.row.shellToolPartsExpanded.title": "shell 도구 항목 펼치기", + "settings.general.row.shellToolPartsExpanded.description": + "타임라인에서 기본적으로 shell 도구 항목을 펼친 상태로 표시합니다", + "settings.general.row.editToolPartsExpanded.title": "edit 도구 항목 펼치기", + "settings.general.row.editToolPartsExpanded.description": + "타임라인에서 기본적으로 edit, write, patch 도구 항목을 펼친 상태로 표시합니다", + "settings.general.row.wayland.title": "네이티브 Wayland 사용", + "settings.general.row.wayland.description": "Wayland에서 X11 폴백을 비활성화합니다. 다시 시작해야 합니다.", + "settings.general.row.wayland.tooltip": + "혼합 주사율 모니터가 있는 Linux에서는 네이티브 Wayland가 더 안정적일 수 있습니다.", + "settings.general.row.releaseNotes.title": "릴리스 노트", + "settings.general.row.releaseNotes.description": "업데이트 후 '새 소식' 팝업 표시", + "settings.updates.row.startup.title": "시작 시 업데이트 확인", + "settings.updates.row.startup.description": "OpenCode를 실행할 때 업데이트를 자동으로 확인합니다", + "settings.updates.row.check.title": "업데이트 확인", + "settings.updates.row.check.description": "업데이트를 수동으로 확인하고, 사용 가능하면 설치합니다", + "settings.updates.action.checkNow": "지금 확인", + "settings.updates.action.checking": "확인 중...", + "settings.updates.toast.latest.title": "최신 상태입니다", + "settings.updates.toast.latest.description": "현재 최신 버전의 OpenCode를 사용 중입니다.", + "sound.option.none": "없음", + "sound.option.alert01": "알림 01", + "sound.option.alert02": "알림 02", + "sound.option.alert03": "알림 03", + "sound.option.alert04": "알림 04", + "sound.option.alert05": "알림 05", + "sound.option.alert06": "알림 06", + "sound.option.alert07": "알림 07", + "sound.option.alert08": "알림 08", + "sound.option.alert09": "알림 09", + "sound.option.alert10": "알림 10", + "sound.option.bipbop01": "빕-밥 01", + "sound.option.bipbop02": "빕-밥 02", + "sound.option.bipbop03": "빕-밥 03", + "sound.option.bipbop04": "빕-밥 04", + "sound.option.bipbop05": "빕-밥 05", + "sound.option.bipbop06": "빕-밥 06", + "sound.option.bipbop07": "빕-밥 07", + "sound.option.bipbop08": "빕-밥 08", + "sound.option.bipbop09": "빕-밥 09", + "sound.option.bipbop10": "빕-밥 10", + "sound.option.staplebops01": "스테이플밥스 01", + "sound.option.staplebops02": "스테이플밥스 02", + "sound.option.staplebops03": "스테이플밥스 03", + "sound.option.staplebops04": "스테이플밥스 04", + "sound.option.staplebops05": "스테이플밥스 05", + "sound.option.staplebops06": "스테이플밥스 06", + "sound.option.staplebops07": "스테이플밥스 07", + "sound.option.nope01": "아니오 01", + "sound.option.nope02": "아니오 02", + "sound.option.nope03": "아니오 03", + "sound.option.nope04": "아니오 04", + "sound.option.nope05": "아니오 05", + "sound.option.nope06": "아니오 06", + "sound.option.nope07": "아니오 07", + "sound.option.nope08": "아니오 08", + "sound.option.nope09": "아니오 09", + "sound.option.nope10": "아니오 10", + "sound.option.nope11": "아니오 11", + "sound.option.nope12": "아니오 12", + "sound.option.yup01": "네 01", + "sound.option.yup02": "네 02", + "sound.option.yup03": "네 03", + "sound.option.yup04": "네 04", + "sound.option.yup05": "네 05", + "sound.option.yup06": "네 06", + "settings.general.notifications.agent.title": "에이전트", + "settings.general.notifications.agent.description": "에이전트가 완료되거나 주의가 필요할 때 시스템 알림 표시", + "settings.general.notifications.permissions.title": "권한", + "settings.general.notifications.permissions.description": "권한이 필요할 때 시스템 알림 표시", + "settings.general.notifications.errors.title": "오류", + "settings.general.notifications.errors.description": "오류가 발생했을 때 시스템 알림 표시", + "settings.general.sounds.agent.title": "에이전트", + "settings.general.sounds.agent.description": "에이전트가 완료되거나 주의가 필요할 때 소리 재생", + "settings.general.sounds.permissions.title": "권한", + "settings.general.sounds.permissions.description": "권한이 필요할 때 소리 재생", + "settings.general.sounds.errors.title": "오류", + "settings.general.sounds.errors.description": "오류가 발생했을 때 소리 재생", + "settings.shortcuts.title": "키보드 단축키", + "settings.shortcuts.reset.button": "기본값으로 초기화", + "settings.shortcuts.reset.toast.title": "단축키 초기화됨", + "settings.shortcuts.reset.toast.description": "키보드 단축키가 기본값으로 초기화되었습니다.", + "settings.shortcuts.conflict.title": "단축키가 이미 사용 중임", + "settings.shortcuts.conflict.description": "{{keybind}}은(는) 이미 {{titles}}에 할당되어 있습니다.", + "settings.shortcuts.unassigned": "할당되지 않음", + "settings.shortcuts.pressKeys": "키 누르기", + "settings.shortcuts.search.placeholder": "단축키 검색", + "settings.shortcuts.search.empty": "단축키를 찾을 수 없습니다", + "settings.shortcuts.group.general": "일반", + "settings.shortcuts.group.session": "세션", + "settings.shortcuts.group.navigation": "탐색", + "settings.shortcuts.group.modelAndAgent": "모델 및 에이전트", + "settings.shortcuts.group.terminal": "터미널", + "settings.shortcuts.group.prompt": "프롬프트", + "settings.providers.title": "공급자", + "settings.providers.description": "공급자 설정은 여기서 구성할 수 있습니다.", + "settings.providers.section.connected": "연결된 공급자", + "settings.providers.connected.empty": "연결된 공급자 없음", + "settings.providers.section.popular": "인기 공급자", + "settings.providers.tag.environment": "환경", + "settings.providers.tag.config": "구성", + "settings.providers.tag.custom": "사용자 지정", + "settings.providers.tag.other": "기타", + "settings.models.title": "모델", + "settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.", + "settings.agents.title": "에이전트", + "settings.agents.description": "에이전트 설정은 여기서 구성할 수 있습니다.", + "settings.commands.title": "명령어", + "settings.commands.description": "명령어 설정은 여기서 구성할 수 있습니다.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP 설정은 여기서 구성할 수 있습니다.", + "settings.permissions.title": "권한", + "settings.permissions.description": "서버가 기본적으로 사용할 수 있는 도구를 제어합니다.", + "settings.permissions.section.tools": "도구", + "settings.permissions.toast.updateFailed.title": "권한 업데이트 실패", + "settings.permissions.action.allow": "허용", + "settings.permissions.action.ask": "묻기", + "settings.permissions.action.deny": "거부", + "settings.permissions.tool.read.title": "읽기", + "settings.permissions.tool.read.description": "파일 읽기 (파일 경로와 일치)", + "settings.permissions.tool.edit.title": "편집", + "settings.permissions.tool.edit.description": "파일 수정 (편집, 쓰기, 패치 및 다중 편집 포함)", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "glob 패턴을 사용하여 파일 일치", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "정규식을 사용하여 파일 내용 검색", + "settings.permissions.tool.list.title": "목록", + "settings.permissions.tool.list.description": "디렉터리 내 파일 나열", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "셸 명령어 실행", + "settings.permissions.tool.task.title": "작업", + "settings.permissions.tool.task.description": "하위 에이전트 실행", + "settings.permissions.tool.skill.title": "스킬", + "settings.permissions.tool.skill.description": "이름으로 스킬 불러오기", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "언어 서버 쿼리 실행", + "settings.permissions.tool.todowrite.title": "할 일 쓰기", + "settings.permissions.tool.todowrite.description": "할 일 목록 업데이트", + "settings.permissions.tool.webfetch.title": "웹 가져오기", + "settings.permissions.tool.webfetch.description": "URL에서 콘텐츠 가져오기", + "settings.permissions.tool.websearch.title": "웹 검색", + "settings.permissions.tool.websearch.description": "웹 검색", + "settings.permissions.tool.external_directory.title": "외부 디렉터리", + "settings.permissions.tool.external_directory.description": "프로젝트 디렉터리 외부의 파일에 액세스", + "settings.permissions.tool.doom_loop.title": "무한 반복", + "settings.permissions.tool.doom_loop.description": "동일한 입력으로 반복되는 도구 호출 감지", + "session.delete.failed.title": "세션 삭제 실패", + "session.delete.title": "세션 삭제", + "session.delete.confirm": '"{{name}}" 세션을 삭제하시겠습니까?', + "session.delete.button": "세션 삭제", + "workspace.new": "새 작업 공간", + "workspace.type.local": "로컬", + "workspace.type.sandbox": "샌드박스", + "workspace.create.failed.title": "작업 공간 생성 실패", + "workspace.delete.failed.title": "작업 공간 삭제 실패", + "workspace.resetting.title": "작업 공간 재설정 중", + "workspace.resetting.description": "잠시 시간이 걸릴 수 있습니다.", + "workspace.reset.failed.title": "작업 공간 재설정 실패", + "workspace.reset.success.title": "작업 공간 재설정됨", + "workspace.reset.success.description": "작업 공간이 이제 기본 브랜치와 일치합니다.", + "workspace.error.stillPreparing": "작업 공간이 아직 준비 중입니다", + "workspace.status.checking": "병합되지 않은 변경 사항 확인 중...", + "workspace.status.error": "Git 상태를 확인할 수 없습니다.", + "workspace.status.clean": "병합되지 않은 변경 사항이 감지되지 않았습니다.", + "workspace.status.dirty": "이 작업 공간에서 병합되지 않은 변경 사항이 감지되었습니다.", + "workspace.delete.title": "작업 공간 삭제", + "workspace.delete.confirm": '"{{name}}" 작업 공간을 삭제하시겠습니까?', + "workspace.delete.button": "작업 공간 삭제", + "workspace.reset.title": "작업 공간 재설정", + "workspace.reset.confirm": '"{{name}}" 작업 공간을 재설정하시겠습니까?', + "workspace.reset.button": "작업 공간 재설정", + "workspace.reset.archived.none": "활성 세션이 보관되지 않습니다.", + "workspace.reset.archived.one": "1개의 세션이 보관됩니다.", + "workspace.reset.archived.many": "{{count}}개의 세션이 보관됩니다.", + "workspace.reset.note": "이 작업은 작업 공간을 기본 브랜치와 일치하도록 재설정합니다.", + "common.open": "열기", + "dialog.releaseNotes.action.getStarted": "시작하기", + "dialog.releaseNotes.action.next": "다음", + "dialog.releaseNotes.action.hideFuture": "다시 보지 않기", + "dialog.releaseNotes.media.alt": "릴리스 미리보기", + "toast.project.reloadFailed.title": "{{project}} 다시 불러오기 실패", + "error.server.invalidConfiguration": "잘못된 구성", + "common.moreCountSuffix": " (외 {{count}}개)", + "common.time.justNow": "방금 전", + "common.time.minutesAgo.short": "{{count}}분 전", + "common.time.hoursAgo.short": "{{count}}시간 전", + "common.time.daysAgo.short": "{{count}}일 전", + "settings.providers.connected.environmentDescription": "환경 변수에서 연결됨", + "settings.providers.custom.description": "기본 URL로 OpenAI 호환 공급자를 추가합니다.", + + "app.server.unreachable": "{{server}}에 연결할 수 없습니다", + "app.server.retrying": "자동으로 재시도 중...", + "app.server.otherServers": "다른 서버", + "dialog.server.add.usernamePlaceholder": "사용자 이름", + "dialog.server.add.passwordPlaceholder": "비밀번호", + "server.row.noUsername": "사용자 이름 없음", + "session.review.noVcs.createGit.title": "Git 저장소 생성", + "session.review.noVcs.createGit.description": "이 프로젝트의 변경 사항을 추적, 검토 및 실행 취소", + "session.review.noVcs.createGit.actionLoading": "Git 저장소 생성 중...", + "session.review.noVcs.createGit.action": "Git 저장소 생성", + "session.todo.progress": "{{total}}개의 할 일 중 {{done}}개 완료", + "session.question.progress": "{{total}}개의 질문 중 {{current}}개", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "파일 탐색기", + "session.header.reveal.finder": "Finder에서 보기", + "session.header.reveal.fileExplorer": "파일 탐색기에서 보기", + "session.header.reveal.containingFolder": "이 항목이 있는 폴더 열기", + "session.header.open.fileManager": "파일 관리자", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "터미널", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "개발 성능 진단", + "debugBar.na": "해당 없음", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "세션 페이지에 닿은 마지막 완료된 라우트 전환. 라우터 시작부터 정착 후 첫 번째 페인트까지 측정됨.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "지난 5초간의 초당 프레임 수.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "지난 5초간의 최악의 프레임 시간.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "지난 5초간 32ms를 초과한 프레임.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "지난 5초간의 차단된 시간 및 긴 작업 수. 최대 작업: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "지난 5초간 관찰된 최악의 입력 지연.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "지난 5초간의 대략적인 상호작용 지속 시간. 이것은 공식 Web Vitals INP가 아닌 INP와 유사합니다.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "현재 앱 수명 동안의 누적 레이아웃 이동.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "사용된 JS 힙 대 힙 제한. Chromium 전용.", + "debugBar.mem.tip": "사용된 JS 힙 대 힙 제한. {{limit}} 중 {{used}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "모든 대화형 요소에 포커스 스타일 강제 적용", + "debugBar.focus.on": "켬", + "debugBar.focus.off": "끔", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "선택한 언어를 변경하지 않고 전체 앱의 레이아웃 방향 강제 설정", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "알 수 없음", + "error.page.circular": "[순환]", + "error.serverSDK.noServerAvailable": "사용 가능한 서버 없음", + "error.serverSDK.serverNotAvailable": "서버를 사용할 수 없음", + "error.childStore.persistedCacheCreateFailed": "영구 캐시 생성 실패", + "error.childStore.persistedProjectMetadataCreateFailed": "영구 프로젝트 메타데이터 생성 실패", + "error.childStore.persistedProjectIconCreateFailed": "영구 프로젝트 아이콘 생성 실패", + "error.childStore.storeCreateFailed": "저장소 생성 실패", + "terminal.connectionLost.abnormalClose": "WebSocket이 비정상적으로 닫힘: {{code}}", + "terminal.connectTicket.csrfError": + "PTY 연결 티켓이 오리진 또는 CSRF 검사에서 거부되었습니다. 서버의 CORS 구성을 확인하세요.", + "terminal.connectTicket.statusError": "PTY 연결 티켓 실패(상태: {{status}})", + + "titlebar.update": "업데이트", + "titlebar.updateVersion": "업데이트 {{version}}", + + "command.project.previous": "이전 프로젝트", + "command.project.next": "다음 프로젝트", + "command.project.index": "{{index}}번 프로젝트로 전환", + "command.tab.reopenClosed": "닫은 탭 다시 열기", + "model.tooltip.context.label": "컨텍스트", + "model.tooltip.inputs": "입력", + "model.tooltip.model": "모델", + "model.tooltip.provider": "공급자", + "model.tooltip.reasoning": "추론", + "common.clear": "지우기", + "session.child.promptDisabled": "하위 에이전트 세션에는 프롬프트를 입력할 수 없습니다.", + "session.child.backToParent": "메인 세션으로 돌아가기.", + + "wsl.server.add": "WSL 서버 추가", + "wsl.server.addShort": "WSL 추가", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL 서버", + "wsl.server.retryStart": "시작 재시도", + "wsl.server.updating": "업데이트 중...", + "wsl.onboarding.step.distro": "배포판 선택", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL 확인 중...", + "wsl.onboarding.restartRequired": "WSL 설치를 완료하려면 Windows를 다시 시작해야 합니다.", + "wsl.onboarding.ready": "WSL이 준비되었습니다.", + "wsl.onboarding.required": "계속하려면 WSL이 필요합니다.", + "wsl.onboarding.checkingDistros": "배포판 확인 중...", + "wsl.onboarding.installingDistro": "{{distro}} 배포판 설치 중...", + "wsl.onboarding.checkingDistro": "{{distro}} 배포판 확인 중...", + "wsl.onboarding.listingDistros": "배포판 목록 확인 중...", + "wsl.onboarding.distroReady": "{{distro}} 배포판이 준비되었습니다.", + "wsl.onboarding.distroNotInstalled": "{{distro}} 배포판이 아직 설치되지 않았습니다.", + "wsl.onboarding.openDistroOnce": "설정을 완료하려면 {{distro}} 배포판을 한 번 여세요.", + "wsl.onboarding.finishingDistro": "{{distro}} 배포판 설정을 마무리하는 중입니다.", + "wsl.onboarding.pickDistro": "배포판을 선택하거나 아래에서 설치하세요.", + "wsl.onboarding.checkingOpencode": "OpenCode 확인 중...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}}에서 OpenCode 확인 중...", + "wsl.onboarding.updatingOpencode": "OpenCode 업데이트 중...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}}에서 OpenCode 업데이트 중...", + "wsl.onboarding.updateOpencodeIn": "{{distro}}에서 OpenCode를 업데이트하세요.", + "wsl.onboarding.updateOpencode": "OpenCode 업데이트", + "wsl.onboarding.opencodeReadyIn": "{{distro}}에서 OpenCode를 사용할 준비가 되었습니다.", + "wsl.onboarding.opencodeReady": "OpenCode가 준비되었습니다.", + "wsl.onboarding.installOpencodeIn": "{{distro}}에 OpenCode를 설치하세요.", + "wsl.onboarding.installOpencode": "OpenCode 설치", + "wsl.onboarding.chooseDistroFirst": "먼저 배포판을 선택하세요.", + "wsl.onboarding.loadFailed": "WSL 상태를 불러오지 못했습니다.", + "wsl.onboarding.loading": "로딩 중...", + "wsl.onboarding.installedDistros": "설치된 배포판", + "wsl.onboarding.checkAgain": "다시 확인", + "wsl.onboarding.distroStatus.ready": "준비됨", + "wsl.onboarding.distroStatus.checking": "확인 중...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode가 설치되지 않음", + "wsl.onboarding.distroStatus.missingTools": "bash, curl 없음", + "wsl.onboarding.distroStatus.unsupported": "지원되지 않음 · WSL 2 사용", + "wsl.onboarding.needAnotherDistro": "다른 배포판이 필요하신가요?", + "wsl.onboarding.needAnotherDistroHint": "WSL 카탈로그에서 Linux 배포판을 설치하세요", + "wsl.onboarding.wslNotInstalled.title": "WSL이 설치되지 않음", + "wsl.onboarding.wslNotInstalled.description": + "OpenCode에서 WSL 서버를 추가하려면 WSL (Windows Subsystem for Linux)이 필요합니다", + "wsl.onboarding.wslUnavailable.title": "WSL을 사용할 수 없음", + "wsl.onboarding.wslUnavailable.description": "OpenCode에서 이 컴퓨터의 WSL을 확인할 수 없습니다.", + "wsl.onboarding.installWsl": "WSL 설치", + "wsl.onboarding.windowsRestartRequired": "WSL 설치를 완료하려면 Windows를 다시 시작한 다음 OpenCode를 다시 여세요.", + "wsl.onboarding.next": "다음", + "wsl.onboarding.refresh": "새로 고침", + "wsl.onboarding.allDistrosAdded": "설치된 모든 배포판이 이미 추가되었습니다.", + "wsl.onboarding.noDistros": "아직 감지된 배포판이 없습니다.", + "wsl.onboarding.install": "설치", + "wsl.onboarding.installing": "설치 중...", + "wsl.onboarding.installDistro": "배포판 설치", + "wsl.onboarding.searchDistros": "배포판 검색", + "wsl.onboarding.wsl2Required": "WSL 2가 필요합니다.", + "wsl.onboarding.toolsRequired": "이 배포판에는 bash와 curl이 필요합니다.", + "wsl.onboarding.openTerminal": "터미널 열기", + "wsl.onboarding.path": "경로: {{path}}", + "wsl.onboarding.notFound": "찾을 수 없음", + "wsl.onboarding.version": "버전: {{version}}", + "wsl.onboarding.unknown": "알 수 없음", + "wsl.onboarding.desktopVersion": "데스크톱 {{version}}", + "wsl.onboarding.versionMismatch": "설치된 버전이 데스크톱 앱 버전과 일치하지 않습니다.", + "wsl.onboarding.adding": "추가 중...", + + "language.uk": "Українська", + "error.page.description.localServerStartup": "로컬 서버를 시작하는 동안 오류가 발생했습니다.", + "error.page.action.exportLogs": "로그 내보내기", + + "home.title": "홈", + "home.projects": "프로젝트", + "home.project.add": "프로젝트 추가", + "home.recentlyClosed": "최근에 닫은 항목", + "home.server.collapse": "서버 프로젝트 접기", + "home.server.expand": "서버 프로젝트 펼치기", + "home.sessions.search.placeholder": "세션 검색", + "home.sessions.search.placeholder.scoped": "{{scope}}에서 세션 검색", + "home.sessions.search.sessions": "세션", + "home.sessions.search.noResults": "{{query}}에 해당하는 세션을 찾을 수 없습니다", + "home.sessions.empty": "아직 아무것도 없습니다", + "home.sessions.empty.description": "세션을 생성하여 시작하세요", + "home.sessions.group.today": "오늘", + "home.sessions.group.yesterday": "어제", + "home.sessions.group.older": "이전", + "home.providerTip": "75개 이상의 공급자에 연결하여 Claude, GPT, Gemini 등의 다른 모델을 사용하세요", + + "session.tab.unknown": "알 수 없는 세션", + "session.error.notFound": "이 세션을 찾을 수 없습니다", + "session.error.notFound.description": "이 탭은 이 서버에 더 이상 존재하지 않는 세션을 가리킵니다.", + "session.error.notFound.closeTab": "탭 닫기", + "session.error.serverConnection": "이 서버에 연결할 수 없습니다", + "session.review.noUncommittedChanges": "아직 커밋되지 않은 변경 사항이 없습니다", + "session.review.noBranchChanges": "아직 브랜치 변경 사항이 없습니다", + "session.question.minimize": "질문 최소화", + "session.question.restore": "질문 복원", + "session.question.pending.one": "{{count}}개의 대기 중인 질문", + "session.question.pending.other": "{{count}}개의 대기 중인 질문", + "session.new.project.new": "새 프로젝트", + "session.new.project.search": "프로젝트 검색", + "session.new.project.add": "프로젝트 추가", + "session.new.workspace.runIn": "세션 실행 위치", + "session.new.workspace.triggerLocal": "로컬", + "session.new.workspace.local": "로컬 저장소", + "session.new.workspace.existing": "작업 공간…", + "session.new.git.none": "Git 없음", + + "sidebar.empty.title": "열린 프로젝트 없음", + "sidebar.empty.description": "프로젝트를 열어 시작하세요", + + "settings.general.section.advanced": "고급", + "settings.general.row.shell.title": "터미널 셸", + "settings.general.row.shell.description": + "터미널에서 사용할 셸을 선택하세요. 호환되는 셸은 에이전트 도구 호출에도 사용됩니다.", + "settings.general.row.shell.autoDefault": "자동 (기본값)", + "settings.general.row.shell.terminalOnly": "터미널 전용", + "settings.general.row.showFileTree.title": "파일 트리", + "settings.general.row.showFileTree.description": "세션에 파일 트리 패널 표시", + "settings.general.row.showNavigation.title": "탐색 컨트롤", + "settings.general.row.showNavigation.description": "데스크톱 제목 표시줄에 뒤로 및 앞으로 버튼 표시", + "settings.general.row.showSearch.title": "명령 팔레트", + "settings.general.row.showSearch.description": "제목 표시줄에 검색 및 명령 팔레트 버튼 표시", + "settings.general.row.showTerminal.title": "터미널", + "settings.general.row.showTerminal.description": "데스크톱 제목 표시줄에 터미널 버튼 표시", + "settings.general.row.showStatus.title": "서버 상태", + "settings.general.row.showStatus.description": "제목 표시줄에 서버 상태 버튼 표시", + "settings.general.row.mobileTitlebarBottom.title": "하단 탐색", + "settings.general.row.mobileTitlebarBottom.description": "모바일에서 제목 표시줄과 세션 탭을 화면 하단에 배치", + "settings.general.row.showCustomAgents.title": "에이전트 표시", + "settings.general.row.showCustomAgents.description": + "입력창에서 에이전트를 전환합니다. 숨기면 기본적으로 Build 에이전트를 사용합니다.", + "settings.general.row.newInterface.title": "새 레이아웃", + "settings.general.row.newInterface.badge": "신규", + "settings.general.row.newInterface.description": + "새 탭과 홈 화면 레이아웃을 사용합니다. 제한된 기간 동안 레이아웃을 전환할 수 있습니다.", + "settings.general.row.newInterfaceNotice.title": "이제 새 레이아웃을 사용 중입니다", + "settings.general.row.newInterfaceNotice.description": "이전 레이아웃은 더 이상 사용할 수 없습니다", + "settings.general.row.newInterfaceNotice.dismiss": "닫기", + "settings.general.row.pinchZoom.title": "핀치 줌", + "settings.general.row.pinchZoom.description": "트랙패드 핀치 및 Ctrl-스크롤 제스처로 확대/축소 허용", + "settings.updates.action.downloading": "다운로드 중...", + "settings.updates.action.installing": "설치 중...", + + "command.logs.export": "로그 내보내기", + + "help.tabs.toast.ariaLabel": "탭을 소개합니다. 탭으로 작업과 활성 세션을 정리하세요", + "help.tabs.toast.dismiss": "탭 정보 닫기", + "help.tabs.title": "탭 소개", + "help.tabs.description": "탭으로 작업과 활성 세션을 정리하세요", + "help.tabs.date": "7월 14일", + "help.tabs.introduction": "이제 OpenCode Desktop은 탭을 중심으로 구성됩니다.", + "help.tabs.sessions": + "탭에서 새 세션을 시작하거나 프로젝트에서 기존 세션을 여세요. 새로운 작업을 시작할 때 새 탭을 열고, 작업을 마치면 닫으세요.", + "help.tabs.organize": + "탭을 몇 개 열어 두면 활성 세션을 더 쉽게 정리할 수 있습니다. 탭을 계속 열어 둘 예정이라면 기억하기 쉬운 이름으로 바꾸세요.", + "help.tabs.home": "새 홈 화면에서 모든 세션과 프로젝트를 확인할 수 있습니다. 세션을 선택하면 탭에서 열립니다.", + "help.tabs.persistence": "앱을 다시 열어도 탭은 그대로 열려 있습니다.", + "help.tabs.worktrees": + "새 디자인은 아직 Git Worktrees를 지원하지 않지만 곧 지원할 예정입니다. 이전 레이아웃을 계속 사용하려면 설정에서 레이아웃을 전환할 수 있습니다. 단, 몇 주 후에는 새 레이아웃이 영구 적용됩니다.", + + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "파일", + "desktop.menu.edit": "편집", + "desktop.menu.view": "보기", + "desktop.menu.go": "이동", + "desktop.menu.window": "윈도우", + "desktop.menu.help": "도움말", + "desktop.menu.checkForUpdates": "업데이트 확인...", + "desktop.menu.settings": "설정", + "desktop.menu.reloadWebview": "WebView 새로 고침", + "desktop.menu.restart": "다시 시작", + "desktop.menu.exportLogs": "로그 내보내기...", + "desktop.menu.newSession": "새 세션", + "desktop.menu.openProject": "프로젝트 열기...", + "desktop.menu.newWindow": "새 윈도우", + "desktop.menu.closeWindow": "윈도우 닫기", + "desktop.menu.undo": "실행 취소", + "desktop.menu.redo": "다시 실행", + "desktop.menu.cut": "잘라내기", + "desktop.menu.copy": "복사", + "desktop.menu.paste": "붙여넣기", + "desktop.menu.delete": "삭제", + "desktop.menu.selectAll": "모두 선택", + "desktop.menu.toggleSidebar": "사이드바 표시/숨기기", + "desktop.menu.toggleTerminal": "터미널 표시/숨기기", + "desktop.menu.toggleFileTree": "파일 트리 표시/숨기기", + "desktop.menu.reload": "새로 고침", + "desktop.menu.toggleDeveloperTools": "개발자 도구 표시/숨기기", + "desktop.menu.actualSize": "실제 크기", + "desktop.menu.zoomIn": "확대", + "desktop.menu.zoomOut": "축소", + "desktop.menu.toggleFullScreen": "전체 화면 전환", + "desktop.menu.back": "뒤로", + "desktop.menu.forward": "앞으로", + "desktop.menu.previousSession": "이전 세션", + "desktop.menu.nextSession": "다음 세션", + "desktop.menu.previousProject": "이전 프로젝트", + "desktop.menu.nextProject": "다음 프로젝트", + "desktop.menu.minimize": "최소화", + "desktop.menu.maximize": "최대화", + "desktop.menu.documentation": "OpenCode 문서", + "desktop.menu.supportForum": "지원 포럼", + "desktop.menu.shareFeedback": "피드백 보내기", + "desktop.menu.reportBug": "버그 신고", + "desktop.menu.ariaLabel": "OpenCode 메뉴", + + "desktop.updater.dialog.checkFailed.message": "업데이트를 확인하지 못했습니다.", + "desktop.updater.dialog.checkFailed.title": "업데이트 오류", + "desktop.updater.dialog.upToDate.message": "최신 버전을 사용 중입니다.", + "desktop.updater.dialog.upToDate.title": "업데이트 없음", + "desktop.updater.dialog.ready.message": "업데이트 {{version}} 다운로드가 완료되었습니다. 지금 다시 시작할까요?", + "desktop.updater.dialog.ready.title": "업데이트 준비 완료", + "desktop.updater.dialog.restart": "다시 시작", + "desktop.updater.dialog.later": "나중에", + + "desktop.recovery.action.relaunch": "다시 실행", + "desktop.recovery.action.exportLogs": "로그 내보내기", + "desktop.recovery.action.keepWaiting": "계속 기다리기", + "desktop.recovery.action.quit": "종료", + "desktop.recovery.loadFailed": "OpenCode를 불러오지 못했습니다", + "desktop.recovery.terminated": "OpenCode 윈도우가 예기치 않게 종료되었습니다", + "desktop.recovery.unresponsive": "OpenCode가 응답하지 않습니다", + "desktop.recovery.unresponsive.detail": "앱을 다시 실행하거나 로그를 열거나 계속 기다릴 수 있습니다.", + "desktop.recovery.loadFailed.detail": "윈도우: {{window}}\nURL: {{url}}\n오류: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "윈도우: {{window}}\n원인: {{reason}}\n코드: {{code}}", + "desktop.recovery.unknown": "<알 수 없음>", + + "desktop.dialog.chooseFolder": "폴더 선택", + "desktop.dialog.chooseFile": "파일 선택", + "desktop.dialog.saveFile": "파일 저장", + "desktop.dialog.files": "파일", + "desktop.server.local": "로컬 서버", + + "desktop.wsl.error.windowsOnly": "WSL은 Windows에서만 사용할 수 있습니다", + "desktop.wsl.error.unavailable": "WSL을 사용할 수 없습니다", + "desktop.wsl.error.listInstalled": "설치된 WSL 배포판 목록을 가져오지 못했습니다", + "desktop.wsl.error.listOnline": "온라인 WSL 배포판 목록을 가져오지 못했습니다", + "desktop.wsl.error.executeDistro": "배포판에서 명령을 실행할 수 없습니다", + "desktop.wsl.error.installWsl": "WSL 설치 실패", + "desktop.wsl.error.installDistro": "배포판 설치 실패: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode 설치 실패", + "desktop.wsl.error.alreadyAdded": "{{distro}} 배포판은 이미 추가되었습니다", + "desktop.wsl.error.opencodeMissing": "이 배포판에 opencode가 설치되어 있지 않습니다", + "desktop.wsl.error.opencodeCannotRun": "opencode가 설치되어 있지만 실행할 수 없습니다", + "desktop.wsl.error.opencodeNotInstalled": "{{distro}}에 OpenCode가 설치되어 있지 않습니다", + "desktop.wsl.error.updateVersion": + "OpenCode 업데이트가 완료되었지만 {{distro}}에서 여전히 {{installed}} 버전으로 표시됩니다. 예상 버전: {{expected}}", + "desktop.wsl.error.noVersion": "버전 없음", + "desktop.wsl.error.serverExited": "WSL 서버가 시작 후 종료되었습니다(코드={{code}} 신호={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL 서버가 정상 상태가 되기 전에 종료되었습니다(코드={{code}} 신호={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} 사이드카의 상태 확인이 {{timeout}}ms 후 시간 초과되었습니다", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} 명령이 {{timeout}}ms 후 시간 초과되었습니다", + "desktop.wsl.error.failedPort": "포트를 가져오지 못했습니다", + + "desktop.picker.error.notSelected": "선택기에서 파일을 선택하지 않았습니다", + "desktop.picker.error.sizeLimit": "선택한 첨부 파일이 {{limit}} MB 제한을 초과합니다", +} diff --git a/packages/app/src/i18n/lo.ts b/packages/app/src/i18n/lo.ts new file mode 100644 index 0000000000000000000000000000000000000000..acdc26df08aac516758bfd6fa27accd6e825633c --- /dev/null +++ b/packages/app/src/i18n/lo.ts @@ -0,0 +1,1148 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ໄຟລ໌", + "desktop.menu.edit": "ແກ້ໄຂ", + "desktop.menu.view": "ເບິ່ງ", + "desktop.menu.go": "ໄປ", + "desktop.menu.window": "ປ່ອງຢ້ຽມ", + "desktop.menu.help": "ຊ່ວຍ", + "desktop.menu.checkForUpdates": "ກວດເບິ່ງການອັບເດດ...", + "desktop.menu.settings": "ການຕັ້ງຄ່າ", + "desktop.menu.reloadWebview": "ໂຫຼດ Webview ຄືນໃໝ່", + "desktop.menu.restart": "ຣີສະຕາດ", + "desktop.menu.exportLogs": "ບັນທຶກການສົ່ງອອກ...", + "desktop.menu.newSession": "ເຊດຊັນໃໝ່", + "desktop.menu.openProject": "ເປີດໂຄງການ...", + "desktop.menu.newWindow": "ໜ້າຈໍໃໝ່", + "desktop.menu.closeWindow": "ປິດໜ້າຈໍ", + "desktop.menu.undo": "ຍົກເລີກ", + "desktop.menu.redo": "ເຮັດຄືນ", + "desktop.menu.cut": "ຕັດ", + "desktop.menu.copy": "ສຳເນົາ", + "desktop.menu.paste": "ວາງ", + "desktop.menu.delete": "ລຶບ", + "desktop.menu.selectAll": "ເລືອກທັງໝົດ", + "desktop.menu.toggleSidebar": "ສະຫຼັບແຖບດ້ານຂ້າງ", + "desktop.menu.toggleTerminal": "ສະຫຼັບສະຖານີ", + "desktop.menu.toggleFileTree": "ປິດ/ເປີດ File Tree", + "desktop.menu.reload": "ໂຫຼດໃໝ່", + "desktop.menu.toggleDeveloperTools": "ສະຫຼັບເຄື່ອງມືນັກພັດທະນາ", + "desktop.menu.actualSize": "ຂະໜາດຕົວຈິງ", + "desktop.menu.zoomIn": "ຊູມເຂົ້າ", + "desktop.menu.zoomOut": "ຊູມອອກ", + "desktop.menu.toggleFullScreen": "ສະຫຼັບເຕັມຈໍ", + "desktop.menu.back": "ກັບ", + "desktop.menu.forward": "ສົ່ງຕໍ່", + "desktop.menu.previousSession": "ເຊດຊັນກ່ອນໜ້າ", + "desktop.menu.nextSession": "ເຊດຊັນຕໍ່ໄປ", + "desktop.menu.previousProject": "ໂຄງການກ່ອນໜ້າ", + "desktop.menu.nextProject": "ໂຄງການຕໍ່ໄປ", + "desktop.menu.minimize": "ຫຍໍ້ລົງ", + "desktop.menu.maximize": "ສູງສຸດ", + "desktop.menu.documentation": "ເອກະສານ OpenCode", + "desktop.menu.supportForum": "ກະດານສະຫນັບສະຫນູນ", + "desktop.menu.shareFeedback": "ແບ່ງປັນຄວາມຄິດເຫັນ", + "desktop.menu.reportBug": "ລາຍງານຂໍ້ຜິດພາດ", + "desktop.menu.ariaLabel": "ເມນູ OpenCode", + "desktop.updater.dialog.checkFailed.message": "ກວດສອບການອັບເດດບໍ່ສຳເລັດ.", + "desktop.updater.dialog.checkFailed.title": "ອັບເດດຜິດພາດ", + "desktop.updater.dialog.upToDate.message": "ທ່ານອັບເດດແລ້ວ.", + "desktop.updater.dialog.upToDate.title": "ບໍ່ມີການອັບເດດ", + "desktop.updater.dialog.ready.message": "ດາວໂຫຼດອັບເດດ {{version}} ແລ້ວ. ຣີສະຕາດດຽວນີ້ບໍ?", + "desktop.updater.dialog.ready.title": "ອັບເດດພ້ອມແລ້ວ", + "desktop.updater.dialog.restart": "ຣີສະຕາດ", + "desktop.updater.dialog.later": "ຕໍ່ມາ", + "desktop.recovery.action.relaunch": "ເປີດຄືນໃໝ່", + "desktop.recovery.action.exportLogs": "ບັນທຶກການສົ່ງອອກ", + "desktop.recovery.action.keepWaiting": "ລໍຖ້າຕໍ່ໄປ", + "desktop.recovery.action.quit": "ອອກ", + "desktop.recovery.loadFailed": "OpenCode ລົ້ມເຫລວໃນການໂຫລດ", + "desktop.recovery.terminated": "ປ່ອງຢ້ຽມ OpenCode ຖືກປິດໂດຍບໍ່ຄາດຄິດ", + "desktop.recovery.unresponsive": "OpenCode ບໍ່ຕອບສະໜອງ", + "desktop.recovery.unresponsive.detail": "ທ່ານສາມາດເປີດແອັບຄືນໃໝ່, ເປີດບັນທຶກ, ຫຼືລໍຖ້າຕໍ່ໄປ.", + "desktop.recovery.loadFailed.detail": "ປ່ອງຢ້ຽມ: {{window}}\nURL: {{url}}\nຂໍ້ຜິດພາດ: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "ປ່ອງຢ້ຽມ: {{window}}\nເຫດຜົນ: {{reason}}\nລະຫັດ: {{code}}", + "desktop.recovery.unknown": "<ບໍ່ຮູ້ຈັກ>", + "desktop.dialog.chooseFolder": "ເລືອກໂຟນເດີ", + "desktop.dialog.chooseFile": "ເລືອກໄຟລ໌", + "desktop.dialog.saveFile": "ບັນທຶກໄຟລ໌", + "desktop.dialog.files": "ໄຟລ໌", + "desktop.server.local": "ເຊີບເວີທ້ອງຖິ່ນ", + "desktop.wsl.error.windowsOnly": "WSL ສາມາດໃຊ້ໄດ້ໃນ Windows ເທົ່ານັ້ນ", + "desktop.wsl.error.unavailable": "WSL ບໍ່ສາມາດໃຊ້ໄດ້", + "desktop.wsl.error.listInstalled": "ລົ້ມເຫລວໃນລາຍຊື່ WSL distros ທີ່ຕິດຕັ້ງ", + "desktop.wsl.error.listOnline": "ລົ້ມເຫລວໃນລາຍຊື່ WSL distros ອອນໄລນ໌", + "desktop.wsl.error.executeDistro": "ບໍ່ສາມາດປະຕິບັດຄໍາສັ່ງໃນ distro", + "desktop.wsl.error.installWsl": "ການຕິດຕັ້ງ WSL ລົ້ມເຫລວ", + "desktop.wsl.error.installDistro": "ລົ້ມເຫລວໃນການຕິດຕັ້ງ distro: {{distro}}", + "desktop.wsl.error.installOpencode": "ການຕິດຕັ້ງ OpenCode ລົ້ມເຫລວ", + "desktop.wsl.error.alreadyAdded": "{{distro}} ຖືກເພີ່ມແລ້ວ", + "desktop.wsl.error.opencodeMissing": "opencode ບໍ່ໄດ້ຕິດຕັ້ງຢູ່ໃນ distro ນີ້", + "desktop.wsl.error.opencodeCannotRun": "opencode ຖືກຕິດຕັ້ງແຕ່ບໍ່ສາມາດດໍາເນີນການໄດ້", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode ບໍ່ໄດ້ຕິດຕັ້ງຢູ່ໃນ {{distro}}", + "desktop.wsl.error.updateVersion": + "ການປັບປຸງ OpenCode ສໍາເລັດແລ້ວແຕ່ {{distro}} ຍັງລາຍງານ {{installed}}; ຄາດວ່າ {{expected}}", + "desktop.wsl.error.noVersion": "ບໍ່ມີສະບັບ", + "desktop.wsl.error.serverExited": "ເຊີບເວີ WSL ອອກຈາກການເລີ່ມຕົ້ນ (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "ເຊີບເວີ WSL ອອກກ່ອນທີ່ຈະມີສຸຂະພາບດີ (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Sidecar ສໍາລັບການກວດສອບສຸຂະພາບ {{distro}} ໝົດເວລາຫຼັງຈາກ {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} ໝົດເວລາຫຼັງຈາກ {{timeout}}ms", + "desktop.wsl.error.failedPort": "ລົ້ມເຫລວໃນການເອົາພອດ", + "desktop.picker.error.notSelected": "ໄຟລ໌ບໍ່ໄດ້ຖືກເລືອກໂດຍຕົວເລືອກ", + "desktop.picker.error.sizeLimit": "ໄຟລ໌ແນບທີ່ເລືອກເກີນຂີດຈຳກັດ {{limit}} MB", + "command.category.suggested": "ແນະນຳ", + "command.category.view": "ເບິ່ງ", + "command.category.project": "ໂຄງການ", + "command.category.provider": "ຜູ້ໃຫ້ບໍລິການ", + "command.category.server": "ເຊີບເວີ", + "command.category.session": "ເຊດຊັນ", + "command.category.theme": "ຫົວຂໍ້", + "command.category.language": "ພາສາ", + "command.category.file": "ໄຟລ໌", + "command.category.context": "ບໍລິບົດ", + "command.category.terminal": "ສະຖານີ", + "command.category.model": "ຕົວແບບ", + "command.category.mcp": "MCP", + "command.category.agent": "ຕົວແທນ", + "command.category.permissions": "ການອະນຸຍາດ", + "command.category.workspace": "ພື້ນທີ່ເຮັດວຽກ", + "command.category.settings": "ການຕັ້ງຄ່າ", + "command.logs.export": "ບັນທຶກການສົ່ງອອກ", + "theme.scheme.system": "ລະບົບ", + "theme.scheme.light": "ແສງສະຫວ່າງ", + "theme.scheme.dark": "ມືດ", + "command.sidebar.toggle": "ສະຫຼັບແຖບດ້ານຂ້າງ", + "command.project.open": "ເປີດໂຄງການ", + "command.project.previous": "ໂຄງການທີ່ຜ່ານມາ", + "command.project.next": "ໂຄງການຕໍ່ໄປ", + "command.project.index": "ປ່ຽນເປັນໂຄງການ {{index}}", + "command.provider.connect": "ເຊື່ອມຕໍ່ຜູ້ໃຫ້ບໍລິການ", + "command.server.switch": "ສະຫຼັບເຊີບເວີ", + "command.settings.open": "ເປີດການຕັ້ງຄ່າ", + "command.session.previous": "ເຊດຊັນທີ່ຜ່ານມາ", + "command.session.next": "ເຊດຊັນຕໍ່ໄປ", + "command.session.previous.unseen": "ເຊດຊັນທີ່ຍັງບໍ່ໄດ້ອ່ານຜ່ານມາ", + "command.session.next.unseen": "ເຊດຊັນທີ່ຍັງບໍ່ໄດ້ອ່ານຕໍ່ໄປ", + "command.session.archive": "ຮວບຮວມເຊດຊັນ", + "command.palette": "ແຖບຄໍາສັ່ງ", + "command.theme.cycle": "ຮູບແບບສີສັນຮອບວຽນ", + "command.theme.set": "ໃຊ້ຫົວຂໍ້: {{theme}}", + "command.theme.scheme.cycle": "ໂຄງການສີຂອງວົງຈອນ", + "command.theme.scheme.set": "ໃຊ້ຮູບແບບສີ: {{scheme}}", + "command.language.cycle": "ພາສາຮອບວຽນ", + "command.language.set": "ໃຊ້ພາສາ: {{language}}", + "command.session.new": "ເຊດຊັນໃໝ່", + "command.file.open": "ເປີດໄຟລ໌", + "command.tab.close": "ປິດແຖບ", + "command.tab.reopenClosed": "ເປີດແຖບປິດຄືນໃໝ່", + "command.context.addSelection": "ເພີ່ມການເລືອກໃສ່ບໍລິບົດ", + "command.context.addSelection.description": "ເພີ່ມສາຍທີ່ເລືອກຈາກໄຟລ໌ປັດຈຸບັນ", + "command.input.focus": "ໂຟກັສໃສ່", + "command.terminal.toggle": "ປິດ/ເປີດ terminal", + "command.fileTree.toggle": "ສະຫຼັບຕົ້ນໄມ້ໄຟລ໌", + "command.review.toggle": "ສະຫຼັບການທົບທວນຄືນ", + "command.terminal.new": "ສະຖານີໃໝ່", + "command.terminal.new.description": "ສ້າງແຖບ terminal ໃໝ່", + "command.steps.toggle": "ສະຫຼັບຂັ້ນຕອນ", + "command.steps.toggle.description": "ສະແດງ ຫຼືເຊື່ອງຂັ້ນຕອນສຳລັບຂໍ້ຄວາມປັດຈຸບັນ", + "command.message.previous": "ຂໍ້ຄວາມກ່ອນໜ້າ", + "command.message.previous.description": "ໄປຫາຂໍ້ຄວາມຜູ້ໃຊ້ທີ່ຜ່ານມາ", + "command.message.next": "ຂໍ້ຄວາມຕໍ່ໄປ", + "command.message.next.description": "ໄປຫາຂໍ້ຄວາມຜູ້ໃຊ້ຕໍ່ໄປ", + "command.model.choose": "ເລືອກຕົວແບບ", + "command.model.choose.description": "ເລືອກຕົວແບບອື່ນ", + "command.mcp.toggle": "ສະຫຼັບ MCPs", + "command.mcp.toggle.description": "ສະຫຼັບ MCPs", + "command.agent.cycle": "ຕົວແທນວົງຈອນ", + "command.agent.cycle.description": "ສະຫຼັບໄປຫາຕົວແທນຕໍ່ໄປ", + "command.agent.cycle.reverse": "ວົງຈອນຕົວແທນກັບຫຼັງ", + "command.agent.cycle.reverse.description": "ສະຫຼັບໄປຫາຕົວແທນກ່ອນໜ້າ", + "command.model.variant.cycle": "ຄວາມພະຍາຍາມຄິດຮອບວຽນ", + "command.model.variant.cycle.description": "ປ່ຽນໄປສູ່ລະດັບຄວາມພະຍາຍາມຕໍ່ໄປ", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "ຍອມຮັບການອະນຸຍາດອັດຕະໂນມັດ", + "command.permissions.autoaccept.disable": "ຢຸດການຍອມຮັບອັດຕະໂນມັດ", + "command.workspace.toggle": "ສະຫຼັບພື້ນທີ່ເຮັດວຽກ", + "command.workspace.toggle.description": "ເປີດ ຫຼືປິດພື້ນທີ່ເຮັດວຽກຫຼາຍບ່ອນຢູ່ໃນແຖບດ້ານຂ້າງ", + "command.session.undo": "ຍົກເລີກ", + "command.session.undo.description": "ຍົກເລີກຂໍ້ຄວາມສຸດທ້າຍ", + "command.session.redo": "ເຮັດຄືນ", + "command.session.redo.description": "ເຮັດຄືນຂໍ້ຄວາມທີ່ຍົກເລີກຫຼ້າສຸດ", + "command.session.compact": "ຊ່ວງເວລາກະທັດຮັດ", + "command.session.compact.description": "ສະຫຼຸບກອງປະຊຸມເພື່ອຫຼຸດຂະໜາດບໍລິບົດ", + "command.session.fork": "ສ້ອມຈາກຂໍ້ຄວາມ", + "command.session.fork.description": "ສ້າງເຊດຊັນໃໝ່ຈາກຂໍ້ຄວາມກ່ອນໜ້າ", + "command.session.share": "ແບ່ງປັນເຊດຊັນ", + "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": "ບໍ່ພົບຜົນການຊອກຫາ", + "palette.group.commands": "ຄໍາສັ່ງ", + "palette.group.files": "ໄຟລ໌", + "dialog.provider.search.placeholder": "ຜູ້ໃຫ້ບໍລິການຄົ້ນຫາ", + "dialog.provider.empty": "ບໍ່ພົບຜູ້ໃຫ້ບໍລິການ", + "dialog.provider.group.popular": "ນິຍົມ", + "dialog.provider.group.other": "ອື່ນໆ", + "dialog.provider.custom.label": "ຜູ້ໃຫ້ບໍລິການທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI ແບບກຳນົດເອງ", + "dialog.provider.tag.recommended": "ແນະນຳ", + "dialog.provider.opencode.note": "ແບບທີ່ຄັດສັນມາລວມທັງ Claude, GPT, Gemini ແລະອື່ນໆ", + "dialog.provider.opencode.tagline": "ຮູບແບບການເພີ່ມປະສິດທິພາບທີ່ເຊື່ອຖືໄດ້", + "dialog.provider.opencodeGo.tagline": "ການສະໝັກໃຊ້ລາຄາຕໍ່າສຳລັບທຸກຄົນ", + "dialog.provider.anthropic.note": "ເຂົ້າເຖິງຕົວແບບ Claude ໂດຍກົງ, ລວມທັງ Pro ແລະ Max", + "dialog.provider.copilot.note": "ຮູບແບບ AI ສໍາລັບການຊ່ວຍເຫຼືອການເຂົ້າລະຫັດຜ່ານ GitHub Copilot", + "dialog.provider.openai.note": "ແບບ GPT ສໍາລັບວຽກງານ AI ທົ່ວໄປທີ່ໄວ, ມີຄວາມສາມາດ", + "dialog.provider.google.note": "ແບບ Gemini ສໍາລັບການຕອບສະຫນອງໄວ, ມີໂຄງສ້າງ", + "dialog.provider.openrouter.note": "ເຂົ້າເຖິງທຸກຮູບແບບທີ່ຮອງຮັບຈາກຜູ້ໃຫ້ບໍລິການດຽວ", + "dialog.provider.vercel.note": "ການເຂົ້າເຖິງແບບຮວມຕົວແບບ AI ດ້ວຍການກຳນົດເສັ້ນທາງອັດສະລິຍະ", + "dialog.model.select.title": "ເລືອກຕົວແບບ", + "dialog.model.search.placeholder": "ຄົ້ນຫາຕົວແບບ", + "dialog.model.empty": "ບໍ່ພົບຕົວແບບ", + "dialog.model.manage": "ຈັດການຕົວແບບ", + "dialog.model.manage.description": "ປັບແຕ່ງຕົວແບບທີ່ປາກົດຢູ່ໃນຕົວເລືອກຕົວແບບ.", + "dialog.model.manage.provider.toggle": "ສະຫຼັບທຸກລຸ້ນ {{provider}}", + "dialog.model.unpaid.freeModels.title": "ແບບຟຣີສະໜອງໃຫ້ໂດຍ OpenCode", + "dialog.model.unpaid.addMore.title": "ເພີ່ມຮູບແບບເພີ່ມເຕີມຈາກຜູ້ໃຫ້ບໍລິການທີ່ນິຍົມ", + "dialog.model.unpaid.viewMoreProviders": "ເບິ່ງ 70+ ຜູ້ໃຫ້ບໍລິການເພີ່ມເຕີມ", + "dialog.provider.viewAll": "ສະແດງຜູ້ໃຫ້ບໍລິການເພີ່ມເຕີມ", + "provider.connect.title": "ເຊື່ອມຕໍ່ {{provider}}", + "provider.connect.title.anthropicProMax": "ເຂົ້າສູ່ລະບົບດ້ວຍ Claude Pro/Max", + "provider.connect.selectMethod": "ເລືອກວິທີການເຂົ້າສູ່ລະບົບສໍາລັບ {{provider}}.", + "provider.connect.method.apiKey": "ລະຫັດ API", + "provider.connect.method.browser": "ຕົວທ່ອງເວັບ", + "provider.connect.method.headless": "ບໍ່ມີຫົວ", + "provider.connect.status.inProgress": "ກໍາລັງດໍາເນີນຢູ່...", + "provider.connect.status.waiting": "ລໍຖ້າການອະນຸຍາດ...", + "provider.connect.status.failed": "ການອະນຸຍາດລົ້ມເຫລວ: {{error}}", + "provider.connect.apiKey.description": + "ໃສ່ລະຫັດ API {{provider}} ຂອງທ່ານເພື່ອເຊື່ອມຕໍ່ບັນຊີຂອງທ່ານ ແລະໃຊ້ຕົວແບບ {{provider}} ໃນ OpenCode.", + "provider.connect.apiKey.label": "ລະຫັດ API {{provider}}", + "provider.connect.apiKey.placeholder": "ລະຫັດ API", + "provider.connect.apiKey.required": "ຕ້ອງການລະຫັດ API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ໃຫ້ທ່ານເຂົ້າເຖິງຊຸດຮູບແບບການປັບແຕ່ງທີ່ໜ້າເຊື່ອຖືໄດ້ສຳລັບຕົວແທນການເຂົ້າລະຫັດ.", + "provider.connect.opencodeZen.line2": + "ດ້ວຍລະຫັດ API ດຽວເຈົ້າຈະໄດ້ຮັບການເຂົ້າເຖິງຕົວແບບເຊັ່ນ: Claude, GPT, Gemini, GLM ແລະອື່ນໆ.", + "provider.connect.opencodeZen.visit.prefix": "ຢ້ຽມຢາມ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "ເພື່ອເກັບກຳລະຫັດ API ຂອງທ່ານ.", + "provider.connect.oauth.code.visit.prefix": "ຢ້ຽມຢາມ", + "provider.connect.oauth.code.visit.link": "ລິ້ງນີ້", + "provider.connect.oauth.code.visit.suffix": + "ເພື່ອເກັບກຳລະຫັດການອະນຸຍາດຂອງທ່ານເພື່ອເຊື່ອມຕໍ່ບັນຊີຂອງທ່ານ ແລະໃຊ້ຕົວແບບ {{provider}} ໃນ OpenCode.", + "provider.connect.oauth.code.label": "ລະຫັດການອະນຸຍາດ {{method}}", + "provider.connect.oauth.code.placeholder": "ລະຫັດການອະນຸຍາດ", + "provider.connect.oauth.code.required": "ຕ້ອງການລະຫັດການອະນຸຍາດ", + "provider.connect.oauth.code.invalid": "ລະຫັດການອະນຸຍາດບໍ່ຖືກຕ້ອງ", + "provider.connect.oauth.auto.visit.prefix": "ຢ້ຽມຢາມ", + "provider.connect.oauth.auto.visit.link": "ລິ້ງນີ້", + "provider.connect.oauth.auto.visit.suffix": + "ແລະໃສ່ລະຫັດຂ້າງລຸ່ມນີ້ເພື່ອເຊື່ອມຕໍ່ບັນຊີຂອງທ່ານ ແລະໃຊ້ຕົວແບບ {{provider}} ໃນ OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "ລະຫັດຢືນຢັນ", + "provider.connect.toast.connected.title": "{{provider}} ເຊື່ອມຕໍ່", + "provider.connect.toast.connected.description": "ລຸ້ນ {{provider}} ມີໃຫ້ນຳໃຊ້ແລ້ວ.", + "provider.custom.title": "ຜູ້ໃຫ້ບໍລິການກຳນົດເອງ", + "provider.custom.unavailable": "ຜູ້ໃຫ້ບໍລິການແບບກຳນົດເອງບໍ່ສາມາດໃຊ້ໄດ້ໃນເຊີບເວີນີ້", + "provider.custom.description.prefix": "ຕັ້ງຄ່າຜູ້ໃຫ້ບໍລິການທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI. ເບິ່ງ", + "provider.custom.description.link": "docs config ຜູ້ໃຫ້ບໍລິການ", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ລະຫັດຜູ້ໃຫ້ບໍລິການ", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "ໂຕພິມນ້ອຍ, ຕົວເລກ, ຂີດຫຍໍ້, ຫຼືຂີດກ້ອງ", + "provider.custom.field.name.label": "ຊື່ສະແດງ", + "provider.custom.field.name.placeholder": "ຜູ້ໃຫ້ບໍລິການ AI ຂອງຂ້ອຍ", + "provider.custom.field.baseURL.label": "URL ພື້ນຖານ", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "ລະຫັດ API", + "provider.custom.field.apiKey.placeholder": "ລະຫັດ API", + "provider.custom.field.apiKey.description": "ທາງເລືອກ. ປ່ອຍຫວ່າງໄວ້ຖ້າທ່ານຈັດການການກວດສອບຜ່ານສ່ວນຫົວ.", + "provider.custom.models.label": "ແບບ", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "ຊື່", + "provider.custom.models.name.placeholder": "ຊື່ສະແດງ", + "provider.custom.models.remove": "ເອົາຕົວແບບອອກ", + "provider.custom.models.add": "ເພີ່ມຕົວແບບ", + "provider.custom.headers.label": "ສ່ວນຫົວ (ທາງເລືອກ)", + "provider.custom.headers.key.label": "ສ່ວນຫົວ", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "ຄ່າ", + "provider.custom.headers.value.placeholder": "ຄ່າ", + "provider.custom.headers.remove": "ເອົາສ່ວນຫົວອອກ", + "provider.custom.headers.add": "ເພີ່ມສ່ວນຫົວ", + "provider.custom.error.providerID.required": "ຕ້ອງການລະຫັດຜູ້ໃຫ້ບໍລິການ", + "provider.custom.error.providerID.format": "ໃຊ້ຕົວພິມນ້ອຍ, ຕົວເລກ, ຂີດຊື່, ຫຼືຂີດກ້ອງ", + "provider.custom.error.providerID.exists": "ລະຫັດຜູ້ໃຫ້ບໍລິການນັ້ນມີຢູ່ແລ້ວ", + "provider.custom.error.name.required": "ຕ້ອງສະແດງຊື່", + "provider.custom.error.baseURL.required": "ຕ້ອງການ URL ພື້ນຖານ", + "provider.custom.error.baseURL.format": "ຕ້ອງເລີ່ມຕົ້ນດ້ວຍ http:// ຫຼື https://", + "provider.custom.error.required": "ຕ້ອງການ", + "provider.custom.error.duplicate": "ຊໍ້າກັນ", + "provider.disconnect.toast.disconnected.title": "{{provider}} ຕັດການເຊື່ອມຕໍ່ແລ້ວ", + "provider.disconnect.toast.disconnected.description": "ໂມເດວ {{provider}} ບໍ່ມີແລ້ວ.", + "model.tag.free": "ຟຣີ", + "model.tag.latest": "ຫຼ້າສຸດ", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "ຂໍ້ຄວາມ", + "model.input.image": "ຮູບພາບ", + "model.input.audio": "ສຽງ", + "model.input.video": "ວິດີໂອ", + "model.input.pdf": "pdf", + "model.tooltip.allows": "ອະນຸຍາດ: {{inputs}}", + "model.tooltip.context.label": "ບໍລິບົດ", + "model.tooltip.inputs": "ວັດສະດຸປ້ອນ", + "model.tooltip.model": "ຕົວແບບ", + "model.tooltip.provider": "ຜູ້ໃຫ້ບໍລິການ", + "model.tooltip.reasoning": "ເຫດຜົນ", + "model.tooltip.reasoning.allowed": "ອະນຸຍາດໃຫ້ມີເຫດຜົນ", + "model.tooltip.reasoning.none": "ບໍ່ມີເຫດຜົນ", + "model.tooltip.context": "ຂອບເຂດຈໍາກັດ {{limit}}", + "common.search.placeholder": "ຊອກຫາ", + "common.goBack": "ນຳທາງກັບຄືນ", + "common.goForward": "ນຳທາງໄປຂ້າງໜ້າ", + "common.loading": "ກຳລັງໂຫຼດ", + "common.loading.ellipsis": "...", + "common.cancel": "ຍົກເລີກ", + "common.clear": "ຈະແຈ້ງ", + "common.open": "ເປີດ", + "common.connect": "ເຊື່ອມຕໍ່", + "common.disconnect": "ຕັດການເຊື່ອມຕໍ່", + "common.continue": "ສືບຕໍ່", + "common.submit": "ສົ່ງ", + "common.save": "ບັນທຶກ", + "common.saving": "ກຳລັງບັນທຶກ...", + "common.default": "ຄ່າເລີ່ມຕົ້ນ", + "common.attachment": "ໄຟລ໌ແນບ", + "prompt.placeholder.shell": "ໃສ່ຄຳສັ່ງ shell... {{example}}", + "prompt.placeholder.normal": 'ຖາມຫຍັງ ... "{{example}}"', + "prompt.placeholder.simple": "ຖາມຫຍັງ...", + "prompt.placeholder.summarizeComments": "ສະຫຼຸບຄຳເຫັນ...", + "prompt.placeholder.summarizeComment": "ສະຫຼຸບຄຳເຫັນ...", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "esc ເພື່ອອອກ", + "session.child.promptDisabled": "ບໍ່ສາມາດສົ່ງ prompt ໃຫ້ເຊດຊັນ subagent ໄດ້.", + "session.child.backToParent": "ກັບໄປທີ່ເຊດຊັນຫຼັກ.", + "prompt.example.1": "ແກ້ໄຂ TODO ໃນ codebase", + "prompt.example.2": "ເທັກໂນໂລຢີຂອງໂຄງການນີ້ແມ່ນຫຍັງ?", + "prompt.example.3": "ແກ້ໄຂການທົດສອບທີ່ແຕກຫັກ", + "prompt.example.4": "ອະທິບາຍວິທີການກວດສອບຄວາມຖືກຕ້ອງ", + "prompt.example.5": "ຊອກຫາ ແລະແກ້ໄຂຊ່ອງໂຫວ່ດ້ານຄວາມປອດໄພ", + "prompt.example.6": "ເພີ່ມການທົດສອບຫນ່ວຍບໍລິການສໍາລັບການບໍລິການຜູ້ໃຊ້", + "prompt.example.7": "ສະທ້ອນຟັງຊັນນີ້ເພື່ອໃຫ້ສາມາດອ່ານໄດ້ຫຼາຍຂຶ້ນ", + "prompt.example.8": "ຂໍ້ຜິດພາດນີ້ຫມາຍຄວາມວ່າແນວໃດ?", + "prompt.example.9": "ຊ່ວຍຂ້ອຍແກ້ໄຂບັນຫານີ້", + "prompt.example.10": "ສ້າງເອກະສານ API", + "prompt.example.11": "ເພີ່ມປະສິດທິພາບການສອບຖາມຖານຂໍ້ມູນ", + "prompt.example.12": "ເພີ່ມການກວດສອບການປ້ອນຂໍ້ມູນ", + "prompt.example.13": "ສ້າງອົງປະກອບໃຫມ່ສໍາລັບ ...", + "prompt.example.14": "ຂ້ອຍຈະນຳໃຊ້ໂຄງການນີ້ແນວໃດ?", + "prompt.example.15": "ທົບທວນລະຫັດຂອງຂ້ອຍສໍາລັບການປະຕິບັດທີ່ດີທີ່ສຸດ", + "prompt.example.16": "ເພີ່ມການຈັດການຂໍ້ຜິດພາດກັບຟັງຊັນນີ້", + "prompt.example.17": "ອະທິບາຍຮູບແບບ regex ນີ້", + "prompt.example.18": "ປ່ຽນອັນນີ້ເປັນ TypeScript", + "prompt.example.19": "ເພີ່ມການບັນທຶກໃນທົ່ວ codebase", + "prompt.example.20": "ການເພິ່ງພາອາໄສອັນໃດທີ່ລ້າສະໄຫມ?", + "prompt.example.21": "ຊ່ວຍຂ້ອຍຂຽນສະຄຣິບການເຄື່ອນຍ້າຍ", + "prompt.example.22": "ປະຕິບັດຖານຄວາມຈໍາສໍາລັບຈຸດສິ້ນສຸດນີ້", + "prompt.example.23": "ເພີ່ມ pagination ໃສ່ບັນຊີລາຍຊື່ນີ້", + "prompt.example.24": "ສ້າງຄໍາສັ່ງ CLI ສໍາລັບ ...", + "prompt.example.25": "ຕົວແປສະພາບແວດລ້ອມເຮັດວຽກແນວໃດຢູ່ທີ່ນີ້?", + "prompt.popover.emptyResults": "ບໍ່ມີຜົນໄດ້ຮັບທີ່ກົງກັນ", + "prompt.popover.emptyCommands": "ບໍ່ມີຄໍາສັ່ງທີ່ກົງກັນ", + "prompt.dropzone.label": "ວາງຮູບ, PDFs, ຫຼືໄຟລ໌ຂໍ້ຄວາມໃສ່ບ່ອນນີ້", + "prompt.dropzone.file.label": "ວາງໃສ່ໄຟລ໌ @mention", + "prompt.slash.badge.custom": "ແບບກຳນົດເອງ", + "prompt.slash.badge.skill": "ທັກສະ", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "ເຄື່ອນໄຫວ", + "prompt.context.includeActiveFile": "ລວມເອົາໄຟລ໌ທີ່ໃຊ້ງານຢູ່", + "prompt.context.removeActiveFile": "ເອົາໄຟລ໌ທີ່ໃຊ້ວຽກອອກຈາກບໍລິບົດ", + "prompt.context.removeFile": "ເອົາໄຟລ໌ອອກຈາກບໍລິບົດ", + "prompt.action.attachFile": "ເພີ່ມໄຟລ໌", + "prompt.menu.addImagesAndFiles": "ເພີ່ມໄຟລ໌ ແລະອື່ນໆອີກ", + "prompt.menu.imagesAndFiles": "ຮູບພາບ ແລະໄຟລ໌", + "prompt.menu.commands": "ຄໍາສັ່ງ", + "prompt.menu.context": "ບໍລິບົດ", + "prompt.menu.shellCommand": "ຄໍາສັ່ງ Shell", + "prompt.attachment.remove": "ເອົາໄຟລ໌ແນບອອກ", + "prompt.action.send": "ສົ່ງ", + "prompt.action.stop": "ຢຸດ", + "prompt.toast.pasteUnsupported.title": "ບໍ່ຮອງຮັບໄຟລ໌ແນບ", + "prompt.toast.pasteUnsupported.description": "ພຽງແຕ່ຮູບພາບ, PDFs, ຫຼືໄຟລ໌ຂໍ້ຄວາມສາມາດຕິດຢູ່ນີ້.", + "prompt.toast.attachmentDuplicate.title": "ໄຟລ໌ນີ້ໄດ້ຖືກອັບໂຫລດໄປກ່ອນແລ້ວ", + "prompt.toast.modelAgentRequired.title": "ເລືອກຕົວແທນ ແລະຕົວແບບ", + "prompt.toast.modelAgentRequired.description": "ເລືອກຕົວແທນ ແລະຕົວແບບກ່ອນສົ່ງ prompt.", + "prompt.toast.worktreeCreateFailed.title": "ສ້າງ Git worktree ບໍ່ສຳເລັດ", + "prompt.toast.sessionCreateFailed.title": "ລົ້ມເຫລວໃນການສ້າງເຊດຊັນ", + "prompt.toast.shellSendFailed.title": "ລົ້ມເຫລວໃນການສົ່ງຄໍາສັ່ງ shell", + "prompt.toast.commandSendFailed.title": "ລົ້ມເຫລວໃນການສົ່ງຄໍາສັ່ງ", + "prompt.toast.promptSendFailed.title": "ສົ່ງ prompt ບໍ່ສຳເລັດ", + "prompt.toast.promptSendFailed.description": "ບໍ່ສາມາດດຶງຂໍ້ມູນເຊດຊັນໄດ້", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} ຂອງ {{total}} ຖືກເປີດໃຊ້", + "dialog.mcp.empty": "ບໍ່ມີ MCPs ທີ່ຖືກຕັ້ງຄ່າ", + "dialog.lsp.empty": "LSPs ກວດພົບອັດຕະໂນມັດຈາກປະເພດໄຟລ໌", + "dialog.plugins.empty": "ປລັກອິນຖືກຕັ້ງຄ່າໃນ opencode.json", + "mcp.status.connected": "ເຊື່ອມຕໍ່", + "mcp.status.failed": "ລົ້ມເຫລວ", + "mcp.status.needs_auth": "ຕ້ອງການການຮັບຮອງ", + "mcp.status.disabled": "ປິດໃຊ້ງານ", + "mcp.auth.clickToAuthenticate": "ຄລິກເພື່ອພິສູດຢືນຢັນ", + "dialog.fork.empty": "ບໍ່ມີຂໍ້ຄວາມທີ່ຈະແຍກອອກຈາກ", + "dialog.directory.search.placeholder": "ຄົ້ນຫາໂຟນເດີ", + "dialog.directory.empty": "ບໍ່ພົບໂຟນເດີ", + "dialog.directory.action.selectFile": "ເລືອກໄຟລ໌", + "dialog.directory.action.selectFolder": "ເລືອກໂຟນເດີ", + "dialog.directory.root": "ຮາກ", + "dialog.directory.parent": "ພໍ່ແມ່", + "dialog.directory.readError": "ບໍ່ສາມາດອ່ານໂຟນເດີນີ້ໄດ້", + "app.server.unreachable": "ບໍ່ສາມາດບັນລຸ {{server}}", + "app.server.retrying": "ກຳລັງລອງໃໝ່ໂດຍອັດຕະໂນມັດ...", + "app.server.otherServers": "ເຊີບເວີອື່ນໆ", + "dialog.server.title": "ເຊີບເວີ", + "dialog.server.description": "ສະຫຼັບເຊີບເວີ OpenCode ທີ່ແອັບຯນີ້ເຊື່ອມຕໍ່ກັບ.", + "dialog.server.search.placeholder": "ເຊີບເວີຄົ້ນຫາ", + "dialog.server.empty": "ບໍ່ມີເຊີບເວີເທື່ອ", + "dialog.server.add.title": "ເພີ່ມເຊີບເວີ", + "dialog.server.add.url": "ທີ່ຢູ່ເຊີບເວີ", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "ບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເຊີບເວີໄດ້", + "dialog.server.add.checking": "ກຳລັງກວດສອບ...", + "dialog.server.add.button": "ເພີ່ມເຊີບເວີ", + "dialog.server.add.name": "ຊື່ເຊີບເວີ (ທາງເລືອກ)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "ຊື່ຜູ້ໃຊ້ (ທາງເລືອກ)", + "dialog.server.add.usernamePlaceholder": "ຊື່ຜູ້ໃຊ້", + "dialog.server.add.password": "ລະຫັດຜ່ານ (ທາງເລືອກ)", + "dialog.server.add.passwordPlaceholder": "ລະຫັດຜ່ານ", + "dialog.server.edit.title": "ແກ້ໄຂເຊີບເວີ", + "dialog.server.default.title": "ເຊີບເວີເລີ່ມຕົ້ນ", + "dialog.server.default.description": + "ເຊື່ອມຕໍ່ຫາເຊີບເວີນີ້ເມື່ອເປີດແອັບຯ ແທນທີ່ຈະເລີ່ມຕົ້ນເຊີບເວີທ້ອງຖິ່ນ. ຕ້ອງການຣີສະຕາດ.", + "dialog.server.default.none": "ບໍ່ໄດ້ເລືອກເຊີບເວີ", + "dialog.server.default.set": "ຕັ້ງເຊີບເວີປັດຈຸບັນເປັນຄ່າເລີ່ມຕົ້ນ", + "dialog.server.default.clear": "ຈະແຈ້ງ", + "dialog.server.action.remove": "ເອົາເຊີບເວີອອກ", + "dialog.server.menu.edit": "ແກ້ໄຂ", + "dialog.server.menu.default": "ຕັ້ງເປັນຄ່າເລີ່ມຕົ້ນ", + "dialog.server.menu.defaultRemove": "ເອົາຄ່າເລີ່ມຕົ້ນ", + "dialog.server.menu.delete": "ລຶບ", + "dialog.server.current": "ເຊີບເວີປັດຈຸບັນ", + "dialog.server.status.default": "ຄ່າເລີ່ມຕົ້ນ", + "wsl.server.add": "ເພີ່ມເຊີບເວີ WSL", + "wsl.server.addShort": "ເພີ່ມ WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "ເຊີບເວີ WSL", + "wsl.server.retryStart": "ລອງເລີ່ມໃໝ່", + "wsl.server.updating": "ກຳລັງອັບເດດ...", + "wsl.onboarding.step.distro": "ເລືອກ distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "ກຳລັງກວດສອບ WSL...", + "wsl.onboarding.restartRequired": "Windows ຕ້ອງການຣີສະຕາດເພື່ອສຳເລັດການຕິດຕັ້ງ WSL.", + "wsl.onboarding.ready": "WSL ພ້ອມແລ້ວ.", + "wsl.onboarding.required": "WSL ແມ່ນຕ້ອງການເພື່ອສືບຕໍ່.", + "wsl.onboarding.checkingDistros": "ກຳລັງກວດສອບການລົບກວນ...", + "wsl.onboarding.installingDistro": "ກຳລັງຕິດຕັ້ງ {{distro}}...", + "wsl.onboarding.checkingDistro": "ກຳລັງກວດສອບ {{distro}}...", + "wsl.onboarding.listingDistros": "ລາຍຊື່ການລົບກວນ...", + "wsl.onboarding.distroReady": "{{distro}} ພ້ອມແລ້ວ.", + "wsl.onboarding.distroNotInstalled": "{{distro}} ຍັງບໍ່ໄດ້ຕິດຕັ້ງເທື່ອ.", + "wsl.onboarding.openDistroOnce": "ເປີດ {{distro}} ຄັ້ງໜຶ່ງເພື່ອສຳເລັດການຕັ້ງຄ່າ.", + "wsl.onboarding.finishingDistro": "ກຳລັງສຳເລັດການຕັ້ງຄ່າ {{distro}}.", + "wsl.onboarding.pickDistro": "ເລືອກ distro ຫຼືຕິດຕັ້ງຫນຶ່ງຂ້າງລຸ່ມນີ້.", + "wsl.onboarding.checkingOpencode": "ກຳລັງກວດສອບ OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "ກຳລັງກວດສອບ OpenCode ໃນ {{distro}}...", + "wsl.onboarding.updatingOpencode": "ກຳລັງອັບເດດ OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "ກຳລັງອັບເດດ OpenCode ໃນ {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "ອັບເດດ OpenCode ໃນ {{distro}}.", + "wsl.onboarding.updateOpencode": "ອັບເດດ OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode ແມ່ນພ້ອມແລ້ວໃນ {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode ພ້ອມແລ້ວ.", + "wsl.onboarding.installOpencodeIn": "ຕິດຕັ້ງ OpenCode ໃນ {{distro}}.", + "wsl.onboarding.installOpencode": "ຕິດຕັ້ງ OpenCode", + "wsl.onboarding.chooseDistroFirst": "ເລືອກ distro ກ່ອນ.", + "wsl.onboarding.loadFailed": "ລົ້ມເຫລວໃນການໂຫຼດສະຖານະ WSL.", + "wsl.onboarding.loading": "ກຳລັງໂຫລດ...", + "wsl.onboarding.installedDistros": "ຕິດຕັ້ງ distros", + "wsl.onboarding.checkAgain": "ກວດເບິ່ງອີກຄັ້ງ", + "wsl.onboarding.distroStatus.ready": "ພ້ອມ", + "wsl.onboarding.distroStatus.checking": "ກຳລັງກວດສອບ...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode ບໍ່ໄດ້ຕິດຕັ້ງ", + "wsl.onboarding.distroStatus.missingTools": "ຂາດ bash, curl", + "wsl.onboarding.distroStatus.unsupported": "ບໍ່ຮອງຮັບ · ໃຊ້ WSL 2", + "wsl.onboarding.needAnotherDistro": "ຕ້ອງການ distro ອື່ນບໍ?", + "wsl.onboarding.needAnotherDistroHint": "ຕິດຕັ້ງການແຈກຢາຍ Linux ຈາກລາຍການ WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL ບໍ່ໄດ້ຕິດຕັ້ງ", + "wsl.onboarding.wslNotInstalled.description": + "WSL (ລະບົບຍ່ອຍຂອງ Windows ສໍາລັບ Linux) ກ່ອນທີ່ OpenCode ສາມາດເພີ່ມເຄື່ອງແມ່ຂ່າຍ WSL ໄດ້", + "wsl.onboarding.wslUnavailable.title": "WSL ບໍ່ສາມາດໃຊ້ໄດ້", + "wsl.onboarding.wslUnavailable.description": "OpenCode ບໍ່ສາມາດກວດສອບ WSL ໃນເຄື່ອງນີ້ໄດ້.", + "wsl.onboarding.installWsl": "ຕິດຕັ້ງ WSL", + "wsl.onboarding.windowsRestartRequired": "ຣີສະຕາດ Windows ເພື່ອສຳເລັດການຕິດຕັ້ງ WSL, ຈາກນັ້ນເປີດ OpenCode ຄືນໃໝ່.", + "wsl.onboarding.next": "ຕໍ່ໄປ", + "wsl.onboarding.refresh": "ໂຫຼດຂໍ້ມູນຄືນໃໝ່", + "wsl.onboarding.allDistrosAdded": "ການຕິດຕັ້ງທັງໝົດຖືກເພີ່ມແລ້ວ.", + "wsl.onboarding.noDistros": "ບໍ່ພົບການລົບກວນເທື່ອ.", + "wsl.onboarding.install": "ຕິດຕັ້ງ", + "wsl.onboarding.installing": "ກຳລັງຕິດຕັ້ງ...", + "wsl.onboarding.installDistro": "ຕິດຕັ້ງ distro", + "wsl.onboarding.searchDistros": "ຄົ້ນຫາ distros", + "wsl.onboarding.wsl2Required": "WSL 2 ແມ່ນຕ້ອງການ.", + "wsl.onboarding.toolsRequired": "distro ນີ້ຕ້ອງການ bash ແລະ curl.", + "wsl.onboarding.openTerminal": "ເປີດ terminal", + "wsl.onboarding.path": "ເສັ້ນທາງ: {{path}}", + "wsl.onboarding.notFound": "ບໍ່ພົບ", + "wsl.onboarding.version": "ລຸ້ນ: {{version}}", + "wsl.onboarding.unknown": "ບໍ່ຮູ້ຈັກ", + "wsl.onboarding.desktopVersion": "ເດັສທັອບ {{version}}", + "wsl.onboarding.versionMismatch": "ເວີຊັນທີ່ຕິດຕັ້ງບໍ່ກົງກັບເວີຊັນຂອງແອັບ desktop.", + "wsl.onboarding.adding": "ກຳລັງເພີ່ມ...", + "help.tabs.toast.ariaLabel": "ແນະນຳແຖບ. ຈັດລະບຽບການເຮັດວຽກແລະກອງປະຊຸມການເຄື່ອນໄຫວຂອງທ່ານດ້ວຍແຖບ", + "help.tabs.toast.dismiss": "ປິດຂໍ້ມູນແຖບ", + "help.tabs.title": "ແນະນຳແຖບ", + "help.tabs.description": "ຈັດກອງປະຊຸມການເຮັດວຽກ ແລະການເຄື່ອນໄຫວຂອງທ່ານດ້ວຍແຖບ", + "help.tabs.date": "ກໍລະກົດ 14", + "help.tabs.introduction": "OpenCode Desktop ຕອນນີ້ຖືກສ້າງຂຶ້ນໃນແຖບຕ່າງໆ.", + "help.tabs.sessions": + "ເລີ່ມເຊດຊັນໃໝ່ໃນແຖບໃດໜຶ່ງ, ຫຼືເປີດເຊດຊັນທີ່ມີຢູ່ແລ້ວຈາກໂຄງການໃດໆກໍຕາມຂອງເຈົ້າ. ເປີດແຖບໃໝ່ເມື່ອທ່ານເລີ່ມອັນໃໝ່, ແລະປິດມັນເມື່ອທ່ານສຳເລັດ.", + "help.tabs.organize": + "ການເປີດບາງແຖບເຮັດໃຫ້ມັນງ່າຍຂຶ້ນໃນການຈັດກອງປະຊຸມທີ່ມີການເຄື່ອນໄຫວຂອງທ່ານ. ປ່ຽນຊື່ແຖບເປັນສິ່ງທີ່ໜ້າຈົດຈຳ ຖ້າທ່ານວາງແຜນທີ່ຈະຮັກສາພວກມັນໄວ້.", + "help.tabs.home": "ທ່ານຈະພົບເຫັນເຊດຊັນ ແລະໂຄງການທັງໝົດຂອງທ່ານໃນໜ້າຈໍຫຼັກໃໝ່. ການເລືອກເຊດຊັນຈະເປີດມັນຢູ່ໃນແຖບ.", + "help.tabs.persistence": "ເມື່ອທ່ານເປີດແອັບຄືນໃໝ່, ແຖບຂອງທ່ານຍັງເປີດຢູ່.", + "help.tabs.worktrees": + "ການອອກແບບໃໝ່ຍັງບໍ່ຮອງຮັບ Git Worktrees ເທື່ອ, ມັນຈະມາໃນໄວໆນີ້. ສະນັ້ນຖ້າທ່ານຕ້ອງການສືບຕໍ່ໃຊ້ໂຄງຮ່າງກ່ອນໜ້າ, ທ່ານສາມາດສະຫຼັບລະຫວ່າງການຈັດວາງໃນການຕັ້ງຄ່າ. ພຽງແຕ່ຈື່ໄວ້ວ່າຮູບແບບໃຫມ່ຈະກາຍເປັນຖາວອນໃນສອງສາມອາທິດ.", + "server.row.noUsername": "ບໍ່ມີຊື່ຜູ້ໃຊ້", + "dialog.project.edit.title": "ແກ້ໄຂໂຄງການ", + "dialog.project.edit.name": "ຊື່", + "dialog.project.edit.icon": "ໄອຄອນ", + "dialog.project.edit.icon.alt": "ໄອຄອນໂຄງການ", + "dialog.project.edit.icon.hint": "ຄລິກ ຫຼື ລາກຮູບ", + "dialog.project.edit.icon.recommended": "ແນະນຳ: 128x128px", + "dialog.project.edit.color": "ສີ", + "dialog.project.edit.color.select": "ເລືອກສີ {{color}}", + "dialog.project.edit.worktree.startup": "ສະຄຣິບເລີ່ມຕົ້ນພື້ນທີ່ເຮັດວຽກ", + "dialog.project.edit.worktree.startup.description": "ເຮັດວຽກຫຼັງຈາກສ້າງພື້ນທີ່ເຮັດວຽກໃໝ່ (Git worktree).", + "dialog.project.edit.worktree.startup.placeholder": "ຕົວຢ່າງ: bun ຕິດຕັ້ງ", + "dialog.releaseNotes.action.getStarted": "ເລີ່ມຕົ້ນ", + "dialog.releaseNotes.action.next": "ຕໍ່ໄປ", + "dialog.releaseNotes.action.hideFuture": "ຢ່າສະແດງສິ່ງເຫຼົ່ານີ້ໃນອະນາຄົດ", + "dialog.releaseNotes.media.alt": "ປ່ອຍຕົວຢ່າງ", + "context.breakdown.title": "ການແບ່ງບໍລິບົດ", + "context.breakdown.note": 'ການແບ່ງຕົວໂດຍປະມານຂອງໂທເຄັນການປ້ອນຂໍ້ມູນ. "ອື່ນໆ" ປະກອບມີຄໍານິຍາມເຄື່ອງມືແລະ overhead.', + "context.breakdown.system": "ລະບົບ", + "context.breakdown.user": "ຜູ້ໃຊ້", + "context.breakdown.assistant": "ຜູ້ຊ່ວຍ", + "context.breakdown.tool": "ໂທເຄື່ອງມື", + "context.breakdown.other": "ອື່ນໆ", + "context.systemPrompt.title": "System prompt", + "context.rawMessages.title": "ຂໍ້ຄວາມດິບ", + "context.export.session": "ເຊດຊັນສົ່ງອອກ", + "context.stats.session": "ເຊດຊັນ", + "context.stats.messages": "ຂໍ້ຄວາມ", + "context.stats.provider": "ຜູ້ໃຫ້ບໍລິການ", + "context.stats.model": "ຕົວແບບ", + "context.stats.limit": "ຂອບເຂດຈໍາກັດ", + "context.stats.totalTokens": "ໂທເຄັນທັງໝົດ", + "context.stats.usage": "ການນໍາໃຊ້", + "context.stats.inputTokens": "Input Token", + "context.stats.outputTokens": "Output Token", + "context.stats.reasoningTokens": "ໂທເຄັນເຫດຜົນ", + "context.stats.cacheTokens": "Cache Token (ອ່ານ/ຂຽນ)", + "context.stats.userMessages": "ຂໍ້ຄວາມຜູ້ໃຊ້", + "context.stats.assistantMessages": "ຂໍ້ຄວາມຜູ້ຊ່ວຍ", + "context.stats.totalCost": "ຄ່າໃຊ້ຈ່າຍທັງໝົດ", + "context.stats.sessionCreated": "ສ້າງເຊດຊັນແລ້ວ", + "context.stats.lastActivity": "ການເຄື່ອນໄຫວຫຼ້າສຸດ", + "context.usage.tokens": "ໂທເຄັນ", + "context.usage.usage": "ການນໍາໃຊ້", + "context.usage.cost": "ຄ່າໃຊ້ຈ່າຍ", + "context.usage.clickToView": "ຄລິກເພື່ອເບິ່ງບໍລິບົດ", + "context.usage.view": "ເບິ່ງການໃຊ້ງານບໍລິບົດ", + "language.en": "ພາສາອັງກິດ", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "ປອກຕຸຍການ (ບຣາຊິວ)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ໄທ", + "language.tr": "Türkçe", + "toast.language.title": "ພາສາ", + "toast.language.description": "ປ່ຽນເປັນ {{language}}", + "toast.theme.title": "ປ່ຽນຮູບແບບສີສັນແລ້ວ", + "toast.scheme.title": "ຮູບແບບສີ", + "toast.workspace.enabled.title": "ພື້ນທີ່ເຮັດວຽກຖືກເປີດໃຊ້", + "toast.workspace.enabled.description": "ດຽວນີ້ Git worktree ຫຼາຍອັນສະແດງຢູ່ໃນແຖບດ້ານຂ້າງ", + "toast.workspace.disabled.title": "ພື້ນທີ່ເຮັດວຽກຖືກປິດໄວ້", + "toast.workspace.disabled.description": "ສະແດງສະເພາະ Git worktree ຫຼັກໃນແຖບດ້ານຂ້າງ", + "toast.permissions.autoaccept.on.title": "ການອະນຸຍາດອັດຕະໂນມັດ", + "toast.permissions.autoaccept.on.description": "ການຮ້ອງຂໍການອະນຸຍາດຈະຖືກອະນຸມັດໂດຍອັດຕະໂນມັດ", + "toast.permissions.autoaccept.off.title": "ຢຸດການອະນຸຍາດອັດຕະໂນມັດ", + "toast.permissions.autoaccept.off.description": "ການຮ້ອງຂໍການອະນຸຍາດຈະຕ້ອງມີການອະນຸມັດ", + "toast.model.none.title": "ບໍ່ໄດ້ເລືອກຕົວແບບ", + "toast.model.none.description": "ເຊື່ອມຕໍ່ຜູ້ໃຫ້ບໍລິການເພື່ອສະຫຼຸບກອງປະຊຸມນີ້", + "toast.file.loadFailed.title": "ລົ້ມເຫລວໃນການໂຫຼດໄຟລ໌", + "toast.file.listFailed.title": "ລົ້ມເຫລວໃນລາຍການໄຟລ໌", + "toast.context.noLineSelection.title": "ບໍ່ມີການເລືອກແຖວ", + "toast.context.noLineSelection.description": "ເລືອກໄລຍະແຖວໃນແຖບໄຟລ໌ກ່ອນ.", + "toast.session.share.copyFailed.title": "ລົ້ມເຫລວໃນການສຳເນົາ URL ໄປໃສ່ຄລິບບອດ", + "toast.session.share.success.title": "ແບ່ງປັນເຊດຊັນ", + "toast.session.share.success.description": "ແບ່ງປັນ URL ທີ່ສຳເນົາໄວ້ໃນຄລິບບອດແລ້ວ!", + "toast.session.share.failed.title": "ລົ້ມເຫລວໃນການແບ່ງປັນເຊດຊັນ", + "toast.session.share.failed.description": "ເກີດຄວາມຜິດພາດໃນຂະນະທີ່ແບ່ງປັນເຊດຊັນ", + "toast.session.unshare.success.title": "ເຊົາແບ່ງປັນເຊດຊັນແລ້ວ", + "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": "ມີອັບເດດ", + "toast.update.description": "ເວີຊັນໃໝ່ຂອງ OpenCode ({{version}}) ມີໃຫ້ຕິດຕັ້ງແລ້ວ.", + "toast.update.action.installRestart": "ຕິດຕັ້ງ ແລະປິດເປີດໃໝ່", + "toast.update.action.notYet": "ຍັງບໍ່ທັນ", + "error.page.title": "ມີບາງຢ່າງຜິດພາດ", + "error.page.description": "ເກີດຄວາມຜິດພາດໃນລະຫວ່າງການໂຫລດແອັບພລິເຄຊັນ.", + "error.page.description.localServerStartup": "ເກີດຄວາມຜິດພາດຂຶ້ນໃນຂະນະທີ່ເລີ່ມເຊີບເວີທ້ອງຖິ່ນ.", + "error.page.details.label": "ລາຍລະອຽດຂໍ້ຜິດພາດ", + "error.page.action.restart": "ຣີສະຕາດ", + "error.page.action.report": "ລາຍງານຄວາມຜິດພາດ", + "error.page.action.reported": "ລາຍງານຂໍ້ຜິດພາດ", + "error.page.action.exportLogs": "ບັນທຶກການສົ່ງອອກ", + "error.page.action.checking": "ກຳລັງກວດສອບ...", + "error.page.action.checkUpdates": "ກວດສອບການອັບເດດ", + "error.page.action.updateTo": "ອັບເດດເປັນ {{version}}", + "error.page.circular": "[Circular]", + "error.page.report.prefix": "ກະລຸນາລາຍງານຄວາມຜິດພາດນີ້ໃຫ້ທີມງານ OpenCode", + "error.page.report.discord": "ໃນ Discord", + "error.page.version": "ລຸ້ນ: {{version}}", + "error.dev.rootNotFound": + "ບໍ່ພົບອົງປະກອບຮາກ. ທ່ານລືມເພີ່ມມັນໃສ່ index.html ຂອງທ່ານບໍ? ຫຼືບາງທີຄຸນສົມບັດ id ມີການສະກົດຜິດ?", + "error.serverSync.connectFailed": "ບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເຊີບເວີໄດ້. ມີເຊີບເວີທີ່ເຮັດວຽກຢູ່ `{{url}}` ບໍ?", + "error.serverSDK.noServerAvailable": "ບໍ່ມີເຊີບເວີ", + "error.serverSDK.serverNotAvailable": "ບໍ່ມີເຊີບເວີ", + "error.childStore.persistedCacheCreateFailed": "ລົ້ມເຫລວໃນການສ້າງແຄດທີ່ຍັງຄົງຄ້າງ", + "error.childStore.persistedProjectMetadataCreateFailed": "ລົ້ມເຫລວໃນການສ້າງ metadata ຂອງໂຄງການທີ່ຍັງຄົງຄ້າງ", + "error.childStore.persistedProjectIconCreateFailed": "ລົ້ມເຫລວໃນການສ້າງໄອຄອນໂຄງການທີ່ຍັງຄົງຄ້າງ", + "error.childStore.storeCreateFailed": "ລົ້ມເຫລວໃນການສ້າງຮ້ານ", + "directory.error.invalidUrl": "ໄດເລກະທໍລີບໍ່ຖືກຕ້ອງໃນ URL.", + "error.chain.unknown": "ຄວາມຜິດພາດທີ່ບໍ່ຮູ້ຈັກ", + "error.server.invalidConfiguration": "ການຕັ້ງຄ່າບໍ່ຖືກຕ້ອງ", + "error.chain.causedBy": "ເກີດຈາກ:", + "error.chain.apiError": "API ຜິດພາດ", + "error.chain.status": "ສະຖານະ: {{status}}", + "error.chain.retryable": "ລອງໃຊ້ໃໝ່ໄດ້: {{retryable}}", + "error.chain.responseBody": "ເນື້ອໃນການຕອບສະໜອງ:\n{{body}}", + "error.chain.didYouMean": "ເຈົ້າຫມາຍຄວາມວ່າ: {{suggestions}}", + "error.chain.modelNotFound": "ບໍ່ພົບໂມເດວ: {{provider}}/{{model}}", + "error.chain.checkConfig": "ກວດເບິ່ງການຕັ້ງຄ່າ (opencode.json) ຜູ້ໃຫ້ບໍລິການ/ຊື່ແບບຈໍາລອງຂອງທ່ານ", + "error.chain.mcpFailed": 'ເຊີບເວີ MCP "{{name}}" ລົ້ມເຫລວ. ໝາຍເຫດ, OpenCode ບໍ່ຮອງຮັບການພິສູດຢືນຢັນ MCP ເທື່ອ.', + "error.chain.providerAuthFailed": "ການພິສູດຢືນຢັນຜູ້ໃຫ້ບໍລິການລົ້ມເຫລວ ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'ລົ້ມເຫລວໃນການເລີ່ມຕົ້ນຜູ້ໃຫ້ບໍລິການ "{{provider}}". ກວດເບິ່ງຂໍ້ມູນປະຈໍາຕົວແລະການຕັ້ງຄ່າ.', + "error.chain.configJsonInvalid": "ໄຟລ໌ Config ຢູ່ {{path}} ບໍ່ຖືກຕ້ອງ JSON(C)", + "error.chain.configJsonInvalidWithMessage": "ໄຟລ໌ Config ຢູ່ {{path}} ບໍ່ຖືກຕ້ອງ JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Directory "{{dir}}" ໃນ {{path}} ບໍ່ຖືກຕ້ອງ. ປ່ຽນຊື່ໄດເລກະທໍລີເປັນ "{{suggestion}}" ຫຼືເອົາມັນອອກ. ນີ້ແມ່ນການພິມຜິດທົ່ວໄປ.', + "error.chain.configFrontmatterError": "ລົ້ມເຫລວໃນການວິເຄາະເລື່ອງໜ້າໃນ {{path}}:\n{{message}}", + "error.chain.configInvalid": "ໄຟລ໌ Config ຢູ່ {{path}} ບໍ່ຖືກຕ້ອງ", + "error.chain.configInvalidWithMessage": "ໄຟລ໌ Config ຢູ່ {{path}} ບໍ່ຖືກຕ້ອງ: {{message}}", + "notification.permission.title": "ຕ້ອງມີການອະນຸຍາດ", + "notification.permission.description": "{{sessionTitle}} ໃນ {{projectName}} ຕ້ອງການການອະນຸຍາດ", + "notification.question.title": "ຄໍາຖາມ", + "notification.question.description": "{{sessionTitle}} ໃນ {{projectName}} ມີຄໍາຖາມ", + "notification.action.goToSession": "ໄປທີ່ເຊດຊັນ", + "notification.session.responseReady.title": "ການຕອບສະໜອງພ້ອມແລ້ວ", + "notification.session.error.title": "ເຊດຊັນຜິດພາດ", + "notification.session.error.fallbackDescription": "ເກີດຄວາມຜິດພາດຂຶ້ນ", + "home.recentProjects": "ໂຄງການທີ່ຜ່ານມາ", + "home.empty.title": "ບໍ່ມີໂຄງການທີ່ຜ່ານມາ", + "home.empty.description": "ເລີ່ມຕົ້ນໂດຍການເປີດໂຄງການທ້ອງຖິ່ນ", + "home.title": "ໜ້າຫຼັກ", + "home.projects": "ໂຄງການ", + "home.project.add": "ເພີ່ມໂຄງການ", + "home.recentlyClosed": "ປິດບໍ່ດົນມານີ້", + "home.server.collapse": "ຍຸບໂຄງການເຊີບເວີ", + "home.server.expand": "ຂະຫຍາຍໂຄງການເຊີບເວີ", + "home.sessions.search.placeholder": "ເຊດຊັນຄົ້ນຫາ", + "home.sessions.search.placeholder.scoped": "ຄົ້ນຫາເຊດຊັນໃນ {{scope}}", + "home.sessions.search.sessions": "ເຊດຊັນ", + "home.sessions.search.noResults": "ບໍ່ພົບເຊດຊັນສຳລັບ {{query}}", + "home.sessions.empty": "ບໍ່ມີຫຍັງຢູ່ທີ່ນີ້ເທື່ອ", + "home.sessions.empty.description": "ສ້າງເຊດຊັນເພື່ອເລີ່ມຕົ້ນ", + "home.sessions.group.today": "ມື້ນີ້", + "home.sessions.group.yesterday": "ມື້ວານນີ້", + "home.sessions.group.older": "ເກົ່າ", + "home.providerTip": "ເຊື່ອມຕໍ່ກັບຜູ້ໃຫ້ບໍລິການ 75+ ເພື່ອໃຊ້ຮູບແບບອື່ນໆ, ລວມທັງ Claude, GPT, Gemini, ແລະອື່ນໆ.", + "session.tab.session": "ເຊດຊັນ", + "session.tab.review": "ການທົບທວນຄືນ", + "session.tab.context": "ບໍລິບົດ", + "session.tab.unknown": "ເຊດຊັນທີ່ບໍ່ຮູ້ຈັກ", + "session.panel.reviewAndFiles": "ການທົບທວນຄືນແລະໄຟລ໌", + "session.error.notFound": "ບໍ່ພົບເຊດຊັນນີ້", + "session.error.notFound.description": "ແຖບນີ້ຊີ້ໄປຫາເຊດຊັນທີ່ບໍ່ມີຢູ່ໃນເຊີບເວີນີ້ແລ້ວ.", + "session.error.notFound.closeTab": "ປິດແຖບ", + "session.error.serverConnection": "ບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເຊີບເວີນີ້ໄດ້", + "session.review.filesChanged": "ໄຟລ໌ປ່ຽນ {{count}}", + "session.review.change.one": "ປ່ຽນແປງ", + "session.review.change.other": "ການປ່ຽນແປງ", + "session.review.loadingChanges": "ກຳລັງໂຫຼດການປ່ຽນແປງ...", + "session.review.empty": "ບໍ່ມີການປ່ຽນແປງໃນເຊດຊັນນີ້ເທື່ອ", + "session.review.noVcs": "ບໍ່ພົບລະບົບການຄວບຄຸມເວີຊັນ Git, ການປ່ຽນແປງບໍ່ສະແດງ", + "session.review.noVcs.createGit.title": "ສ້າງບ່ອນເກັບຂໍ້ມູນ Git", + "session.review.noVcs.createGit.description": "ຕິດຕາມ, ທົບທວນ, ແລະຍົກເລີກການປ່ຽນແປງໃນໂຄງການນີ້", + "session.review.noVcs.createGit.actionLoading": "ກຳລັງສ້າງບ່ອນເກັບຂໍ້ມູນ Git...", + "session.review.noVcs.createGit.action": "ສ້າງບ່ອນເກັບຂໍ້ມູນ Git", + "session.review.noSnapshot": "ການຕິດຕາມພາບຖ່າຍຖືກປິດໃຊ້ງານຢູ່ໃນການຕັ້ງຄ່າ, ດັ່ງນັ້ນການປ່ຽນແປງເຊດຊັນບໍ່ສາມາດໃຊ້ໄດ້", + "session.review.noChanges": "ບໍ່ມີການປ່ຽນແປງ", + "session.review.noUncommittedChanges": "ບໍ່ມີການປ່ຽນແປງທີ່ບໍ່ໄດ້ຕົກລົງເທື່ອ", + "session.review.noBranchChanges": "ບໍ່ມີການປ່ຽນແປງສາຂາເທື່ອ", + "session.files.selectToOpen": "ເລືອກໄຟລ໌ທີ່ຈະເປີດ", + "session.files.all": "ໄຟລ໌ທັງໝົດ", + "session.files.empty": "ບໍ່ມີໄຟລ໌", + "session.files.binaryContent": "ໄຟລ໌ຖານສອງ (ເນື້ອຫາບໍ່ສາມາດສະແດງໄດ້)", + "session.messages.renderEarlier": "ສະແດງຂໍ້ຄວາມກ່ອນໜ້ານີ້", + "session.messages.loadingEarlier": "ກຳລັງໂຫຼດຂໍ້ຄວາມກ່ອນໜ້ານີ້...", + "session.messages.loadEarlier": "ໂຫຼດຂໍ້ຄວາມກ່ອນໜ້ານີ້", + "session.messages.loading": "ກຳລັງໂຫຼດຂໍ້ຄວາມ...", + "session.messages.jumpToLatest": "ຂ້າມໄປຫາຫຼ້າສຸດ", + "session.context.addToContext": "ເພີ່ມ {{selection}} ໃສ່ບໍລິບົດ", + "session.todo.title": "ສິ່ງທີ່ຕ້ອງເຮັດ", + "session.todo.collapse": "ຫຍໍ້ລົງ", + "session.todo.expand": "ຂະຫຍາຍ", + "session.todo.progress": "{{done}} ຂອງ {{total}} todos ສຳເລັດແລ້ວ", + "session.question.progress": "{{current}} ຂອງ {{total}} ຄໍາຖາມ", + "session.question.minimize": "ຫຍໍ້ຄໍາຖາມ", + "session.question.restore": "ຟື້ນຟູຄໍາຖາມ", + "session.question.pending.one": "{{count}} ຄຳຖາມທີ່ຍັງຄ້າງຢູ່", + "session.question.pending.other": "{{count}} ຄຳຖາມທີ່ຍັງຄ້າງຢູ່", + "session.followupDock.summary.one": "{{count}} ຄິວຂໍ້ຄວາມ", + "session.followupDock.summary.other": "{{count}} ຄິວຂໍ້ຄວາມ", + "session.followupDock.sendNow": "ສົ່ງດຽວນີ້", + "session.followupDock.edit": "ແກ້ໄຂ", + "session.followupDock.collapse": "ຫຍໍ້ຂໍ້ຄວາມທີ່ຢູ່ໃນຄິວລົງ", + "session.followupDock.expand": "ຂະຫຍາຍຂໍ້ຄວາມທີ່ຢູ່ໃນຄິວ", + "session.revertDock.summary.one": "{{count}} ມ້ວນຂໍ້ຄວາມກັບຄືນ", + "session.revertDock.summary.other": "{{count}} ມ້ວນຂໍ້ຄວາມກັບຄືນ", + "session.revertDock.collapse": "ຫຍໍ້ຂໍ້ຄວາມທີ່ມ້ວນຄືນ", + "session.revertDock.expand": "ຂະຫຍາຍຂໍ້ຄວາມທີ່ມ້ວນຄືນ", + "session.revertDock.restore": "ຟື້ນຟູຂໍ້ຄວາມ", + "session.new.title": "ສ້າງສິ່ງໃດ", + "session.new.project.new": "ໂຄງການໃໝ່", + "session.new.project.search": "ໂຄງການຄົ້ນຫາ", + "session.new.project.add": "ເພີ່ມໂຄງການ", + "session.new.worktree.main": "ສາຂາຫຼັກ", + "session.new.worktree.mainWithBranch": "ສາຂາຫຼັກ ({{branch}})", + "session.new.worktree.create": "ສ້າງ Git worktree ໃໝ່", + "session.new.workspace.runIn": "ແລ່ນເຊສຊັນໃນ", + "session.new.workspace.triggerLocal": "ທ້ອງຖິ່ນ", + "session.new.workspace.local": "ພື້ນທີ່ເກັບຂໍ້ມູນທ້ອງຖິ່ນ", + "session.new.workspace.existing": "ພື້ນທີ່ເຮັດວຽກ...", + "session.new.git.none": "ບໍ່ມີ Git", + "session.new.lastModified": "ດັດແກ້ຫຼ້າສຸດ", + "session.header.search.placeholder": "ຄົ້ນຫາ {{project}}", + "session.header.searchFiles": "ຊອກຫາໄຟລ໌", + "session.header.openIn": "ເປີດໃນ", + "session.header.open.action": "ເປີດ {{app}}", + "session.header.open.ariaLabel": "ເປີດໃນ {{app}}", + "session.header.open.menu": "ເປີດທາງເລືອກ", + "session.header.open.copyPath": "ສຳເນົາເສັ້ນທາງ", + "session.header.open.finder": "ຊອກຫາ", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "ຜູ້ຈັດການໄຟລ໌", + "session.header.reveal.finder": "ເປີດເຜີຍໃນ Finder", + "session.header.reveal.fileExplorer": "ເປີດເຜີຍໃນ File Explorer", + "session.header.reveal.containingFolder": "ເປີດໂຟນເດີທີ່ບັນຈຸ", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "ສະຖານະ", + "status.popover.ariaLabel": "ການຕັ້ງຄ່າເຊີບເວີ", + "status.popover.tab.servers": "ເຊີບເວີ", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "ປລັກອິນ", + "status.popover.action.manageServers": "ຈັດການເຊີບເວີ", + "session.share.popover.title": "ເຜີຍແຜ່ໃນເວັບ", + "session.share.popover.description.shared": "ເຊດຊັນນີ້ແມ່ນສາທາລະນະໃນເວັບ. ມັນສາມາດເຂົ້າເຖິງທຸກຄົນທີ່ມີການເຊື່ອມຕໍ່.", + "session.share.popover.description.unshared": + "ແບ່ງປັນເຊດຊັນສາທາລະນະໃນເວັບ. ມັນຈະສາມາດເຂົ້າເຖິງທຸກຄົນທີ່ມີການເຊື່ອມຕໍ່.", + "session.share.action.share": "ແບ່ງປັນ", + "session.share.action.publish": "ເຜີຍແຜ່", + "session.share.action.publishing": "ກຳລັງເຜີຍແຜ່...", + "session.share.action.unpublish": "ຍົກເລີກການເຜີຍແຜ່", + "session.share.action.unpublishing": "ກຳລັງຍົກເລີກການເຜີຍແຜ່...", + "session.share.action.view": "ເບິ່ງ", + "session.share.copy.copied": "ສຳເນົາແລ້ວ", + "session.share.copy.copyLink": "ສຳເນົາລິ້ງ", + "lsp.tooltip.none": "ບໍ່ມີເຊີບເວີ LSP", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "ກຳລັງໂຫຼດຂໍ້ຄວາມ...", + "terminal.loading": "ກຳລັງໂຫຼດເຄື່ອງໃຊ້...", + "terminal.title": "ສະຖານີ", + "terminal.title.numbered": "ສະຖານີ {{number}}", + "terminal.close": "ປິດ terminal", + "terminal.connectionLost.title": "ສູນເສຍການເຊື່ອມຕໍ່", + "terminal.connectionLost.abnormalClose": "WebSocket ປິດຢ່າງຜິດປົກກະຕິ: {{code}}", + "terminal.connectionLost.description": + "ການເຊື່ອມຕໍ່ຢູ່ປາຍຍອດຖືກຂັດຈັງຫວະ. ອັນນີ້ສາມາດເກີດຂຶ້ນໄດ້ເມື່ອເຊີບເວີຣີສະຕາດ.", + "terminal.connectTicket.csrfError": + "ປີ້ເຊື່ອມຕໍ່ PTY ຖືກປະຕິເສດໂດຍການກວດສອບຕົ້ນທາງ ຫຼື CSRF. ກວດເບິ່ງເຊີບເວີ CORS config.", + "terminal.connectTicket.statusError": "ປີ້ເຊື່ອມຕໍ່ PTY ລົ້ມເຫລວກັບ {{status}}", + "titlebar.update": "ອັບເດດ", + "titlebar.updateVersion": "ອັບເດດ {{version}}", + "common.closeTab": "ປິດແຖບ", + "common.dismiss": "ປິດ", + "common.moreCountSuffix": "(+{{count}} ເພີ່ມເຕີມ)", + "common.requestFailed": "ການຮ້ອງຂໍລົ້ມເຫລວ", + "common.moreOptions": "ທາງເລືອກເພີ່ມເຕີມ", + "common.learnMore": "ສຶກສາເພີ່ມເຕີມ", + "common.rename": "ປ່ຽນຊື່", + "common.export": "ສົ່ງອອກ", + "common.reset": "ຣີເຊັດ", + "common.archive": "ຮວບຮວມ", + "common.delete": "ລຶບ", + "common.close": "ປິດ", + "common.edit": "ແກ້ໄຂ", + "common.loadMore": "ໂຫຼດເພີ່ມເຕີມ", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "ບໍ່ຮູ້ຈັກ", + "common.time.justNow": "ດຽວນີ້", + "common.time.minutesAgo.short": "{{count}}m ກ່ອນໜ້ານີ້", + "common.time.hoursAgo.short": "{{count}}h ກ່ອນຫນ້ານີ້", + "common.time.daysAgo.short": "{{count}}d ກ່ອນຫນ້ານີ້", + "sidebar.menu.toggle": "ສະຫຼັບເມນູ", + "sidebar.nav.projectsAndSessions": "ໂຄງການ ແລະກອງປະຊຸມ", + "sidebar.settings": "ການຕັ້ງຄ່າ", + "sidebar.help": "ຊ່ວຍ", + "sidebar.workspaces.enable": "ເປີດໃຊ້ພື້ນທີ່ເຮັດວຽກ", + "sidebar.workspaces.disable": "ປິດການໃຊ້ງານພື້ນທີ່ເຮັດວຽກ", + "sidebar.gettingStarted.title": "ການເລີ່ມຕົ້ນ", + "sidebar.gettingStarted.line1": "OpenCode ປະກອບມີຮູບແບບຟຣີເພື່ອໃຫ້ທ່ານສາມາດເລີ່ມຕົ້ນໄດ້ທັນທີ.", + "sidebar.gettingStarted.line2": "ເຊື່ອມຕໍ່ຜູ້ໃຫ້ບໍລິການໃດນຶ່ງເພື່ອໃຊ້ແບບຈໍາລອງ, inc. Claude, GPT, Gemini ແລະອື່ນໆ.", + "sidebar.project.recentSessions": "ເຊດຊັນທີ່ຜ່ານມາ", + "sidebar.project.viewAllSessions": "ເບິ່ງເຊດຊັນທັງໝົດ", + "sidebar.project.clearNotifications": "ລຶບການແຈ້ງເຕືອນ", + "sidebar.empty.title": "ບໍ່ມີໂຄງການເປີດ", + "sidebar.empty.description": "ເປີດໂຄງການເພື່ອເລີ່ມຕົ້ນ", + "debugBar.ariaLabel": "ການວິນິດໄສປະສິດທິພາບການພັດທະນາ", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "ການປ່ຽນເສັ້ນທາງທີ່ສຳເລັດຫຼ້າສຸດໂດຍແຕະໃສ່ໜ້າເຊສຊັນ, ວັດແທກຈາກການເລີ່ມຕົ້ນຂອງເຣົາເຕີຈົນຮອດສີທຳອິດຫຼັງຈາກທີ່ມັນຕົກລົງ.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "ເຟຣມມ້ວນຕໍ່ວິນາທີໃນໄລຍະ 5 ວິນາທີທີ່ຜ່ານມາ.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "ເວລາເຟຣມຮ້າຍແຮງທີ່ສຸດໃນຮອບ 5 ວິນາທີທີ່ຜ່ານມາ.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "ເຟຣມຫຼາຍກວ່າ 32ms ໃນ 5 ວິນາທີສຸດທ້າຍ.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "ເວລາທີ່ຖືກບລັອກ ແລະນັບວຽກຍາວໃນ 5 ວິນາທີສຸດທ້າຍ. ໜ້າວຽກສູງສຸດ: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "ການຊັກຊ້າການປ້ອນຂໍ້ມູນທີ່ສັງເກດເຫັນຮ້າຍແຮງທີ່ສຸດໃນ 5 ວິນາທີທີ່ຜ່ານມາ.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "ໄລຍະເວລາການໂຕ້ຕອບໂດຍປະມານໃນໄລຍະ 5 ວິນາທີທີ່ຜ່ານມາ. ນີ້ແມ່ນຄ້າຍຄື INP, ບໍ່ແມ່ນ Web Vitals INP ທີ່ເປັນທາງການ.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "ການປ່ຽນຮູບແບບສະສົມສຳລັບອາຍຸຂອງແອັບປັດຈຸບັນ.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "ໃຊ້ JS heap vs heap limit. Chromium ເທົ່ານັ້ນ.", + "debugBar.mem.tip": "ໃຊ້ JS heap ທຽບກັບ heap limit. {{used}} ຂອງ {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "ບັງຄັບຮູບແບບຈຸດສຸມຢູ່ໃນອົງປະກອບການໂຕ້ຕອບທັງໝົດ", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "ບັງຄັບທິດທາງການຈັດວາງແອັບເຕັມໂດຍບໍ່ປ່ຽນພາສາທີ່ເລືອກ", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "ເດັສທັອບ", + "settings.section.server": "ເຊີບເວີ", + "settings.tab.general": "ທົ່ວໄປ", + "settings.tab.shortcuts": "ທາງລັດ", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "ການເຊື່ອມໂຍງ WSL", + "settings.desktop.wsl.description": "ແລ່ນເຊີບເວີ OpenCode ພາຍໃນ WSL ໃນ Windows.", + "settings.general.section.appearance": "ຮູບລັກສະນະ", + "settings.general.section.advanced": "ຂັ້ນສູງ", + "settings.general.section.notifications": "ການແຈ້ງເຕືອນລະບົບ", + "settings.general.section.updates": "ອັບເດດ", + "settings.general.section.sounds": "ເອັບເຟັກສຽງ", + "settings.general.section.feed": "ອາຫານ", + "settings.general.section.display": "ຈໍສະແດງຜົນ", + "settings.general.row.language.title": "ພາສາ", + "settings.general.row.language.description": "ປ່ຽນພາສາສະແດງສໍາລັບ OpenCode", + "settings.general.row.shell.title": "Shell ຂອງເທີມິນອນ", + "settings.general.row.shell.description": "Shell ທີ່ໃຊ້ໂດຍເຄື່ອງໃຊ້ປາຍທາງ ແລະຕົວແທນ", + "settings.general.row.shell.autoDefault": "ອັດຕະໂນມັດ (ຄ່າເລີ່ມຕົ້ນ)", + "settings.general.row.shell.terminalOnly": "terminal ເທົ່ານັ້ນ", + "settings.general.row.appearance.title": "ຮູບລັກສະນະ", + "settings.general.row.appearance.description": "ປັບແຕ່ງວິທີ OpenCode ເບິ່ງຢູ່ໃນອຸປະກອນຂອງທ່ານ", + "settings.general.row.colorScheme.title": "ຮູບແບບສີ", + "settings.general.row.colorScheme.description": "ເລືອກວ່າ OpenCode ປະຕິບັດຕາມລະບົບ, ແສງສະຫວ່າງ, ຫຼືຮູບແບບຊ້ໍາ", + "settings.general.row.theme.title": "ຫົວຂໍ້", + "settings.general.row.theme.description": "ປັບແຕ່ງວິທີ OpenCode ເປັນຫົວຂໍ້.", + "settings.general.row.font.title": "ຕົວອັກສອນລະຫັດ", + "settings.general.row.font.description": "ປັບແຕ່ງຕົວອັກສອນທີ່ໃຊ້ໃນບລັອກລະຫັດ", + "settings.general.row.terminalFont.title": "ຕົວອັກສອນປາຍ", + "settings.general.row.terminalFont.description": "ປັບແຕ່ງຟອນທີ່ໃຊ້ຢູ່ໃນເຄື່ອງໝາຍປາຍທາງ", + "settings.general.row.uiFont.title": "ຟອນ UI", + "settings.general.row.uiFont.description": "ປັບແຕ່ງຟອນທີ່ໃຊ້ໃນທົ່ວອິນເຕີເຟດ", + "settings.general.row.followup.title": "ພຶດຕິກໍາການຕິດຕາມ", + "settings.general.row.followup.description": "ເລືອກວ່າການຕິດຕາມການກະຕຸ້ນເຕືອນທັນທີຫຼືລໍຖ້າຢູ່ໃນຄິວ", + "settings.general.row.followup.option.queue": "ຄິວ", + "settings.general.row.followup.option.steer": "ຊີ້ນໍາ", + "settings.general.row.showFileTree.title": "ຕົ້ນໄມ້ໄຟລ໌", + "settings.general.row.showFileTree.description": "ສະແດງແຖບຕົ້ນໄມ້ໄຟລ໌ໃນເຊດຊັນຕ່າງໆ", + "settings.general.row.showNavigation.title": "ການຄວບຄຸມການນໍາທາງ", + "settings.general.row.showNavigation.description": "ສະແດງປຸ່ມກັບຄືນ ແລະສົ່ງຕໍ່ໃນແຖບຫົວຂໍ້ desktop", + "settings.general.row.showSearch.title": "ແຖບຄໍາສັ່ງ", + "settings.general.row.showSearch.description": "ສະແດງປຸ່ມ palette ຄົ້ນຫາ ແລະຄໍາສັ່ງຢູ່ໃນແຖບຫົວຂໍ້", + "settings.general.row.showTerminal.title": "ສະຖານີ", + "settings.general.row.showTerminal.description": "ສະແດງປຸ່ມ terminal ໃນແຖບຫົວຂໍ້ desktop", + "settings.general.row.showStatus.title": "ສະຖານະເຊີບເວີ", + "settings.general.row.showStatus.description": "ສະແດງປຸ່ມສະຖານະເຊີບເວີໃນແຖບຫົວຂໍ້", + "settings.general.row.mobileTitlebarBottom.title": "ການນໍາທາງລຸ່ມ", + "settings.general.row.mobileTitlebarBottom.description": "ວາງແຖບຫົວຂໍ້ ແລະແຖບເຊດຊັນຢູ່ລຸ່ມສຸດຂອງໜ້າຈໍໃນມືຖື", + "settings.general.row.showCustomAgents.title": "ສະແດງຕົວແທນ", + "settings.general.row.showCustomAgents.description": + "ສະຫຼັບລະຫວ່າງຕົວແທນໃນຕົວປະກອບ. ເມື່ອຖືກເຊື່ອງໄວ້, ເລີ່ມຕົ້ນທີ່ຈະສ້າງຕົວແທນ.", + "settings.general.row.reasoningSummaries.title": "ສະແດງການສັງລວມເຫດຜົນ", + "settings.general.row.reasoningSummaries.description": "ສະແດງສະຫຼຸບເຫດຜົນຂອງຕົວແບບໃນທາມລາຍ", + "settings.general.row.shellToolPartsExpanded.title": "ຂະຫຍາຍສ່ວນຂອງເຄື່ອງມື Shell", + "settings.general.row.shellToolPartsExpanded.description": + "ສະແດງສ່ວນຂອງເຄື່ອງມື Shell ແບບຂະຫຍາຍເປັນຄ່າເລີ່ມຕົ້ນໃນໄທມ໌ລາຍ", + "settings.general.row.editToolPartsExpanded.title": "ຂະຫຍາຍພາກສ່ວນເຄື່ອງມືແກ້ໄຂ", + "settings.general.row.editToolPartsExpanded.description": + "ສະ​ແດງ​ການ​ແກ້​ໄຂ, ຂຽນ, ແລະ​ການ​ແກ້​ໄຂ​ສ່ວນ​ເຄື່ອງ​ມື​ທີ່​ຂະ​ຫຍາຍ​ໂດຍ​ຄ່າ​ເລີ່ມ​ຕົ້ນ​ໃນ​ໄລ​ຍະ​ເວ​ລາ", + "settings.general.row.newInterface.title": "ໂຄງຮ່າງໃໝ່", + "settings.general.row.newInterface.badge": "ໃໝ່", + "settings.general.row.newInterface.description": "ໃຊ້ແຖບໃໝ່ ແລະຮູບແບບໜ້າຫຼັກ. ສະຫຼັບລະຫວ່າງໂຄງຮ່າງເປັນເວລາຈຳກັດ.", + "settings.general.row.newInterfaceNotice.title": "ຕອນນີ້ທ່ານກຳລັງໃຊ້ໂຄງຮ່າງໃໝ່", + "settings.general.row.newInterfaceNotice.description": "ຮູບແບບກ່ອນໜ້ານີ້ບໍ່ມີແລ້ວ", + "settings.general.row.newInterfaceNotice.dismiss": "ປິດ", + "settings.general.row.pinchZoom.title": "ບີບເພື່ອຊູມ", + "settings.general.row.pinchZoom.description": "ອະ​ນຸ​ຍາດ​ໃຫ້​ການ pinch trackpad ແລະ gestures Ctrl-scroll ເພື່ອຊູມ", + "settings.general.row.wayland.title": "ໃຊ້ Wayland ພື້ນເມືອງ", + "settings.general.row.wayland.description": "ປິດການໃຊ້ງານ X11 fallback ໃນ Wayland. ຕ້ອງການຣີສະຕາດ.", + "settings.general.row.wayland.tooltip": + "ໃນ Linux ທີ່ມີຈໍພາບອັດຕາການໂຫຼດຫນ້າຈໍຄືນແບບປະສົມ, Wayland ພື້ນເມືອງສາມາດມີຄວາມຫມັ້ນຄົງຫຼາຍ.", + "settings.general.row.releaseNotes.title": "ບັນທຶກການປ່ອຍ", + "settings.general.row.releaseNotes.description": "ສະແດງປັອບອັບໃໝ່ແມ່ນຫຍັງຫຼັງຈາກອັບເດດ", + "settings.updates.row.startup.title": "ກວດເບິ່ງການອັບເດດກ່ຽວກັບການເລີ່ມຕົ້ນ", + "settings.updates.row.startup.description": "ກວດສອບການອັບເດດໂດຍອັດຕະໂນມັດເມື່ອ OpenCode ເປີດ", + "settings.updates.row.check.title": "ກວດສອບການອັບເດດ", + "settings.updates.row.check.description": "ກວດສອບການອັບເດດດ້ວຍຕົນເອງ ແລະຕິດຕັ້ງຖ້າມີ", + "settings.updates.action.checkNow": "ກວດເບິ່ງດຽວນີ້", + "settings.updates.action.checking": "ກຳລັງກວດສອບ...", + "settings.updates.action.downloading": "ກຳລັງດາວໂຫຼດ...", + "settings.updates.action.installing": "ກຳລັງຕິດຕັ້ງ...", + "settings.updates.toast.latest.title": "ທ່ານອັບເດດແລ້ວ", + "settings.updates.toast.latest.description": "ທ່ານກຳລັງໃຊ້ OpenCode ເວີຊັນຫຼ້າສຸດ.", + "sound.option.none": "ບໍ່ມີ", + "sound.option.alert01": "ແຈ້ງເຕືອນ 01", + "sound.option.alert02": "ແຈ້ງເຕືອນ 02", + "sound.option.alert03": "ແຈ້ງເຕືອນ 03", + "sound.option.alert04": "ແຈ້ງເຕືອນ 04", + "sound.option.alert05": "ແຈ້ງເຕືອນ 05", + "sound.option.alert06": "ແຈ້ງເຕືອນ 06", + "sound.option.alert07": "ແຈ້ງເຕືອນ 07", + "sound.option.alert08": "ແຈ້ງເຕືອນ 08", + "sound.option.alert09": "ແຈ້ງເຕືອນ 09", + "sound.option.alert10": "ແຈ້ງເຕືອນ 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "ບິບໂບ 02", + "sound.option.bipbop03": "ບິບໂບ 03", + "sound.option.bipbop04": "ບິບໂບ 04", + "sound.option.bipbop05": "ບິບໂບ 05", + "sound.option.bipbop06": "ບິບໂບ 06", + "sound.option.bipbop07": "ບິບໂບ 07", + "sound.option.bipbop08": "ບຸນບັ້ງໄຟ 08", + "sound.option.bipbop09": "ບິບໂບ 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "ບໍ່ 01", + "sound.option.nope02": "ບໍ່ 02", + "sound.option.nope03": "ບໍ່ 03", + "sound.option.nope04": "ບໍ່ 04", + "sound.option.nope05": "ບໍ່ 05", + "sound.option.nope06": "ບໍ່ 06", + "sound.option.nope07": "ບໍ່ 07", + "sound.option.nope08": "ບໍ່ 08", + "sound.option.nope09": "ບໍ່ 09", + "sound.option.nope10": "ບໍ່ 10", + "sound.option.nope11": "ບໍ່ 11", + "sound.option.nope12": "ບໍ່ 12", + "sound.option.yup01": "ແມ່ນແລ້ວ 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "ແມ່ນແລ້ວ 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "ຕົວແທນ", + "settings.general.notifications.agent.description": + "ສະແດງການແຈ້ງເຕືອນລະບົບເມື່ອຕົວແທນຄົບຖ້ວນສົມບູນ ຫຼືຕ້ອງການຄວາມສົນໃຈ", + "settings.general.notifications.permissions.title": "ການອະນຸຍາດ", + "settings.general.notifications.permissions.description": "ສະແດງການແຈ້ງເຕືອນລະບົບເມື່ອຕ້ອງການການອະນຸຍາດ", + "settings.general.notifications.errors.title": "ຄວາມຜິດພາດ", + "settings.general.notifications.errors.description": "ສະແດງການແຈ້ງເຕືອນລະບົບເມື່ອມີຂໍ້ຜິດພາດເກີດຂຶ້ນ", + "settings.general.sounds.agent.title": "ຕົວແທນ", + "settings.general.sounds.agent.description": "ຫຼິ້ນສຽງເມື່ອຕົວແທນສົມບູນ ຫຼືຕ້ອງການຄວາມສົນໃຈ", + "settings.general.sounds.permissions.title": "ການອະນຸຍາດ", + "settings.general.sounds.permissions.description": "ຫຼິ້ນສຽງເມື່ອຕ້ອງການການອະນຸຍາດ", + "settings.general.sounds.errors.title": "ຄວາມຜິດພາດ", + "settings.general.sounds.errors.description": "ຫຼິ້ນສຽງເມື່ອມີຂໍ້ຜິດພາດເກີດຂຶ້ນ", + "settings.shortcuts.title": "ປຸ່ມລັດແປ້ນພິມ", + "settings.shortcuts.reset.button": "ຣີເຊັດເປັນຄ່າເລີ່ມຕົ້ນ", + "settings.shortcuts.reset.toast.title": "ຣີເຊັດທາງລັດ", + "settings.shortcuts.reset.toast.description": "ປຸ່ມລັດຖືກຕັ້ງຄືນເປັນຄ່າເລີ່ມຕົ້ນ.", + "settings.shortcuts.conflict.title": "ທາງລັດໃຊ້ຢູ່ແລ້ວ", + "settings.shortcuts.conflict.description": "{{keybind}} ຖືກມອບໝາຍໃຫ້ {{titles}} ແລ້ວ.", + "settings.shortcuts.unassigned": "ບໍ່ໄດ້ມອບໝາຍ", + "settings.shortcuts.pressKeys": "ກົດປຸ່ມ", + "settings.shortcuts.search.placeholder": "ທາງລັດຄົ້ນຫາ", + "settings.shortcuts.search.empty": "ບໍ່ພົບທາງລັດ", + "settings.shortcuts.group.general": "ທົ່ວໄປ", + "settings.shortcuts.group.session": "ເຊດຊັນ", + "settings.shortcuts.group.navigation": "ການນໍາທາງ", + "settings.shortcuts.group.modelAndAgent": "ຕົວແບບ ແລະຕົວແທນ", + "settings.shortcuts.group.terminal": "ສະຖານີ", + "settings.shortcuts.group.prompt": "Prompt", + "settings.providers.title": "ຜູ້ໃຫ້ບໍລິການ", + "settings.providers.description": "ການຕັ້ງຄ່າຜູ້ໃຫ້ບໍລິການຈະຖືກຕັ້ງຄ່າໄດ້ທີ່ນີ້.", + "settings.providers.section.connected": "ຜູ້ໃຫ້ບໍລິການເຊື່ອມຕໍ່", + "settings.providers.connected.empty": "ບໍ່ມີຜູ້ໃຫ້ບໍລິການເຊື່ອມຕໍ່", + "settings.providers.connected.environmentDescription": "ເຊື່ອມຕໍ່ຈາກຕົວແປສະພາບແວດລ້ອມຂອງທ່ານ", + "settings.providers.section.popular": "ຜູ້ໃຫ້ບໍລິການທີ່ນິຍົມ", + "settings.providers.custom.description": "ເພີ່ມຜູ້ໃຫ້ບໍລິການທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI ໂດຍ URL ພື້ນຖານ.", + "settings.providers.tag.environment": "ສະພາບແວດລ້ອມ", + "settings.providers.tag.config": "ການຕັ້ງຄ່າ", + "settings.providers.tag.custom": "ກຳນົດເອງ", + "settings.providers.tag.other": "ອື່ນໆ", + "settings.models.title": "ຕົວແບບ", + "settings.models.description": "ການຕັ້ງຄ່າຕົວແບບຈະຕັ້ງຄ່າໄດ້ທີ່ນີ້.", + "settings.agents.title": "ຕົວແທນ", + "settings.agents.description": "ການຕັ້ງຄ່າຕົວແທນຈະຖືກຕັ້ງຄ່າໄດ້ທີ່ນີ້.", + "settings.commands.title": "ຄໍາສັ່ງ", + "settings.commands.description": "ການຕັ້ງຄ່າຄໍາສັ່ງຈະຖືກຕັ້ງຄ່າໄດ້ທີ່ນີ້.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "ການຕັ້ງຄ່າ MCP ຈະຖືກຕັ້ງຄ່າໄດ້ທີ່ນີ້.", + "settings.permissions.title": "ການອະນຸຍາດ", + "settings.permissions.description": "ຄວບຄຸມເຄື່ອງມືທີ່ເຊີບເວີໃຊ້ໄດ້ໂດຍຄ່າເລີ່ມຕົ້ນ.", + "settings.permissions.section.tools": "ເຄື່ອງມື", + "settings.permissions.toast.updateFailed.title": "ລົ້ມເຫລວໃນການປັບປຸງການອະນຸຍາດ", + "settings.permissions.action.allow": "ອະນຸຍາດ", + "settings.permissions.action.ask": "ຖາມ", + "settings.permissions.action.deny": "ປະຕິເສດ", + "settings.permissions.tool.read.title": "ອ່ານ", + "settings.permissions.tool.read.description": "ການອ່ານໄຟລ໌ (ກົງກັບເສັ້ນທາງໄຟລ໌)", + "settings.permissions.tool.edit.title": "ແກ້ໄຂ", + "settings.permissions.tool.edit.description": "ແກ້ໄຂໄຟລ໌, ລວມທັງການແກ້ໄຂ, ຂຽນ, ແລະແກ້ໄຂ", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "ຈັບຄູ່ໄຟລ໌ໂດຍໃຊ້ຮູບແບບ glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "ຄົ້ນ​ຫາ​ເນື້ອ​ຫາ​ຂອງ​ໄຟລ​໌​ໂດຍ​ນໍາ​ໃຊ້​ສໍາ​ນວນ​ປົກ​ກະ​ຕິ​", + "settings.permissions.tool.list.title": "ລາຍການ", + "settings.permissions.tool.list.description": "ລາຍຊື່ໄຟລ໌ພາຍໃນໄດເລກະທໍລີ", + "settings.permissions.tool.bash.title": "ບາຊ", + "settings.permissions.tool.bash.description": "ດໍາເນີນການຄໍາສັ່ງ shell", + "settings.permissions.tool.task.title": "ວຽກງານ", + "settings.permissions.tool.task.description": "ເປີດຕົວຕົວແທນຍ່ອຍ", + "settings.permissions.tool.skill.title": "ທັກສະ", + "settings.permissions.tool.skill.description": "ໂຫຼດທັກສະຕາມຊື່", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "ດໍາເນີນການສອບຖາມເຊີບເວີພາສາ", + "settings.permissions.tool.todowrite.title": "Todo ຂຽນ", + "settings.permissions.tool.todowrite.description": "ອັບເດດລາຍການທີ່ຕ້ອງເຮັດ", + "settings.permissions.tool.webfetch.title": "ການດຶງຂໍ້ມູນເວັບ", + "settings.permissions.tool.webfetch.description": "ເອົາເນື້ອຫາຈາກ URL", + "settings.permissions.tool.websearch.title": "ຄົ້ນຫາເວັບ", + "settings.permissions.tool.websearch.description": "ຊອກຫາເວັບ", + "settings.permissions.tool.external_directory.title": "ໄດເລກະທໍລີພາຍນອກ", + "settings.permissions.tool.external_directory.description": "ເຂົ້າເຖິງໄຟລ໌ນອກໄດເລກະທໍລີໂຄງການ", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "ກວດພົບການໂທດ້ວຍເຄື່ອງມືຊ້ຳໆດ້ວຍການປ້ອນຂໍ້ມູນຄືກັນ", + "session.delete.failed.title": "ລົ້ມເຫລວໃນການລຶບເຊດຊັນ", + "session.delete.title": "ລຶບເຊດຊັນ", + "session.delete.confirm": 'ລຶບເຊດຊັນ "{{name}}" ບໍ?', + "session.delete.button": "ລຶບເຊດຊັນ", + "workspace.new": "ພື້ນທີ່ເຮັດວຽກໃໝ່", + "workspace.type.local": "ທ້ອງຖິ່ນ", + "workspace.type.sandbox": "ກ່ອງຊາຍ", + "workspace.create.failed.title": "ລົ້ມເຫລວໃນການສ້າງພື້ນທີ່ເຮັດວຽກ", + "workspace.delete.failed.title": "ລົ້ມເຫລວໃນການລຶບພື້ນທີ່ເຮັດວຽກ", + "workspace.resetting.title": "ຣີເຊັດພື້ນທີ່ເຮັດວຽກ", + "workspace.resetting.description": "ນີ້ອາດຈະໃຊ້ເວລານາທີ.", + "workspace.reset.failed.title": "ລົ້ມເຫລວໃນການຣີເຊັດພື້ນທີ່ເຮັດວຽກ", + "workspace.reset.success.title": "ຣີເຊັດພື້ນທີ່ເຮັດວຽກ", + "workspace.reset.success.description": "ພື້ນທີ່ເຮັດວຽກຕອນນີ້ກົງກັບສາຂາເລີ່ມຕົ້ນ.", + "workspace.error.stillPreparing": "ພື້ນທີ່ເຮັດວຽກກຳລັງກະກຽມຢູ່", + "workspace.status.checking": "ກຳລັງກວດສອບການປ່ຽນແປງທີ່ບໍ່ໄດ້ຮວມ...", + "workspace.status.error": "ບໍ່ສາມາດກວດສອບສະຖານະ git ໄດ້.", + "workspace.status.clean": "ບໍ່ພົບການປ່ຽນແປງທີ່ບໍ່ຮວມເຂົ້າກັນ.", + "workspace.status.dirty": "ກວດພົບການປ່ຽນແປງທີ່ບໍ່ຮວມຢູ່ໃນບ່ອນເຮັດວຽກນີ້.", + "workspace.delete.title": "ລຶບພື້ນທີ່ເຮັດວຽກ", + "workspace.delete.confirm": 'ລຶບພື້ນທີ່ເຮັດວຽກ "{{name}}" ບໍ?', + "workspace.delete.button": "ລຶບພື້ນທີ່ເຮັດວຽກ", + "workspace.reset.title": "ຣີເຊັດພື້ນທີ່ເຮັດວຽກ", + "workspace.reset.confirm": 'ຣີເຊັດພື້ນທີ່ເຮັດວຽກ "{{name}}" ບໍ?', + "workspace.reset.button": "ຣີເຊັດພື້ນທີ່ເຮັດວຽກ", + "workspace.reset.archived.none": "ບໍ່ມີເຊດຊັນທີ່ເຄື່ອນໄຫວຈະຖືກເກັບໄວ້.", + "workspace.reset.archived.one": "1 ເຊດຊັນຈະຖືກເກັບໄວ້.", + "workspace.reset.archived.many": "ເຊດຊັນ {{count}} ຈະຖືກເກັບໄວ້.", + "workspace.reset.note": "ນີ້ຈະຣີເຊັດພື້ນທີ່ເຮັດວຽກໃຫ້ກົງກັບສາຂາເລີ່ມຕົ້ນ.", + "dialog.usageExceeded.dontShowAgain": "ຢ່າສະແດງອີກ", +} diff --git a/packages/app/src/i18n/lt.ts b/packages/app/src/i18n/lt.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba161eb31879d0432ad0a8d32c87318abf933ccb --- /dev/null +++ b/packages/app/src/i18n/lt.ts @@ -0,0 +1,1173 @@ +import { DESKTOP_NATIVE_KEYS } from "./desktop-native" + +const desktop = [ + "OpenCode", + "Failas", + "Taisa", + "Rodymas", + "Eiti", + "Langas", + "Žinynas", + "Tikrinti, ar yra naujinimų...", + "Nuostatos", + "Įkelti Webview iš naujo", + "Paleisti iš naujo", + "Eksportuoti žurnalus...", + "Naujas seansas", + "Atverti projektą...", + "Naujas langas", + "Užverti langą", + "Atšaukti", + "Pakartoti", + "Iškirpti", + "Kopijuoti", + "Įdėti", + "Pašalinti", + "Pažymėti viską", + "Rodyti arba slėpti parankinę", + "Rodyti arba slėpti terminalą", + "Rodyti arba slėpti failų medį", + "Įkelti iš naujo", + "Rodyti arba slėpti programavimo priemones", + "Tikras dydis", + "Padidinti", + "Sumažinti", + "Rodyti arba slėpti visą ekraną", + "Atgal", + "Pirmyn", + "Ankstesnis seansas", + "Kitas seansas", + "Ankstesnis projektas", + "Kitas projektas", + "Suskleisti", + "Išskleisti", + "OpenCode dokumentacija", + "Pagalbos forumas", + "Pateikti atsiliepimą", + "Pranešti apie klaidą", + "OpenCode meniu", + "Naujinių patikrinti nepavyko.", + "Naujinimo klaida", + "Naudojate naujausią versiją.", + "Naujinių nėra", + "Naujinimas {{version}} parsiųstas. Paleisti iš naujo dabar?", + "Naujinimas paruoštas", + "Paleisti iš naujo", + "Vėliau", + "Paleisti iš naujo", + "Eksportuoti žurnalus", + "Laukti toliau", + "Baigti darbą", + "OpenCode įkelti nepavyko", + "OpenCode langas netikėtai užsidarė", + "OpenCode neatsako", + "Galite paleisti programą iš naujo, atverti žurnalus arba laukti toliau.", + "Langas: {{window}}\nURL: {{url}}\nKlaida: {{code}} {{description}}", + "Langas: {{window}}\nPriežastis: {{reason}}\nKodas: {{code}}", + "", + "Pasirinkite aplanką", + "Pasirinkite failą", + "Įrašyti failą", + "Failai", + "Vietinis serveris", + "WSL veikia tik sistemoje Windows", + "WSL nepasiekiamas", + "Nepavyko išvardyti įdiegtų WSL distributyvų", + "Nepavyko išvardyti internete pasiekiamų WSL distributyvų", + "Negalima vykdyti komandų distributyve", + "WSL įdiegti nepavyko", + "Nepavyko įdiegti distributyvo: {{distro}}", + "OpenCode įdiegti nepavyko", + "{{distro}} jau pridėtas", + "opencode šiame distributyve neįdiegtas", + "opencode įdiegtas, bet jo paleisti nepavyko", + "OpenCode neįdiegtas distributyve {{distro}}", + "OpenCode naujinimas baigtas, bet {{distro}} vis dar praneša {{installed}}; tikėtasi {{expected}}", + "versijos nėra", + "WSL serveris užsidarė po paleidimo (code={{code}} signal={{signal}})", + "WSL serveris užsidarė prieš pradėdamas tinkamai veikti (code={{code}} signal={{signal}}){{output}}", + "Baigėsi {{distro}} pagalbinio proceso būklės patikros {{timeout}} ms skirtasis laikas", + "Baigėsi komandai {{command}} {{args}} skirtas {{timeout}} ms laikas", + "Nepavyko gauti prievado", + "Failas nebuvo pasirinktas failų parinkiklyje", + "Pasirinkti priedai viršija {{limit}} MB ribą", +] + +export const dict = { + ...Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key, index) => [key, desktop[index]])), + "command.category.suggested": "Siūloma", + "command.category.view": "Rodinys", + "command.category.project": "Projektas", + "command.category.provider": "Teikėjas", + "command.category.server": "Serveris", + "command.category.session": "Seansas", + "command.category.theme": "tema", + "command.category.language": "Kalba", + "command.category.file": "Failas", + "command.category.context": "Kontekstas", + "command.category.terminal": "Terminalas", + "command.category.model": "Modelis", + "command.category.mcp": "MCP", + "command.category.agent": "Agentas", + "command.category.permissions": "Leidimai", + "command.category.workspace": "Darbo sritis", + "command.category.settings": "Nustatymai", + "command.logs.export": "Eksportuoti žurnalus", + "theme.scheme.system": "Sistema", + "theme.scheme.light": "Šviesi", + "theme.scheme.dark": "Tamsi", + "command.sidebar.toggle": "Perjungti šoninę juostą", + "command.project.open": "Atidaryti projektą", + "command.project.previous": "Ankstesnis projektas", + "command.project.next": "Kitas projektas", + "command.project.index": "Perjunkite į projektą {{index}}", + "command.provider.connect": "Prijungti teikėją", + "command.server.switch": "Perjungti serverį", + "command.settings.open": "Atidaryti nustatymus", + "command.session.previous": "Ankstesnis seansas", + "command.session.next": "Kitas seansas", + "command.session.previous.unseen": "Ankstesnis neskaitytas seansas", + "command.session.next.unseen": "Kitas neskaitytas seansas", + "command.session.archive": "Archyvuoti seansą", + "command.palette": "Komandų paletė", + "command.theme.cycle": "Perjungti temą", + "command.theme.set": "Naudokite temą: {{theme}}", + "command.theme.scheme.cycle": "Perjungti spalvų schemą", + "command.theme.scheme.set": "Naudokite spalvų schemą: {{scheme}}", + "command.language.cycle": "Perjungti kalbą", + "command.language.set": "Naudokite kalbą: {{language}}", + "command.session.new": "Naujas seansas", + "command.file.open": "Atidaryti failą", + "command.tab.close": "Uždaryti skirtuką", + "command.tab.reopenClosed": "Iš naujo atidaryti uždarytą skirtuką", + "command.context.addSelection": "Pridėkite pasirinkimą prie konteksto", + "command.context.addSelection.description": "Pridėkite pasirinktas eilutes iš dabartinio failo", + "command.input.focus": "Fokusuoti įvesties lauką", + "command.terminal.toggle": "Perjungti terminalą", + "command.fileTree.toggle": "Perjungti failų medį", + "command.review.toggle": "Perjungti peržiūrą", + "command.terminal.new": "Naujas terminalas", + "command.terminal.new.description": "Sukurkite naują terminalo skirtuką", + "command.steps.toggle": "Perjungti žingsnius", + "command.steps.toggle.description": "Rodyti arba slėpti dabartinio pranešimo veiksmus", + "command.message.previous": "Ankstesnė žinutė", + "command.message.previous.description": "Eikite į ankstesnį vartotojo pranešimą", + "command.message.next": "Kitas pranešimas", + "command.message.next.description": "Eikite į kitą vartotojo pranešimą", + "command.model.choose": "Pasirinkite modelį", + "command.model.choose.description": "Pasirinkite kitą modelį", + "command.mcp.toggle": "Perjungti MCPs", + "command.mcp.toggle.description": "Perjungti MCPs", + "command.agent.cycle": "Perjungti agentą", + "command.agent.cycle.description": "Perjunkite į kitą agentą", + "command.agent.cycle.reverse": "Perjungti į ankstesnį agentą", + "command.agent.cycle.reverse.description": "Perjungti į ankstesnį agentą", + "command.model.variant.cycle": "Perkelkite mąstymo pastangas", + "command.model.variant.cycle.description": "Perjunkite į kitą pastangų lygį", + "command.prompt.mode.shell": "Apvalkalas", + "command.prompt.mode.normal": "Užklausa", + "command.permissions.autoaccept.enable": "Automatiškai priimti leidimus", + "command.permissions.autoaccept.disable": "Sustabdykite automatinį leidimų priėmimą", + "command.workspace.toggle": "Perjungti darbo sritis", + "command.workspace.toggle.description": "Šoninėje juostoje įgalinkite arba išjunkite kelias darbo sritis", + "command.session.undo": "Anuliuoti", + "command.session.undo.description": "Anuliuoti paskutinį pranešimą", + "command.session.redo": "Perdaryti", + "command.session.redo.description": "Pakartokite paskutinį anuliuotą pranešimą", + "command.session.compact": "Glaudinti seansą", + "command.session.compact.description": "Apibendrinti seansą, kad sumažėtų kontekstas", + "command.session.fork": "Atskirti nuo pranešimo", + "command.session.fork.description": "Sukurti naują seansą iš ankstesnio pranešimo", + "command.session.share": "Bendrinti seansą", + "command.session.share.description": "Bendrinti šį seansą ir nukopijuoti URL į mainų sritį", + "command.session.unshare": "Atšaukti seanso bendrinimą", + "command.session.unshare.description": "Nebebendrinti šio seanso", + "command.session.export": "Eksportuoti seansą", + "command.session.export.description": "Eksportuokite visą seanso stenogramą kaip JSON", + "palette.search.placeholder": "Ieškokite failų, komandų ir seansų", + "palette.search.placeholder.home": "Ieškoti komandų ir seansų", + "palette.empty": "Rezultatų nerasta", + "palette.group.commands": "Komandos", + "palette.group.files": "Failai", + "dialog.provider.search.placeholder": "Paieškos teikėjai", + "dialog.provider.empty": "Teikėjų nerasta", + "dialog.provider.group.popular": "Populiarus", + "dialog.provider.group.other": "Kita", + "dialog.provider.custom.label": "Pasirinktinis su OpenAI suderinamas tiekėjas", + "dialog.provider.tag.recommended": "Rekomenduojama", + "dialog.provider.opencode.note": "Kuruojami modeliai, įskaitant Claude, GPT, Gemini ir kt.", + "dialog.provider.opencode.tagline": "Patikimi optimizuoti modeliai", + "dialog.provider.opencodeGo.tagline": "Mažos kainos abonementas visiems", + "dialog.provider.anthropic.note": "Tiesioginė prieiga prie Claude modelių, įskaitant Pro ir Max", + "dialog.provider.copilot.note": "AI modeliai, skirti kodavimo pagalbai per GitHub Copilot", + "dialog.provider.openai.note": "GPT modeliai, skirti greitoms, bendroms AI užduotims atlikti", + "dialog.provider.google.note": "Gemini modeliai greitiems, struktūriniams atsakymams", + "dialog.provider.openrouter.note": "Pasiekite visus palaikomus modelius iš vieno tiekėjo", + "dialog.provider.vercel.note": "Vieninga prieiga prie AI modelių su išmaniuoju maršruto parinkimu", + "dialog.model.select.title": "Pasirinkite modelį", + "dialog.model.search.placeholder": "Ieškoti modelių", + "dialog.model.empty": "Nėra modelio rezultatų", + "dialog.model.manage": "Tvarkyti modelius", + "dialog.model.manage.description": "Tinkinkite, kurie modeliai rodomi modelio parinkiklyje.", + "dialog.model.manage.provider.toggle": "Perjungti visus {{provider}} modelius", + "dialog.model.unpaid.freeModels.title": "Nemokami OpenCode modeliai", + "dialog.model.unpaid.addMore.title": "Pridėkite daugiau populiarių tiekėjų modelių", + "dialog.model.unpaid.viewMoreProviders": "Žr. daugiau nei 70 tiekėjų", + "dialog.provider.viewAll": "Rodyti daugiau teikėjų", + "provider.connect.title": "Prijunkite {{provider}}", + "provider.connect.title.anthropicProMax": "Prisijunkite naudodami Claude Pro/Max", + "provider.connect.selectMethod": "Pasirinkite {{provider}} prisijungimo būdą.", + "provider.connect.method.apiKey": "API raktas", + "provider.connect.method.browser": "Naršyklė", + "provider.connect.method.headless": "Be galvos", + "provider.connect.status.inProgress": "Vykdoma prieigos teisė...", + "provider.connect.status.waiting": "Laukiama įgaliojimo...", + "provider.connect.status.failed": "Autorizacija nepavyko: {{error}}", + "provider.connect.apiKey.description": + "Įveskite {{provider}} API raktą, kad susietumėte paskyrą ir naudotumėte {{provider}} modelius programoje OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API raktas", + "provider.connect.apiKey.placeholder": "API raktas", + "provider.connect.apiKey.required": "Reikalingas API raktas", + "provider.connect.opencodeZen.line1": + "OpenCode Zen suteikia prieigą prie kuruojamo patikimų optimizuotų kodavimo agentų modelių rinkinio.", + "provider.connect.opencodeZen.line2": + "Su vienu API raktu galėsite pasiekti tokius modelius kaip Claude, GPT, Gemini, GLM ir kt.", + "provider.connect.opencodeZen.visit.prefix": "Aplankykite", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "norėdami pasiimti savo API raktą.", + "provider.connect.oauth.code.visit.prefix": "Aplankykite", + "provider.connect.oauth.code.visit.link": "šią nuorodą", + "provider.connect.oauth.code.visit.suffix": + "rinkti prieigos kodą, kad galėtumėte prisijungti prie paskyros ir naudoti {{provider}} modelius OpenCode.", + "provider.connect.oauth.code.label": "{{method}} autorizacijos kodas", + "provider.connect.oauth.code.placeholder": "Autorizacijos kodas", + "provider.connect.oauth.code.required": "Reikalingas autorizacijos kodas", + "provider.connect.oauth.code.invalid": "Neteisingas prieigos teisės kodas", + "provider.connect.oauth.auto.visit.prefix": "Apsilankykite", + "provider.connect.oauth.auto.visit.link": "šią nuorodą", + "provider.connect.oauth.auto.visit.suffix": + "ir įveskite toliau esantį kodą, kad susietumėte paskyrą ir naudotumėte {{provider}} modelius OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Patvirtinimo kodas", + "provider.connect.toast.connected.title": "{{provider}} prijungtas", + "provider.connect.toast.connected.description": "Dabar galima naudoti {{provider}} modelius.", + "provider.custom.title": "Pasirinktinis teikėjas", + "provider.custom.unavailable": "Pasirinktiniai teikėjai šiame serveryje nepasiekiami", + "provider.custom.description.prefix": "Sukonfigūruokite su OpenAI suderinamą teikėją. Žiūrėkite", + "provider.custom.description.link": "teikėjo konfigūracijos dokumentai", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Teikėjo ID", + "provider.custom.field.providerID.placeholder": "mano teikėjas", + "provider.custom.field.providerID.description": "Mažosios raidės, skaičiai, brūkšneliai arba apatiniai brūkšniai", + "provider.custom.field.name.label": "Rodomas vardas", + "provider.custom.field.name.placeholder": "Mano AI teikėjas", + "provider.custom.field.baseURL.label": "Pagrindas URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API raktas", + "provider.custom.field.apiKey.placeholder": "API raktas", + "provider.custom.field.apiKey.description": + "Neprivaloma. Palikite tuščią, jei autentifikavimą valdote per antraštes.", + "provider.custom.models.label": "Modeliai", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "modelio ID", + "provider.custom.models.name.label": "Vardas", + "provider.custom.models.name.placeholder": "Rodomas pavadinimas", + "provider.custom.models.remove": "Pašalinti modelį", + "provider.custom.models.add": "Pridėti modelį", + "provider.custom.headers.label": "Antraštės (nebūtina)", + "provider.custom.headers.key.label": "Antraštė", + "provider.custom.headers.key.placeholder": "Antraštė-Pavadinimas", + "provider.custom.headers.value.label": "Vertė", + "provider.custom.headers.value.placeholder": "vertė", + "provider.custom.headers.remove": "Pašalinti antraštę", + "provider.custom.headers.add": "Pridėti antraštę", + "provider.custom.error.providerID.required": "Reikalingas teikėjo ID", + "provider.custom.error.providerID.format": + "Naudokite mažąsias raides, skaičius, brūkšnelius arba apatinius brūkšnius", + "provider.custom.error.providerID.exists": "Tas teikėjo ID jau yra", + "provider.custom.error.name.required": "Būtinas rodomas vardas", + "provider.custom.error.baseURL.required": "Reikalingas pagrindas URL", + "provider.custom.error.baseURL.format": "Turi prasidėti http:// arba https://", + "provider.custom.error.required": "Privaloma", + "provider.custom.error.duplicate": "Pasikartoti", + "provider.disconnect.toast.disconnected.title": "{{provider}} atjungtas", + "provider.disconnect.toast.disconnected.description": "{{provider}} modeliai nebepasiekiami.", + "model.tag.free": "Nemokama", + "model.tag.latest": "Naujausias", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekstą", + "model.input.image": "vaizdas", + "model.input.audio": "garso", + "model.input.video": "vaizdo įrašą", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Leidžia: {{inputs}}", + "model.tooltip.context.label": "Kontekstas", + "model.tooltip.inputs": "Įėjimai", + "model.tooltip.model": "Modelis", + "model.tooltip.provider": "Teikėjas", + "model.tooltip.reasoning": "Samprotavimas", + "model.tooltip.reasoning.allowed": "Leidžia samprotauti", + "model.tooltip.reasoning.none": "Jokių samprotavimų", + "model.tooltip.context": "Konteksto riba {{limit}}", + "common.search.placeholder": "Ieškoti", + "common.goBack": "Grįžti atgal", + "common.goForward": "Eikite į priekį", + "common.loading": "Įkeliama", + "common.loading.ellipsis": "...", + "common.cancel": "Atšaukti", + "common.clear": "Išvalyti", + "common.open": "Atidaryti", + "common.connect": "Prisijunkite", + "common.disconnect": "Atsijungti", + "common.continue": "Tęsti", + "common.submit": "Pateikti", + "common.save": "Išsaugoti", + "common.saving": "Išsaugoma...", + "common.default": "Numatytoji", + "common.attachment": "priedą", + "prompt.placeholder.shell": "Įveskite apvalkalo komandą... {{example}}", + "prompt.placeholder.normal": 'Paklauskite bet ko... "{{example}}"', + "prompt.placeholder.simple": "Paklausk bet ko...", + "prompt.placeholder.summarizeComments": "Apibendrinkite komentarus…", + "prompt.placeholder.summarizeComment": "Apibendrinti komentarą…", + "prompt.mode.shell": "Apvalkalas", + "prompt.mode.normal": "Užklausa", + "prompt.mode.shell.exit": "esc, kad išeitumėte", + "session.child.promptDisabled": "Poagento seansui negalima siųsti užklausų.", + "session.child.backToParent": "Grįžti į pagrindinį seansą.", + "prompt.example.1": "Pataisykite TODO kodų bazėje", + "prompt.example.2": "Kokia šio projekto techninė dalis?", + "prompt.example.3": "Pataisykite sugedusius testus", + "prompt.example.4": "Paaiškinkite, kaip veikia autentifikavimas", + "prompt.example.5": "Raskite ir ištaisykite saugos spragas", + "prompt.example.6": "Pridėkite naudotojo paslaugos vienetų testus", + "prompt.example.7": "Pataisykite šią funkciją, kad ji būtų lengviau skaitoma", + "prompt.example.8": "Ką reiškia ši klaida?", + "prompt.example.9": "Padėkite man derinti šią problemą", + "prompt.example.10": "Sukurkite API dokumentaciją", + "prompt.example.11": "Optimizuokite duomenų bazės užklausas", + "prompt.example.12": "Pridėti įvesties patvirtinimą", + "prompt.example.13": "Sukurkite naują komponentą...", + "prompt.example.14": "Kaip įdiegti šį projektą?", + "prompt.example.15": "Peržiūrėkite mano kodą, kad gautumėte geriausią praktiką", + "prompt.example.16": "Prie šios funkcijos pridėkite klaidų tvarkymą", + "prompt.example.17": "Paaiškinkite šį reguliarųjį reiškinį", + "prompt.example.18": "Konvertuokite tai į TypeScript", + "prompt.example.19": "Pridėkite registravimą visoje kodų bazėje", + "prompt.example.20": "Kokios priklausomybės yra pasenusios?", + "prompt.example.21": "Padėkite man parašyti perkėlimo scenarijų", + "prompt.example.22": "Įdiekite šio galutinio taško talpyklą", + "prompt.example.23": "Į šį sąrašą įtraukite puslapius", + "prompt.example.24": "Sukurkite komandą CLI, skirtą...", + "prompt.example.25": "Kaip čia veikia aplinkos kintamieji?", + "prompt.popover.emptyResults": "Nėra atitinkančių rezultatų", + "prompt.popover.emptyCommands": "Nėra atitinkamų komandų", + "prompt.dropzone.label": "Čia numeskite vaizdus, ​​PDF ar tekstinius failus", + "prompt.dropzone.file.label": "Nukreipkite į @minėjimo failą", + "prompt.slash.badge.custom": "paprotys", + "prompt.slash.badge.skill": "įgūdžių", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktyvus", + "prompt.context.includeActiveFile": "Įtraukti aktyvų failą", + "prompt.context.removeActiveFile": "Pašalinkite aktyvų failą iš konteksto", + "prompt.context.removeFile": "Pašalinti failą iš konteksto", + "prompt.action.attachFile": "Pridėti failus", + "prompt.menu.addImagesAndFiles": "Pridėkite failus ir daugiau", + "prompt.menu.imagesAndFiles": "Vaizdai ir failai", + "prompt.menu.commands": "Komandos", + "prompt.menu.context": "Kontekstas", + "prompt.menu.shellCommand": "Apvalkalo komanda", + "prompt.attachment.remove": "Pašalinti priedą", + "prompt.action.send": "Siųsti", + "prompt.action.stop": "Stabdyti", + "prompt.toast.pasteUnsupported.title": "Nepalaikomas priedas", + "prompt.toast.pasteUnsupported.description": "Čia galima pridėti tik vaizdus, ​​PDF arba tekstinius failus.", + "prompt.toast.attachmentDuplicate.title": "Šis failas jau buvo įkeltas", + "prompt.toast.modelAgentRequired.title": "Pasirinkite agentą ir modelį", + "prompt.toast.modelAgentRequired.description": "Prieš siųsdami raginimą, pasirinkite agentą ir modelį.", + "prompt.toast.worktreeCreateFailed.title": "Nepavyko sukurti darbo medžio", + "prompt.toast.sessionCreateFailed.title": "Nepavyko sukurti seanso", + "prompt.toast.shellSendFailed.title": "Nepavyko išsiųsti apvalkalo komandos", + "prompt.toast.commandSendFailed.title": "Nepavyko išsiųsti komandos", + "prompt.toast.promptSendFailed.title": "Nepavyko išsiųsti raginimo", + "prompt.toast.promptSendFailed.description": "Nepavyko nuskaityti seanso", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "Įjungtas {{enabled}} arba {{total}}", + "dialog.mcp.empty": "MCP nesukonfigūruota", + "dialog.lsp.empty": "LSP automatiškai aptinkami pagal failų tipus", + "dialog.plugins.empty": "Papildiniai sukonfigūruoti opencode.json", + "mcp.status.connected": "prijungtas", + "mcp.status.failed": "nepavyko", + "mcp.status.needs_auth": "reikia autentifikavimo", + "mcp.status.disabled": "neįgalus", + "mcp.auth.clickToAuthenticate": "Spustelėkite, kad patvirtintumėte", + "dialog.fork.empty": "Nėra pranešimų, iš kurių būtų galima išsišakoti", + "dialog.directory.search.placeholder": "Ieškokite aplankų", + "dialog.directory.empty": "Aplankų nerasta", + "dialog.directory.action.selectFile": "Pasirinkite failą", + "dialog.directory.action.selectFolder": "Pasirinkite aplanką", + "dialog.directory.root": "Šaknis", + "dialog.directory.parent": "Tėvas", + "dialog.directory.readError": "Nepavyko perskaityti šio aplanko", + "app.server.unreachable": "Nepavyko pasiekti {{server}}", + "app.server.retrying": "Automatiškai bandoma iš naujo...", + "app.server.otherServers": "Kiti serveriai", + "dialog.server.title": "Serveriai", + "dialog.server.description": "Perjunkite, prie kurio OpenCode serverio jungiasi ši programa.", + "dialog.server.search.placeholder": "Paieškos serveriai", + "dialog.server.empty": "Serverių dar nėra", + "dialog.server.add.title": "Pridėti serverį", + "dialog.server.add.url": "Serverio adresas", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Nepavyko prisijungti prie serverio", + "dialog.server.add.checking": "Tikrinama...", + "dialog.server.add.button": "Pridėti serverį", + "dialog.server.add.name": "Serverio pavadinimas (neprivaloma)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Vartotojo vardas (neprivaloma)", + "dialog.server.add.usernamePlaceholder": "vartotojo vardas", + "dialog.server.add.password": "Slaptažodis (neprivaloma)", + "dialog.server.add.passwordPlaceholder": "slaptažodis", + "dialog.server.edit.title": "Redaguoti serverį", + "dialog.server.default.title": "Numatytasis serveris", + "dialog.server.default.description": + "Prisijunkite prie šio serverio paleidę programą, o ne paleiskite vietinį serverį. Reikia paleisti iš naujo.", + "dialog.server.default.none": "Nepasirinktas serveris", + "dialog.server.default.set": "Nustatyti dabartinį serverį kaip numatytąjį", + "dialog.server.default.clear": "Aišku", + "dialog.server.action.remove": "Pašalinti serverį", + "dialog.server.menu.edit": "Redaguoti", + "dialog.server.menu.default": "Nustatyti kaip numatytąjį", + "dialog.server.menu.defaultRemove": "Pašalinti numatytąjį", + "dialog.server.menu.delete": "Ištrinti", + "dialog.server.current": "Dabartinis serveris", + "dialog.server.status.default": "Numatytoji", + "wsl.server.add": "Pridėti WSL serverį", + "wsl.server.addShort": "Pridėkite WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL serveris", + "wsl.server.retryStart": "Pabandykite pradėti iš naujo", + "wsl.server.updating": "Atnaujinama...", + "wsl.onboarding.step.distro": "Pasirinkite distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Tikrinama WSL...", + "wsl.onboarding.restartRequired": "Windows reikia paleisti iš naujo, kad būtų baigtas WSL diegimas.", + "wsl.onboarding.ready": "WSL yra paruoštas.", + "wsl.onboarding.required": "Norint tęsti, reikia WSL.", + "wsl.onboarding.checkingDistros": "Tikrinamos distribucijos...", + "wsl.onboarding.installingDistro": "Diegimas {{distro}}...", + "wsl.onboarding.checkingDistro": "Tikrinama {{distro}}...", + "wsl.onboarding.listingDistros": "Sąrašas platinimo...", + "wsl.onboarding.distroReady": "{{distro}} yra paruoštas.", + "wsl.onboarding.distroNotInstalled": "{{distro}} dar neįdiegtas.", + "wsl.onboarding.openDistroOnce": "Atidarykite {{distro}} vieną kartą, kad užbaigtumėte sąranką.", + "wsl.onboarding.finishingDistro": "{{distro}} apdailos sąranka.", + "wsl.onboarding.pickDistro": "Pasirinkite platinimą arba įdiekite jį žemiau.", + "wsl.onboarding.checkingOpencode": "Tikrinama OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Tikrinama OpenCode {{distro}}...", + "wsl.onboarding.updatingOpencode": "Atnaujinamas OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Atnaujinamas OpenCode {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Atnaujinkite OpenCode {{distro}}.", + "wsl.onboarding.updateOpencode": "Atnaujinkite OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode yra paruoštas {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode yra paruoštas.", + "wsl.onboarding.installOpencodeIn": "Įdiekite OpenCode į {{distro}}.", + "wsl.onboarding.installOpencode": "Įdiekite OpenCode", + "wsl.onboarding.chooseDistroFirst": "Pirmiausia pasirinkite distribuciją.", + "wsl.onboarding.loadFailed": "Nepavyko įkelti WSL būsenos.", + "wsl.onboarding.loading": "Įkeliama...", + "wsl.onboarding.installedDistros": "Įdiegtos distribucijos", + "wsl.onboarding.checkAgain": "Patikrinkite dar kartą", + "wsl.onboarding.distroStatus.ready": "Paruošta", + "wsl.onboarding.distroStatus.checking": "Tikrinama...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode neįdiegtas", + "wsl.onboarding.distroStatus.missingTools": "Trūksta bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Nepalaikoma · Naudokite WSL 2", + "wsl.onboarding.needAnotherDistro": "Reikia kito platinimo?", + "wsl.onboarding.needAnotherDistroHint": "Įdiekite Linux paskirstymą iš WSL katalogo", + "wsl.onboarding.wslNotInstalled.title": "WSL neįdiegtas", + "wsl.onboarding.wslNotInstalled.description": + "Kad OpenCode galėtų pridėti WSL serverį, reikalingas WSL (Windows posistemis, skirtas Linux)", + "wsl.onboarding.wslUnavailable.title": "WSL nepasiekiamas", + "wsl.onboarding.wslUnavailable.description": "OpenCode nepavyko patikrinti WSL šiame įrenginyje.", + "wsl.onboarding.installWsl": "Įdiekite WSL", + "wsl.onboarding.windowsRestartRequired": + "Iš naujo paleiskite Windows, kad užbaigtumėte WSL diegimą, tada iš naujo atidarykite OpenCode.", + "wsl.onboarding.next": "Kitas", + "wsl.onboarding.refresh": "Atnaujinti", + "wsl.onboarding.allDistrosAdded": "Visos įdiegtos distribucijos jau pridėtos.", + "wsl.onboarding.noDistros": "Dar neaptikta jokių paskirstymų.", + "wsl.onboarding.install": "Įdiegti", + "wsl.onboarding.installing": "Diegiama...", + "wsl.onboarding.installDistro": "Įdiekite distro", + "wsl.onboarding.searchDistros": "Ieškoti paskirstymų", + "wsl.onboarding.wsl2Required": "Reikalingas WSL 2.", + "wsl.onboarding.toolsRequired": "Šiam platinimui reikia bash ir curl.", + "wsl.onboarding.openTerminal": "Atidarykite terminalą", + "wsl.onboarding.path": "Kelias: {{path}}", + "wsl.onboarding.notFound": "nerasta", + "wsl.onboarding.version": "Versija: {{version}}", + "wsl.onboarding.unknown": "nežinomas", + "wsl.onboarding.desktopVersion": "darbalaukis {{version}}", + "wsl.onboarding.versionMismatch": "Įdiegta versija neatitinka darbalaukio programos versijos.", + "wsl.onboarding.adding": "Pridedama...", + "help.tabs.toast.ariaLabel": "Pristatome Tab. Tvarkykite savo darbą ir aktyvias sesijas naudodami skirtukus", + "help.tabs.toast.dismiss": "Atsisakyti Tabs informacijos", + "help.tabs.title": "Pristatome Tab", + "help.tabs.description": "Tvarkykite savo darbą ir aktyvias sesijas naudodami skirtukus", + "help.tabs.date": "liepos 14 d", + "help.tabs.introduction": "OpenCode Desktop dabar sukurtas iš skirtukų.", + "help.tabs.sessions": + "Pradėkite naują seansą skirtuke arba atidarykite esamą seansą iš bet kurio savo projekto. Atidarykite naują skirtuką, kai pradedate ką nors naujo, ir uždarykite, kai baigsite.", + "help.tabs.organize": + "Jei atidarysite kelis skirtukus, bus lengviau organizuoti aktyvias sesijas. Pervardykite skirtukus į kažką įsimintino, jei planuojate juos palikti.", + "help.tabs.home": + "Visas savo sesijas ir projektus rasite naujajame Home ekrane. Pasirinkus sesiją, ji atidaroma skirtuke.", + "help.tabs.persistence": "Kai iš naujo atidarote programą, skirtukai vis dar yra atidaryti.", + "help.tabs.worktrees": + "Naujasis dizainas dar nepalaiko Git Worktrees, jis pasirodys netrukus. Taigi, jei norite ir toliau naudoti ankstesnį išdėstymą, galite perjungti išdėstymus nustatymuose. Tiesiog nepamirškite, kad naujasis išdėstymas taps nuolatinis po kelių savaičių.", + "server.row.noUsername": "jokio vartotojo vardo", + "dialog.project.edit.title": "Redaguoti projektą", + "dialog.project.edit.name": "Vardas", + "dialog.project.edit.icon": "Piktograma", + "dialog.project.edit.icon.alt": "Projekto piktograma", + "dialog.project.edit.icon.hint": "Spustelėkite arba vilkite vaizdą", + "dialog.project.edit.icon.recommended": "Rekomenduojama: 128x128px", + "dialog.project.edit.color": "Spalva", + "dialog.project.edit.color.select": "Pasirinkite {{color}} spalvą", + "dialog.project.edit.worktree.startup": "Darbo srities paleidimo scenarijus", + "dialog.project.edit.worktree.startup.description": "Veikia sukūrus naują darbo sritį (darbo medį).", + "dialog.project.edit.worktree.startup.placeholder": "pvz. bandelė įdiegti", + "dialog.releaseNotes.action.getStarted": "Pradėkite", + "dialog.releaseNotes.action.next": "Kitas", + "dialog.releaseNotes.action.hideFuture": "Nerodykite šių dalykų ateityje", + "dialog.releaseNotes.media.alt": "Išleidimo peržiūra", + "context.breakdown.title": "Konteksto suskirstymas", + "context.breakdown.note": + "Apytikslis įvesties prieigos raktų suskirstymas. „Kita“ apima įrankių apibrėžimus ir pridėtines išlaidas.", + "context.breakdown.system": "Sistema", + "context.breakdown.user": "Vartotojas", + "context.breakdown.assistant": "Padėjėjas", + "context.breakdown.tool": "Įrankio skambučiai", + "context.breakdown.other": "Kita", + "context.systemPrompt.title": "Sistemos raginimas", + "context.rawMessages.title": "Neapdoroti pranešimai", + "context.export.session": "Eksportuoti seansą", + "context.stats.session": "Seansas", + "context.stats.messages": "Žinutės", + "context.stats.provider": "Teikėjas", + "context.stats.model": "Modelis", + "context.stats.limit": "Konteksto riba", + "context.stats.totalTokens": "Iš viso žetonų", + "context.stats.usage": "Naudojimas", + "context.stats.inputTokens": "Įvesties žetonai", + "context.stats.outputTokens": "Išvesties žetonai", + "context.stats.reasoningTokens": "Samprotavimo ženklai", + "context.stats.cacheTokens": "Talpyklos prieigos raktai (skaityti / rašyti)", + "context.stats.userMessages": "Vartotojo pranešimai", + "context.stats.assistantMessages": "Asistento pranešimai", + "context.stats.totalCost": "Bendra kaina", + "context.stats.sessionCreated": "Seansas sukurtas", + "context.stats.lastActivity": "Paskutinė veikla", + "context.usage.tokens": "Žetonai", + "context.usage.usage": "Naudojimas", + "context.usage.cost": "Kaina", + "context.usage.clickToView": "Spustelėkite norėdami pamatyti kontekstą", + "context.usage.view": "Peržiūrėkite kontekstinį naudojimą", + "language.en": "anglų kalba", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "portugalų k. (Brazilija)", + "language.bs": "Bosanskis", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Turkçe", + "toast.language.title": "Kalba", + "toast.language.description": "Perjungta į {{language}}", + "toast.theme.title": "Tema perjungta", + "toast.scheme.title": "Spalvų schema", + "toast.workspace.enabled.title": "Darbo sritys įjungtos", + "toast.workspace.enabled.description": "Dabar šoninėje juostoje rodomi keli darbo medžiai", + "toast.workspace.disabled.title": "Darbo vietos išjungtos", + "toast.workspace.disabled.description": "Šoninėje juostoje rodomas tik pagrindinis darbo medis", + "toast.permissions.autoaccept.on.title": "Automatinis leidimų priėmimas", + "toast.permissions.autoaccept.on.description": "Leidimų prašymai bus patvirtinti automatiškai", + "toast.permissions.autoaccept.off.title": "Sustabdė automatinį leidimų priėmimą", + "toast.permissions.autoaccept.off.description": "Reikės patvirtinti leidimo prašymus", + "toast.model.none.title": "Nepasirinktas joks modelis", + "toast.model.none.description": "Prisijunkite prie teikėjo, kad apibendrintumėte šią sesiją", + "toast.file.loadFailed.title": "Nepavyko įkelti failo", + "toast.file.listFailed.title": "Nepavyko įtraukti failų sąrašo", + "toast.context.noLineSelection.title": "Nėra linijos pasirinkimo", + "toast.context.noLineSelection.description": "Pirmiausia failo skirtuke pasirinkite eilučių diapazoną.", + "toast.session.share.copyFailed.title": "Nepavyko nukopijuoti URL į mainų sritį", + "toast.session.share.success.title": "Seansas bendrinamas", + "toast.session.share.success.description": "Dalintis URL nukopijuotas į iškarpinę!", + "toast.session.share.failed.title": "Nepavyko bendrinti seanso", + "toast.session.share.failed.description": "Bendrinant seansą įvyko klaida", + "toast.session.unshare.success.title": "Seanso bendrinimas atšauktas", + "toast.session.unshare.success.description": "Seanso bendrinimas sėkmingai atšauktas!", + "toast.session.unshare.failed.title": "Nepavyko atšaukti seanso bendrinimo", + "toast.session.unshare.failed.description": "Atšaukiant seanso bendrinimą įvyko klaida", + "toast.session.export.success.title": "Seansas eksportuotas", + "toast.session.export.success.description": "Seansas išsaugotas {{filename}}", + "toast.session.export.failed.title": "Nepavyko eksportuoti seanso", + "toast.session.export.failed.description": "Eksportuojant seansą įvyko klaida", + "toast.session.listFailed.title": "Nepavyko įkelti seansų, skirtų {{project}}", + "toast.project.reloadFailed.title": "Nepavyko iš naujo įkelti {{project}}", + "toast.update.title": "Galimas atnaujinimas", + "toast.update.description": "Dabar galima įdiegti naują OpenCode ({{version}}) versiją.", + "toast.update.action.installRestart": "Įdiekite ir paleiskite iš naujo", + "toast.update.action.notYet": "Dar ne", + "error.page.title": "Kažkas nutiko", + "error.page.description": "Įkeliant programą įvyko klaida.", + "error.page.description.localServerStartup": "Paleidžiant vietinį serverį įvyko klaida.", + "error.page.details.label": "Išsami informacija apie klaidą", + "error.page.action.restart": "Paleisti iš naujo", + "error.page.action.report": "Pranešti apie klaidą", + "error.page.action.reported": "Pranešta apie klaidą", + "error.page.action.exportLogs": "Eksportuoti žurnalus", + "error.page.action.checking": "Tikrinama...", + "error.page.action.checkUpdates": "Patikrinkite, ar nėra atnaujinimų", + "error.page.action.updateTo": "Atnaujinkite į {{version}}", + "error.page.circular": "[Aplinkraštis]", + "error.page.report.prefix": "Praneškite apie šią klaidą OpenCode komandai", + "error.page.report.discord": "ant Discord", + "error.page.version": "Versija: {{version}}", + "error.dev.rootNotFound": + "Šakninis elementas nerastas. Ar pamiršote jį įtraukti į index.html? O gal id atributas buvo neteisingai parašytas?", + "error.serverSync.connectFailed": "Nepavyko prisijungti prie serverio. Ar `{{url}}` veikia serveris?", + "error.serverSDK.noServerAvailable": "Nėra serverio", + "error.serverSDK.serverNotAvailable": "Serveris nepasiekiamas", + "error.childStore.persistedCacheCreateFailed": "Nepavyko sukurti nuolatinės talpyklos", + "error.childStore.persistedProjectMetadataCreateFailed": "Nepavyko sukurti išliekančių projekto metaduomenų", + "error.childStore.persistedProjectIconCreateFailed": "Nepavyko sukurti išliekančio projekto piktogramos", + "error.childStore.storeCreateFailed": "Nepavyko sukurti parduotuvės", + "directory.error.invalidUrl": "Neteisingas katalogas URL.", + "error.chain.unknown": "Nežinoma klaida", + "error.server.invalidConfiguration": "Netinkama konfigūracija", + "error.chain.causedBy": "Sukėlė:", + "error.chain.apiError": "API klaida", + "error.chain.status": "Būsena: {{status}}", + "error.chain.retryable": "Galima pakartotinai: {{retryable}}", + "error.chain.responseBody": "Atsakymo turinys:\n{{body}}", + "error.chain.didYouMean": "Ar turėjote omenyje: {{suggestions}}", + "error.chain.modelNotFound": "Modelis nerastas: {{provider}}/{{model}}", + "error.chain.checkConfig": "Patikrinkite konfigūracijos (opencode.json) teikėjo / modelio pavadinimus", + "error.chain.mcpFailed": + "MCP serveris „{{name}}“ nepavyko. Atminkite, kad OpenCode dar nepalaiko MCP autentifikavimo.", + "error.chain.providerAuthFailed": "Teikėjo autentifikavimas nepavyko ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + "Nepavyko inicijuoti teikėjo „{{provider}}“. Patikrinkite kredencialus ir konfigūraciją.", + "error.chain.configJsonInvalid": "{{path}} konfigūracijos failas negalioja JSON(C)", + "error.chain.configJsonInvalidWithMessage": "{{path}} konfigūracijos failas negalioja JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + "Katalogas „{{dir}}“, esantis {{path}}, negalioja. Pervardykite katalogą į „{{suggestion}}“ arba pašalinkite jį. Tai dažna rašybos klaida.", + "error.chain.configFrontmatterError": "Nepavyko išanalizuoti frontmatter {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfigūracijos failas {{path}} yra neteisingas", + "error.chain.configInvalidWithMessage": "Konfigūracijos failas, esantis {{path}}, yra neteisingas: {{message}}", + "notification.permission.title": "Reikalingas leidimas", + "notification.permission.description": "{{sessionTitle}} {{projectName}} reikia leidimo", + "notification.question.title": "Klausimas", + "notification.question.description": "{{sessionTitle}} {{projectName}} turi klausimą", + "notification.action.goToSession": "Eiti į sesiją", + "notification.session.responseReady.title": "Atsakymas paruoštas", + "notification.session.error.title": "Seanso klaida", + "notification.session.error.fallbackDescription": "Įvyko klaida", + "home.recentProjects": "Naujausi projektai", + "home.empty.title": "Nėra naujausių projektų", + "home.empty.description": "Pradėkite atidarydami vietinį projektą", + "home.title": "Pagrindinis", + "home.projects": "Projektai", + "home.project.add": "Pridėti projektą", + "home.recentlyClosed": "Neseniai uždaryta", + "home.server.collapse": "Sutraukti serverio projektus", + "home.server.expand": "Išplėsti serverio projektus", + "home.sessions.search.placeholder": "Paieškos seansai", + "home.sessions.search.placeholder.scoped": "Ieškoti seansų {{scope}}", + "home.sessions.search.sessions": "Sesijos", + "home.sessions.search.noResults": "Nerasta jokių {{query}} seansų", + "home.sessions.empty": "Čia dar nieko", + "home.sessions.empty.description": "Norėdami pradėti, sukurkite sesiją", + "home.sessions.group.today": "Šiandien", + "home.sessions.group.yesterday": "vakar", + "home.sessions.group.older": "Vyresni", + "home.providerTip": + "Prisijunkite prie 75 ir daugiau tiekėjų, kad galėtumėte naudoti kitus modelius, įskaitant Claude, GPT, Gemini ir kt.", + "session.tab.session": "Seansas", + "session.tab.review": "Apžvalga", + "session.tab.context": "Kontekstas", + "session.tab.unknown": "Nežinomas seansas", + "session.panel.reviewAndFiles": "Apžvalga ir failai", + "session.error.notFound": "Šios sesijos rasti nepavyko", + "session.error.notFound.description": "Šis skirtukas nurodo seansą, kurio šiame serveryje nebėra.", + "session.error.notFound.closeTab": "Uždarykite Tab", + "session.error.serverConnection": "Nepavyksta prisijungti prie šio serverio", + "session.review.filesChanged": "Failai pakeisti {{count}}", + "session.review.change.one": "Keisti", + "session.review.change.other": "Pakeitimai", + "session.review.loadingChanges": "Įkeliami pakeitimai...", + "session.review.empty": "Pakeitimų šioje sesijoje dar nėra", + "session.review.noVcs": "Neaptikta Git versijos valdymo sistema, pakeitimai nerodomi", + "session.review.noVcs.createGit.title": "Sukurkite Git saugyklą", + "session.review.noVcs.createGit.description": "Stebėkite, peržiūrėkite ir anuliuokite šio projekto pakeitimus", + "session.review.noVcs.createGit.actionLoading": "Kuriama Git saugykla...", + "session.review.noVcs.createGit.action": "Sukurkite Git saugyklą", + "session.review.noSnapshot": + "Konfigūracijos metu momentinės nuotraukos stebėjimas išjungtas, todėl seanso pakeitimai nepasiekiami", + "session.review.noChanges": "Jokių pakeitimų", + "session.review.noUncommittedChanges": "Dar nėra nepadarytų pakeitimų", + "session.review.noBranchChanges": "Šakoje pakeitimų dar nėra", + "session.files.selectToOpen": "Pasirinkite failą, kurį norite atidaryti", + "session.files.all": "Visi failai", + "session.files.empty": "Failų nėra", + "session.files.binaryContent": "Dvejetainis failas (turinys negali būti rodomas)", + "session.messages.renderEarlier": "Pateikti ankstesnius pranešimus", + "session.messages.loadingEarlier": "Įkeliami ankstesni pranešimai...", + "session.messages.loadEarlier": "Įkelti ankstesnius pranešimus", + "session.messages.loading": "Įkeliami pranešimai...", + "session.messages.jumpToLatest": "Pereiti prie naujausio", + "session.context.addToContext": "Pridėkite {{selection}} prie konteksto", + "session.todo.title": "Užduotys", + "session.todo.collapse": "Sutraukti", + "session.todo.expand": "Išskleisti", + "session.todo.progress": "{{done}} iš {{total}} todos baigtas", + "session.question.progress": "{{current}} iš {{total}} klausimų", + "session.question.minimize": "Sumažinti klausimą", + "session.question.restore": "Atkurti klausimą", + "session.question.pending.one": "Laukiamas klausimas {{count}}", + "session.question.pending.other": "{{count}} laukiantys klausimai", + "session.question.pending.few": "{{count}} laukiantys klausimai", + "session.question.pending.many": "{{count}} laukiamo klausimo", + "session.followupDock.summary.one": "{{count}} eilėje esantis pranešimas", + "session.followupDock.summary.other": "{{count}} eilėje esantys pranešimai", + "session.followupDock.summary.few": "{{count}} eilėje esantys pranešimai", + "session.followupDock.summary.many": "{{count}} eilėje esančio pranešimo", + "session.followupDock.sendNow": "Siųsti dabar", + "session.followupDock.edit": "Redaguoti", + "session.followupDock.collapse": "Sutraukti eilėje esančius pranešimus", + "session.followupDock.expand": "Išplėskite eilėje esančius pranešimus", + "session.revertDock.summary.one": "{{count}} atšauktas pranešimas", + "session.revertDock.summary.other": "{{count}} atšaukti pranešimai", + "session.revertDock.summary.few": "{{count}} atšaukti pranešimai", + "session.revertDock.summary.many": "{{count}} atšaukto pranešimo", + "session.revertDock.collapse": "Sutraukti atšauktus pranešimus", + "session.revertDock.expand": "Išplėskite atšauktus pranešimus", + "session.revertDock.restore": "Atkurti pranešimą", + "session.new.title": "Statyti bet ką", + "session.new.project.new": "Naujas projektas", + "session.new.project.search": "Ieškoti projektų", + "session.new.project.add": "Pridėti projektą", + "session.new.worktree.main": "Pagrindinė šaka", + "session.new.worktree.mainWithBranch": "Pagrindinė šaka ({{branch}})", + "session.new.worktree.create": "Sukurti naują darbo medį", + "session.new.workspace.runIn": "Vykdykite sesiją", + "session.new.workspace.triggerLocal": "Vietinis", + "session.new.workspace.local": "Vietinė saugykla", + "session.new.workspace.existing": "Darbo sritis…", + "session.new.git.none": "Nėra Git", + "session.new.lastModified": "Paskutinis modifikuotas", + "session.header.search.placeholder": "Ieškoti {{project}}", + "session.header.searchFiles": "Ieškokite failų", + "session.header.openIn": "Atidaryti", + "session.header.open.action": "Atidarykite {{app}}", + "session.header.open.ariaLabel": "Atidaryti {{app}}", + "session.header.open.menu": "Atidarykite parinktis", + "session.header.open.copyPath": "Kopijuoti kelią", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "Failų tvarkytuvė", + "session.header.reveal.finder": "Atskleiskite Finder", + "session.header.reveal.fileExplorer": "Atskleiskite File Explorer", + "session.header.reveal.containingFolder": "Atidarykite aplanką, kuriame yra", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminalas", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Būsena", + "status.popover.ariaLabel": "Serverio konfigūracijos", + "status.popover.tab.servers": "Serveriai", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Papildiniai", + "status.popover.action.manageServers": "Tvarkyti serverius", + "session.share.popover.title": "Paskelbti internete", + "session.share.popover.description.shared": "Ši sesija yra vieša internete. Ją gali pasiekti visi, turintys nuorodą.", + "session.share.popover.description.unshared": + "Bendrinkite sesiją viešai žiniatinklyje. Ją galės pasiekti visi, turintys nuorodą.", + "session.share.action.share": "Dalintis", + "session.share.action.publish": "Paskelbti", + "session.share.action.publishing": "Skelbiama...", + "session.share.action.unpublish": "Atšaukti paskelbimą", + "session.share.action.unpublishing": "Atšaukiamas paskelbimas...", + "session.share.action.view": "Rodinys", + "session.share.copy.copied": "Nukopijuota", + "session.share.copy.copyLink": "Nukopijuoti nuorodą", + "lsp.tooltip.none": "Nėra LSP serverių", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Įkeliamas raginimas...", + "terminal.loading": "Įkeliamas terminalas...", + "terminal.title": "Terminalas", + "terminal.title.numbered": "Terminalas {{number}}", + "terminal.close": "Uždarykite terminalą", + "terminal.connectionLost.title": "Ryšys nutrūko", + "terminal.connectionLost.abnormalClose": "WebSocket neįprastai uždarytas: {{code}}", + "terminal.connectionLost.description": + "Terminalo ryšys nutrūko. Tai gali atsitikti, kai serveris paleidžiamas iš naujo.", + "terminal.connectTicket.csrfError": + "PTY prijungimo bilietas atmestas dėl kilmės arba CSRF patikrinimų. Patikrinkite serverio CORS konfigūraciją.", + "terminal.connectTicket.statusError": "PTY prisijungimo bilietas nepavyko naudojant {{status}}", + "titlebar.update": "Atnaujinti", + "titlebar.updateVersion": "Atnaujinkite {{version}}", + "common.closeTab": "Uždaryti skirtuką", + "common.dismiss": "Atsisakyti", + "common.moreCountSuffix": "(+{{count}} daugiau)", + "common.requestFailed": "Užklausa nepavyko", + "common.moreOptions": "Daugiau parinkčių", + "common.learnMore": "Sužinokite daugiau", + "common.rename": "Pervardyti", + "common.export": "Eksportuoti", + "common.reset": "Nustatyti iš naujo", + "common.archive": "Archyvas", + "common.delete": "Ištrinti", + "common.close": "Uždaryti", + "common.edit": "Redaguoti", + "common.loadMore": "Įkelti daugiau", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Ištrinti", + "common.key.home": "Pagrindinis", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Įterpti", + "common.unknown": "nežinomas", + "common.time.justNow": "Tik dabar", + "common.time.minutesAgo.short": "Prieš {{count}}m", + "common.time.hoursAgo.short": "Prieš {{count}}h", + "common.time.daysAgo.short": "Prieš {{count}}d", + "sidebar.menu.toggle": "Perjungti meniu", + "sidebar.nav.projectsAndSessions": "Projektai ir užsiėmimai", + "sidebar.settings": "Nustatymai", + "sidebar.help": "Pagalba", + "sidebar.workspaces.enable": "Įgalinti darbo sritis", + "sidebar.workspaces.disable": "Išjungti darbo sritis", + "sidebar.gettingStarted.title": "Darbo pradžia", + "sidebar.gettingStarted.line1": "OpenCode apima nemokamus modelius, todėl galite pradėti iš karto.", + "sidebar.gettingStarted.line2": + "Prijunkite bet kurį teikėją, kad galėtumėte naudoti modelius, įskaitant. Claude, GPT, Gemini ir kt.", + "sidebar.project.recentSessions": "Naujausi seansai", + "sidebar.project.viewAllSessions": "Peržiūrėkite visas sesijas", + "sidebar.project.clearNotifications": "Išvalyti pranešimus", + "sidebar.empty.title": "Jokių atvirų projektų", + "sidebar.empty.description": "Norėdami pradėti, atidarykite projektą", + "debugBar.ariaLabel": "Vystymosi veiklos diagnostika", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Paskutinis baigtas maršruto perėjimas, liečiant seanso puslapį, matuojant nuo maršrutizatoriaus pradžios iki pirmojo dažymo po to, kai jis nusistovi.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Slenkantys kadrai per sekundę per paskutines 5 sekundes.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Blogiausias kadro laikas per paskutines 5 sekundes.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Kadrai daugiau nei 32 ms per paskutines 5 sekundes.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Užblokuotas laikas ir ilgų užduočių skaičius per paskutines 5 sekundes. Maksimali užduotis: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Blogiausias pastebėtas įvesties delsimas per paskutines 5 sekundes.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Apytikslė sąveikos trukmė per paskutines 5 sekundes. Tai panašu į INP, o ne į oficialų „Web Vitals INP“.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Suminis išdėstymo poslinkis dabartiniam programos veikimo laikui.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Naudota JS krūva ir krūvos riba. Tik Chromium.", + "debugBar.mem.tip": "Naudota JS krūva ir krūvos riba. {{used}} arba {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Priverskite fokusuoti visus interaktyvius elementus", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Priverskite visą programos išdėstymo kryptį nekeisdami pasirinktos kalbos", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode darbalaukis", + "settings.section.desktop": "Darbalaukis", + "settings.section.server": "Serveris", + "settings.tab.general": "Bendra", + "settings.tab.shortcuts": "Spartieji klavišai", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integracija", + "settings.desktop.wsl.description": "Paleiskite OpenCode serverį WSL viduje Windows.", + "settings.general.section.appearance": "Išvaizda", + "settings.general.section.advanced": "Išplėstinė", + "settings.general.section.notifications": "Sistemos pranešimai", + "settings.general.section.updates": "Atnaujinimai", + "settings.general.section.sounds": "Garso efektai", + "settings.general.section.feed": "Pašaras", + "settings.general.section.display": "Ekranas", + "settings.general.row.language.title": "Kalba", + "settings.general.row.language.description": "Pakeiskite OpenCode ekrano kalbą", + "settings.general.row.shell.title": "Terminalo apvalkalas", + "settings.general.row.shell.description": "Terminalo ir agento įrankių naudojamas apvalkalas", + "settings.general.row.shell.autoDefault": "Automatinis (numatytasis)", + "settings.general.row.shell.terminalOnly": "tik terminalas", + "settings.general.row.appearance.title": "Išvaizda", + "settings.general.row.appearance.description": "Tinkinkite, kaip OpenCode atrodo jūsų įrenginyje", + "settings.general.row.colorScheme.title": "Spalvų schema", + "settings.general.row.colorScheme.description": "Pasirinkite, ar OpenCode seka sistemos, šviesios ar tamsios temos", + "settings.general.row.theme.title": "tema", + "settings.general.row.theme.description": "Tinkinkite OpenCode temą.", + "settings.general.row.font.title": "Kodo šriftas", + "settings.general.row.font.description": "Tinkinkite kodo blokuose naudojamą šriftą", + "settings.general.row.terminalFont.title": "Šriftas Terminal", + "settings.general.row.terminalFont.description": "Tinkinkite terminale naudojamą šriftą", + "settings.general.row.uiFont.title": "UI šriftas", + "settings.general.row.uiFont.description": "Tinkinkite visoje sąsajoje naudojamą šriftą", + "settings.general.row.followup.title": "Tolesnis elgesys", + "settings.general.row.followup.description": + "Pasirinkite, ar tolesni raginimai bus nukreipti nedelsiant, ar laukti eilėje", + "settings.general.row.followup.option.queue": "Eilė", + "settings.general.row.followup.option.steer": "Vairuoti", + "settings.general.row.showFileTree.title": "Failų medis", + "settings.general.row.showFileTree.description": "Rodyti failų medžio skydelį seansuose", + "settings.general.row.showNavigation.title": "Navigacijos valdikliai", + "settings.general.row.showNavigation.description": "Rodyti mygtukus atgal ir pirmyn darbalaukio pavadinimo juostoje", + "settings.general.row.showSearch.title": "Komandų paletė", + "settings.general.row.showSearch.description": "Rodyti paieškos ir komandų paletės mygtuką pavadinimo juostoje", + "settings.general.row.showTerminal.title": "Terminalas", + "settings.general.row.showTerminal.description": "Rodyti terminalo mygtuką darbalaukio pavadinimo juostoje", + "settings.general.row.showStatus.title": "Serverio būsena", + "settings.general.row.showStatus.description": "Rodyti serverio būsenos mygtuką pavadinimo juostoje", + "settings.general.row.mobileTitlebarBottom.title": "Apatinė navigacija", + "settings.general.row.mobileTitlebarBottom.description": + "Įdėkite pavadinimo juostą ir seanso skirtukus mobiliojo telefono ekrano apačioje", + "settings.general.row.showCustomAgents.title": "Rodyti agentą", + "settings.general.row.showCustomAgents.description": + "Perjunkite tarp kompozitoriaus agentų. Kai paslėpta, numatytasis kūrimo agentas.", + "settings.general.row.reasoningSummaries.title": "Rodyti samprotavimų santraukas", + "settings.general.row.reasoningSummaries.description": "Rodyti modelio motyvų santraukas laiko juostoje", + "settings.general.row.shellToolPartsExpanded.title": "Išskleiskite apvalkalo įrankių dalis", + "settings.general.row.shellToolPartsExpanded.description": + "Rodyti pagal numatytuosius nustatymus išplėstas apvalkalo įrankio dalis laiko juostoje", + "settings.general.row.editToolPartsExpanded.title": "Išplėskite redagavimo įrankio dalis", + "settings.general.row.editToolPartsExpanded.description": + "Rodyti pagal numatytuosius nustatymus laiko juostoje išplėstas redagavimo, rašymo ir pataisymo įrankio dalis", + "settings.general.row.newInterface.title": "Naujas išdėstymas", + "settings.general.row.newInterface.badge": "Nauja", + "settings.general.row.newInterface.description": + "Naudokite naujus skirtukus ir pagrindinio puslapio išdėstymą. Perjungti išdėstymą ribotą laiką.", + "settings.general.row.newInterfaceNotice.title": "Dabar naudojate naują išdėstymą", + "settings.general.row.newInterfaceNotice.description": "Ankstesnis išdėstymas nebepasiekiamas", + "settings.general.row.newInterfaceNotice.dismiss": "Atsisakyti", + "settings.general.row.pinchZoom.title": "Suimkite, kad padidintumėte mastelį", + "settings.general.row.pinchZoom.description": "Leiskite priartinti valdiklį ir Ctrl slinkimo gestus", + "settings.general.row.wayland.title": "Naudokite vietinį Wayland", + "settings.general.row.wayland.description": "Išjungti atsarginį X11 Wayland. Reikia paleisti iš naujo.", + "settings.general.row.wayland.tooltip": + "Linux su mišraus atnaujinimo dažnio monitoriais vietinis Wayland gali būti stabilesnis.", + "settings.general.row.releaseNotes.title": "Išleidimo pastabos", + "settings.general.row.releaseNotes.description": "Po atnaujinimų rodyti iššokančius langus „Kas naujo“.", + "settings.updates.row.startup.title": "Patikrinkite, ar nėra naujinimų paleidžiant", + "settings.updates.row.startup.description": "Automatiškai patikrinkite, ar nėra naujinimų, kai paleidžiamas OpenCode", + "settings.updates.row.check.title": "Patikrinkite, ar nėra atnaujinimų", + "settings.updates.row.check.description": "Rankiniu būdu patikrinkite, ar nėra naujinimų, ir įdiekite, jei yra", + "settings.updates.action.checkNow": "Patikrinkite dabar", + "settings.updates.action.checking": "Tikrinama...", + "settings.updates.action.downloading": "Atsisiunčiama...", + "settings.updates.action.installing": "Diegiama...", + "settings.updates.toast.latest.title": "Jūs esate atnaujintas", + "settings.updates.toast.latest.description": "Naudojate naujausią OpenCode versiją.", + "sound.option.none": "Nėra", + "sound.option.alert01": "Įspėjimas 01", + "sound.option.alert02": "Įspėjimas 02", + "sound.option.alert03": "Įspėjimas 03", + "sound.option.alert04": "Įspėjimas 04", + "sound.option.alert05": "Įspėjimas 05", + "sound.option.alert06": "Įspėjimas 06", + "sound.option.alert07": "Įspėjimas 07", + "sound.option.alert08": "Įspėjimas 08", + "sound.option.alert09": "Įspėjimas 09", + "sound.option.alert10": "Įspėjimas 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Ne 01", + "sound.option.nope02": "Ne 02", + "sound.option.nope03": "Ne 03", + "sound.option.nope04": "Ne 04", + "sound.option.nope05": "Ne 05", + "sound.option.nope06": "Ne 06", + "sound.option.nope07": "Ne 07", + "sound.option.nope08": "Ne 08", + "sound.option.nope09": "Ne 09", + "sound.option.nope10": "Ne 10", + "sound.option.nope11": "Ne 11", + "sound.option.nope12": "Ne 12", + "sound.option.yup01": "Taip 01", + "sound.option.yup02": "Taip 02", + "sound.option.yup03": "Taip 03", + "sound.option.yup04": "Taip 04", + "sound.option.yup05": "Taip 05", + "sound.option.yup06": "Taip 06", + "settings.general.notifications.agent.title": "Agentas", + "settings.general.notifications.agent.description": + "Rodyti sistemos pranešimą, kai agentas baigtas arba jam reikia dėmesio", + "settings.general.notifications.permissions.title": "Leidimai", + "settings.general.notifications.permissions.description": "Rodyti sistemos pranešimą, kai reikalingas leidimas", + "settings.general.notifications.errors.title": "Klaidos", + "settings.general.notifications.errors.description": "Rodyti sistemos pranešimą, kai įvyksta klaida", + "settings.general.sounds.agent.title": "Agentas", + "settings.general.sounds.agent.description": "Leisti garsą, kai agentas baigtas arba jam reikia dėmesio", + "settings.general.sounds.permissions.title": "Leidimai", + "settings.general.sounds.permissions.description": "Leisti garsą, kai reikia leidimo", + "settings.general.sounds.errors.title": "Klaidos", + "settings.general.sounds.errors.description": "Leisti garsą, kai įvyksta klaida", + "settings.shortcuts.title": "Spartieji klavišai", + "settings.shortcuts.reset.button": "Atstatyti į numatytuosius nustatymus", + "settings.shortcuts.reset.toast.title": "Spartieji klavišai nustatyti iš naujo", + "settings.shortcuts.reset.toast.description": "Spartieji klavišai buvo atkurti į numatytuosius nustatymus.", + "settings.shortcuts.conflict.title": "Spartusis klavišas jau naudojamas", + "settings.shortcuts.conflict.description": "{{keybind}} jau priskirtas {{titles}}.", + "settings.shortcuts.unassigned": "Nepriskirta", + "settings.shortcuts.pressKeys": "Paspauskite klavišus", + "settings.shortcuts.search.placeholder": "Paieškos spartieji klavišai", + "settings.shortcuts.search.empty": "Nerasta jokių sparčiųjų klavišų", + "settings.shortcuts.group.general": "Bendra", + "settings.shortcuts.group.session": "Seansas", + "settings.shortcuts.group.navigation": "Navigacija", + "settings.shortcuts.group.modelAndAgent": "Modelis ir agentas", + "settings.shortcuts.group.terminal": "Terminalas", + "settings.shortcuts.group.prompt": "Užklausa", + "settings.providers.title": "Teikėjai", + "settings.providers.description": "Teikėjo nustatymus bus galima konfigūruoti čia.", + "settings.providers.section.connected": "Prisijungę teikėjai", + "settings.providers.connected.empty": "Nėra prisijungusių teikėjų", + "settings.providers.connected.environmentDescription": "Prisijungta iš jūsų aplinkos kintamųjų", + "settings.providers.section.popular": "Populiarūs tiekėjai", + "settings.providers.custom.description": "Pridėkite su OpenAI suderinamą tiekėją pagal bazinį URL.", + "settings.providers.tag.environment": "Aplinka", + "settings.providers.tag.config": "Konfigūracija", + "settings.providers.tag.custom": "Pasirinktinis", + "settings.providers.tag.other": "Kita", + "settings.models.title": "Modeliai", + "settings.models.description": "Čia bus konfigūruojami modelio nustatymai.", + "settings.agents.title": "Agentai", + "settings.agents.description": "Agento nustatymus bus galima konfigūruoti čia.", + "settings.commands.title": "Komandos", + "settings.commands.description": "Čia bus galima konfigūruoti komandų nustatymus.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Čia bus galima konfigūruoti MCP nustatymus.", + "settings.permissions.title": "Leidimai", + "settings.permissions.description": + "Valdykite, kokius įrankius serveris gali naudoti pagal numatytuosius nustatymus.", + "settings.permissions.section.tools": "Įrankiai", + "settings.permissions.toast.updateFailed.title": "Nepavyko atnaujinti leidimų", + "settings.permissions.action.allow": "Leisti", + "settings.permissions.action.ask": "Paklausk", + "settings.permissions.action.deny": "Neigti", + "settings.permissions.tool.read.title": "Skaityti", + "settings.permissions.tool.read.description": "Failo skaitymas (atitinka failo kelią)", + "settings.permissions.tool.edit.title": "Redaguoti", + "settings.permissions.tool.edit.description": "Keiskite failus, įskaitant redagavimus, įrašus ir pataisas", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Suderinkite failus naudodami globos šablonus", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Ieškokite failo turinio naudodami įprastus posakius", + "settings.permissions.tool.list.title": "Sąrašas", + "settings.permissions.tool.list.description": "Išvardykite failus kataloge", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Vykdykite apvalkalo komandas", + "settings.permissions.tool.task.title": "Užduotis", + "settings.permissions.tool.task.description": "Paleisti antrinius agentus", + "settings.permissions.tool.skill.title": "Įgūdis", + "settings.permissions.tool.skill.description": "Įkelkite įgūdžius pagal pavadinimą", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Vykdykite kalbos serverio užklausas", + "settings.permissions.tool.todowrite.title": "Todo Rašyti", + "settings.permissions.tool.todowrite.description": "Atnaujinkite darbų sąrašą", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Gaukite turinį iš URL", + "settings.permissions.tool.websearch.title": "Interneto paieška", + "settings.permissions.tool.websearch.description": "Ieškokite internete", + "settings.permissions.tool.external_directory.title": "Išorinis katalogas", + "settings.permissions.tool.external_directory.description": "Pasiekite failus už projekto katalogo ribų", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": + "Aptikti pasikartojančius įrankių iškvietimus naudojant identišką įvestį", + "session.delete.failed.title": "Nepavyko ištrinti sesijos", + "session.delete.title": "Ištrinti seansą", + "session.delete.confirm": 'Ištrinti seansą "{{name}}"?', + "session.delete.button": "Ištrinti seansą", + "workspace.new": "Nauja darbo vieta", + "workspace.type.local": "vietinis", + "workspace.type.sandbox": "smėlio dėžė", + "workspace.create.failed.title": "Nepavyko sukurti darbo vietos", + "workspace.delete.failed.title": "Nepavyko ištrinti darbo srities", + "workspace.resetting.title": "Iš naujo nustatoma darbo sritis", + "workspace.resetting.description": "Tai gali užtrukti minutę.", + "workspace.reset.failed.title": "Nepavyko iš naujo nustatyti darbo srities", + "workspace.reset.success.title": "Darbo sritis nustatyta iš naujo", + "workspace.reset.success.description": "Dabar darbo sritis atitinka numatytąją šaką.", + "workspace.error.stillPreparing": "Darbo vieta vis dar ruošiama", + "workspace.status.checking": "Tikrinama, ar nėra nesujungtų pakeitimų...", + "workspace.status.error": "Nepavyko patvirtinti git būsenos.", + "workspace.status.clean": "Neaptikta jokių nesujungtų pakeitimų.", + "workspace.status.dirty": "Šioje darbo srityje aptikti nesujungti pakeitimai.", + "workspace.delete.title": "Ištrinti darbo sritį", + "workspace.delete.confirm": 'Ištrinti darbo sritį "{{name}}"?', + "workspace.delete.button": "Ištrinti darbo sritį", + "workspace.reset.title": "Iš naujo nustatyti darbo sritį", + "workspace.reset.confirm": "Iš naujo nustatyti darbo sritį „{{name}}“?", + "workspace.reset.button": "Iš naujo nustatyti darbo sritį", + "workspace.reset.archived.none": "Jokia aktyvi sesija nebus archyvuojama.", + "workspace.reset.archived.one": "1 sesija bus suarchyvuota.", + "workspace.reset.archived.many": "{{count}} sesijos bus archyvuojamos.", + "workspace.reset.note": "Tai iš naujo nustatys darbo sritį, kad ji atitiktų numatytąją šaką.", + "dialog.usageExceeded.dontShowAgain": "Daugiau nerodyti", +} diff --git a/packages/app/src/i18n/lv.ts b/packages/app/src/i18n/lv.ts new file mode 100644 index 0000000000000000000000000000000000000000..df4c5b2dcd9134b04edcef8bb2510cb4181e33f1 --- /dev/null +++ b/packages/app/src/i18n/lv.ts @@ -0,0 +1,1162 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Fails", + "desktop.menu.edit": "Rediģēt", + "desktop.menu.view": "Skats", + "desktop.menu.go": "Iet", + "desktop.menu.window": "Logs", + "desktop.menu.help": "Palīdzība", + "desktop.menu.checkForUpdates": "Pārbaudīt atjauninājumus...", + "desktop.menu.settings": "Iestatījumi", + "desktop.menu.reloadWebview": "Pārlādēt tīmekļa skatu", + "desktop.menu.restart": "Restartēt", + "desktop.menu.exportLogs": "Eksportēt žurnālus...", + "desktop.menu.newSession": "Jauna sesija", + "desktop.menu.openProject": "Atvērt projektu...", + "desktop.menu.newWindow": "Jauns logs", + "desktop.menu.closeWindow": "Aizvērt logu", + "desktop.menu.undo": "Atsaukt", + "desktop.menu.redo": "Atkārtot", + "desktop.menu.cut": "Izgriezt", + "desktop.menu.copy": "Kopēt", + "desktop.menu.paste": "Ielīmēt", + "desktop.menu.delete": "Dzēst", + "desktop.menu.selectAll": "Atlasīt visu", + "desktop.menu.toggleSidebar": "Rādīt/Slēpt sānjoslu", + "desktop.menu.toggleTerminal": "Rādīt/Slēpt termināli", + "desktop.menu.toggleFileTree": "Rādīt/Slēpt failu koku", + "desktop.menu.reload": "Pārlādēt", + "desktop.menu.toggleDeveloperTools": "Rādīt/Slēpt izstrādātāja rīkus", + "desktop.menu.actualSize": "Sākotnējais izmērs", + "desktop.menu.zoomIn": "Tuvināt", + "desktop.menu.zoomOut": "Samazināt", + "desktop.menu.toggleFullScreen": "Pilnekrāna režīms", + "desktop.menu.back": "Atpakaļ", + "desktop.menu.forward": "Uz priekšu", + "desktop.menu.previousSession": "Iepriekšējā sesija", + "desktop.menu.nextSession": "Nākamā sesija", + "desktop.menu.previousProject": "Iepriekšējais projekts", + "desktop.menu.nextProject": "Nākamais projekts", + "desktop.menu.minimize": "Minimizēt", + "desktop.menu.maximize": "Maksimizēt", + "desktop.menu.documentation": "OpenCode dokumentācija", + "desktop.menu.supportForum": "Atbalsta forums", + "desktop.menu.shareFeedback": "Sniegt atsauksmi", + "desktop.menu.reportBug": "Ziņot par kļūdu", + "desktop.menu.ariaLabel": "OpenCode izvēlne", + "desktop.updater.dialog.checkFailed.message": "Neizdevās pārbaudīt atjauninājumus.", + "desktop.updater.dialog.checkFailed.title": "Atjaunināšanas kļūda", + "desktop.updater.dialog.upToDate.message": "Jums ir jaunākā versija.", + "desktop.updater.dialog.upToDate.title": "Nav atjauninājumu", + "desktop.updater.dialog.ready.message": "Atjauninājums {{version}} lejupielādēts. Vai pārstartēt tagad?", + "desktop.updater.dialog.ready.title": "Atjauninājums gatavs", + "desktop.updater.dialog.restart": "Pārstartēt", + "desktop.updater.dialog.later": "Vēlāk", + "desktop.recovery.action.relaunch": "Palaist no jauna", + "desktop.recovery.action.exportLogs": "Eksportēt žurnālus", + "desktop.recovery.action.keepWaiting": "Turpināt gaidīt", + "desktop.recovery.action.quit": "Iziet", + "desktop.recovery.loadFailed": "Neizdevās ielādēt OpenCode", + "desktop.recovery.terminated": "OpenCode logs negaidīti aizvērts", + "desktop.recovery.unresponsive": "OpenCode neatbild", + "desktop.recovery.unresponsive.detail": "Varat palaist lietotni no jauna, atvērt žurnālus vai turpināt gaidīt.", + "desktop.recovery.loadFailed.detail": "Logs: {{window}}\nURL: {{url}}\nKļūda: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Logs: {{window}}\nIemesls: {{reason}}\nKods: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Izvēlieties mapi", + "desktop.dialog.chooseFile": "Izvēlieties failu", + "desktop.dialog.saveFile": "Saglabāt failu", + "desktop.dialog.files": "Faili", + "desktop.server.local": "Lokālais serveris", + "desktop.wsl.error.windowsOnly": "WSL ir pieejams tikai Windows", + "desktop.wsl.error.unavailable": "WSL nav pieejams", + "desktop.wsl.error.listInstalled": "Neizdevās uzskaitīt instalētās WSL distribūcijas", + "desktop.wsl.error.listOnline": "Neizdevās uzskaitīt tiešsaistes WSL distribūcijas", + "desktop.wsl.error.executeDistro": "Nevar izpildīt komandas distribūcijā", + "desktop.wsl.error.installWsl": "WSL instalēšana neizdevās", + "desktop.wsl.error.installDistro": "Neizdevās instalēt distribūciju: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode instalēšana neizdevās", + "desktop.wsl.error.alreadyAdded": "{{distro}} jau ir pievienota", + "desktop.wsl.error.opencodeMissing": "opencode nav instalēts šajā distribūcijā", + "desktop.wsl.error.opencodeCannotRun": "opencode ir instalēts, bet to nevar palaist", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode nav instalēts {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode atjaunināšana pabeigta, bet {{distro}} joprojām rāda {{installed}}; gaidīts {{expected}}", + "desktop.wsl.error.noVersion": "nav versijas", + "desktop.wsl.error.serverExited": "WSL serveris izslēdzās pēc palaišanas (kods={{code}} signāls={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL serveris izslēdzās pirms kļuva gatavs darbam (kods={{code}} signāls={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} pārbaudes Sidecar laiks beidzās pēc {{timeout}} ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} laiks beidzās pēc {{timeout}} ms", + "desktop.wsl.error.failedPort": "Neizdevās iegūt portu", + "desktop.picker.error.notSelected": "Fails netika izvēlēts ar atlasītāju", + "desktop.picker.error.sizeLimit": "Izvēlētie pielikumi pārsniedz {{limit}} MB ierobežojumu", + "command.category.suggested": "Ieteikts", + "command.category.view": "Skats", + "command.category.project": "Projekts", + "command.category.provider": "Pakalpojumu sniedzējs", + "command.category.server": "Serveris", + "command.category.session": "Sesija", + "command.category.theme": "Tēma", + "command.category.language": "Valoda", + "command.category.file": "Fails", + "command.category.context": "Konteksts", + "command.category.terminal": "Terminālis", + "command.category.model": "Modelis", + "command.category.mcp": "MCP", + "command.category.agent": "Aģents", + "command.category.permissions": "Atļaujas", + "command.category.workspace": "Darbtelpa", + "command.category.settings": "Iestatījumi", + "command.logs.export": "Eksportēt žurnālus", + "theme.scheme.system": "Sistēma", + "theme.scheme.light": "Gaišs", + "theme.scheme.dark": "Tumšs", + "command.sidebar.toggle": "Pārslēgt sānjoslu", + "command.project.open": "Atvērt projektu", + "command.project.previous": "Iepriekšējais projekts", + "command.project.next": "Nākamais projekts", + "command.project.index": "Pārslēgties uz projektu {{index}}", + "command.provider.connect": "Pievienot pakalpojumu sniedzēju", + "command.server.switch": "Pārslēgt serveri", + "command.settings.open": "Atvērt iestatījumus", + "command.session.previous": "Iepriekšējā sesija", + "command.session.next": "Nākamā sesija", + "command.session.previous.unseen": "Iepriekšējā nelasītā sesija", + "command.session.next.unseen": "Nākamā nelasītā sesija", + "command.session.archive": "Arhivēt sesiju", + "command.palette": "Komandu palete", + "command.theme.cycle": "Mainīt motīvu", + "command.theme.set": "Izmantot motīvu: {{theme}}", + "command.theme.scheme.cycle": "Mainīt krāsu shēmu", + "command.theme.scheme.set": "Izmantot krāsu shēmu: {{scheme}}", + "command.language.cycle": "Mainīt valodu", + "command.language.set": "Izmantot valodu: {{language}}", + "command.session.new": "Jauna sesija", + "command.file.open": "Atvērt failu", + "command.tab.close": "Aizvērt cilni", + "command.tab.reopenClosed": "Atvērt aizvērtu cilni", + "command.context.addSelection": "Pievienot atlasi kontekstam", + "command.context.addSelection.description": "Pievienot atlasītās rindas no pašreizējā faila", + "command.input.focus": "Aktivizēt ievadi", + "command.terminal.toggle": "Rādīt/paslēpt termināli", + "command.fileTree.toggle": "Rādīt/paslēpt failu koku", + "command.review.toggle": "Rādīt/paslēpt pārskatu", + "command.terminal.new": "Jauns terminālis", + "command.terminal.new.description": "Izveidot jaunu termināļa cilni", + "command.steps.toggle": "Rādīt/paslēpt soļus", + "command.steps.toggle.description": "Rādīt vai slēpt soļus šim ziņojumam", + "command.message.previous": "Iepriekšējais ziņojums", + "command.message.previous.description": "Pāriet uz iepriekšējo lietotāja ziņojumu", + "command.message.next": "Nākamais ziņojums", + "command.message.next.description": "Pāriet uz nākamo lietotāja ziņojumu", + "command.model.choose": "Izvēlēties modeli", + "command.model.choose.description": "Izvēlieties citu modeli", + "command.mcp.toggle": "Pārslēgt MCP", + "command.mcp.toggle.description": "Pārslēgt MCP", + "command.agent.cycle": "Mainīt aģentu", + "command.agent.cycle.description": "Pārslēgt uz nākamo aģentu", + "command.agent.cycle.reverse": "Mainīt aģentu atpakaļ", + "command.agent.cycle.reverse.description": "Pārslēgt uz iepriekšējo aģentu", + "command.model.variant.cycle": "Mainīt domāšanas līmeni", + "command.model.variant.cycle.description": "Pārslēgt uz nākamo līmeni", + "command.prompt.mode.shell": "Čaula", + "command.prompt.mode.normal": "Uzvedne", + "command.permissions.autoaccept.enable": "Automātiski pieņemt atļaujas", + "command.permissions.autoaccept.disable": "Apturēt automātisku atļauju pieņemšanu", + "command.workspace.toggle": "Pārslēgt darbvietas", + "command.workspace.toggle.description": "Ieslēgt vai izslēgt vairākas darbvietas sānjoslā", + "command.session.undo": "Atsaukt", + "command.session.undo.description": "Atsaukt pēdējo ziņojumu", + "command.session.redo": "Atatsaukt", + "command.session.redo.description": "Atatsaukt pēdējo atsaukto ziņojumu", + "command.session.compact": "Saspiest sesiju", + "command.session.compact.description": "Kopsavilkt sesiju, lai samazinātu konteksta apjomu", + "command.session.fork": "Atzarot no ziņojuma", + "command.session.fork.description": "Izveidot jaunu sesiju no iepriekšēja ziņojuma", + "command.session.share": "Kopīgot sesiju", + "command.session.share.description": "Kopīgot šo sesiju un kopēt URL starpliktuvē", + "command.session.unshare": "Pārtraukt kopīgošanu", + "command.session.unshare.description": "Pārtraukt šīs sesijas kopīgošanu", + "command.session.export": "Eksportēt sesiju", + "command.session.export.description": "Eksportēt pilnu sesijas transkriptu kā JSON", + "palette.search.placeholder": "Meklēt failus, komandas un sesijas", + "palette.search.placeholder.home": "Meklēt komandas un sesijas", + "palette.empty": "Rezultāti nav atrasti", + "palette.group.commands": "Komandas", + "palette.group.files": "Faili", + "dialog.provider.search.placeholder": "Meklēt nodrošinātājus", + "dialog.provider.empty": "Nodrošinātāji nav atrasti", + "dialog.provider.group.popular": "Populāri", + "dialog.provider.group.other": "Citi", + "dialog.provider.custom.label": "Pielāgots OpenAI saderīgs nodrošinātājs", + "dialog.provider.tag.recommended": "Ieteikts", + "dialog.provider.opencode.note": "Atlasīti modeļi, tostarp Claude, GPT, Gemini un citi", + "dialog.provider.opencode.tagline": "Uzticami optimizēti modeļi", + "dialog.provider.opencodeGo.tagline": "Zemas cenas abonements visiem", + "dialog.provider.anthropic.note": "Tieša piekļuve Claude modeļiem, tostarp Pro un Max", + "dialog.provider.copilot.note": "AI modeļi programmēšanas atbalstam ar GitHub Copilot", + "dialog.provider.openai.note": "GPT modeļi ātriem, spējīgiem vispārējiem AI uzdevumiem", + "dialog.provider.google.note": "Gemini modeļi ātrām, strukturētām atbildēm", + "dialog.provider.openrouter.note": "Piekļuve visiem atbalstītajiem modeļiem no viena nodrošinātāja", + "dialog.provider.vercel.note": "Vienota piekļuve AI modeļiem ar viedu maršrutēšanu", + "dialog.model.select.title": "Izvēlieties modeli", + "dialog.model.search.placeholder": "Meklēt modeļus", + "dialog.model.empty": "Modeļi nav atrasti", + "dialog.model.manage": "Pārvaldīt modeļus", + "dialog.model.manage.description": "Pielāgojiet, kuri modeļi tiek rādīti modeļu atlasītājā.", + "dialog.model.manage.provider.toggle": "Pārslēgt visus {{provider}} modeļus", + "dialog.model.unpaid.freeModels.title": "Bezmaksas modeļi no OpenCode", + "dialog.model.unpaid.addMore.title": "Pievienot vairāk modeļu no populāriem nodrošinātājiem", + "dialog.model.unpaid.viewMoreProviders": "Skatīt vēl 70+ nodrošinātājus", + "dialog.provider.viewAll": "Rādīt vairāk nodrošinātāju", + "provider.connect.title": "Savienot {{provider}}", + "provider.connect.title.anthropicProMax": "Pieteikties ar Claude Pro/Max", + "provider.connect.selectMethod": "Izvēlieties pieteikšanās metodi pakalpojumam {{provider}}.", + "provider.connect.method.apiKey": "API atslēga", + "provider.connect.method.browser": "Pārlūks", + "provider.connect.method.headless": "Bezgalvas režīms", + "provider.connect.status.inProgress": "Notiek autorizācija...", + "provider.connect.status.waiting": "Gaida autorizāciju...", + "provider.connect.status.failed": "Autorizācija neizdevās: {{error}}", + "provider.connect.apiKey.description": + "Ievadiet savu {{provider}} API atslēgu, lai savienotu kontu un izmantotu {{provider}} modeļus OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API atslēga", + "provider.connect.apiKey.placeholder": "API atslēga", + "provider.connect.apiKey.required": "Nepieciešama API atslēga", + "provider.connect.opencodeZen.line1": + "OpenCode Zen nodrošina piekļuvi atlasītiem uzticamiem optimizētiem modeļiem kodēšanas aģentiem.", + "provider.connect.opencodeZen.line2": + "Ar vienu API atslēgu iegūsiet piekļuvi tādiem modeļiem kā Claude, GPT, Gemini, GLM un citiem.", + "provider.connect.opencodeZen.visit.prefix": "Apmeklējiet", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": ", lai saņemtu savu API atslēgu.", + "provider.connect.oauth.code.visit.prefix": "Apmeklējiet", + "provider.connect.oauth.code.visit.link": "šo saiti", + "provider.connect.oauth.code.visit.suffix": + ", lai saņemtu autorizācijas kodu, savienotu kontu un izmantotu {{provider}} modeļus OpenCode.", + "provider.connect.oauth.code.label": "{{method}} autorizācijas kods", + "provider.connect.oauth.code.placeholder": "Autorizācijas kods", + "provider.connect.oauth.code.required": "Nepieciešams autorizācijas kods", + "provider.connect.oauth.code.invalid": "Nederīgs autorizācijas kods", + "provider.connect.oauth.auto.visit.prefix": "Apmeklējiet", + "provider.connect.oauth.auto.visit.link": "šo saiti", + "provider.connect.oauth.auto.visit.suffix": + " un ievadiet zemāk redzamo kodu, lai savienotu kontu un izmantotu {{provider}} modeļus OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Apstiprinājuma kods", + "provider.connect.toast.connected.title": "{{provider}} savienots", + "provider.connect.toast.connected.description": "{{provider}} modeļi tagad ir pieejami lietošanai.", + "provider.custom.title": "Pielāgots nodrošinātājs", + "provider.custom.unavailable": "Pielāgoti nodrošinātāji šajā serverī nav pieejami", + "provider.custom.description.prefix": "Konfigurējiet ar OpenAI saderīgu nodrošinātāju. Skatiet", + "provider.custom.description.link": "nodrošinātāja konfigurācijas dokumentāciju", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Nodrošinātāja ID", + "provider.custom.field.providerID.placeholder": "mananodrosinatajs", + "provider.custom.field.providerID.description": "Mazie burti, cipari, domuzīmes vai pasvītras", + "provider.custom.field.name.label": "Parādāmais nosaukums", + "provider.custom.field.name.placeholder": "Mans AI nodrošinātājs", + "provider.custom.field.baseURL.label": "Bāzes URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API atslēga", + "provider.custom.field.apiKey.placeholder": "API atslēga", + "provider.custom.field.apiKey.description": + "Nav obligāti. Atstājiet tukšu, ja autentifikāciju pārvaldāt ar galvenēm.", + "provider.custom.models.label": "Modeļi", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Nosaukums", + "provider.custom.models.name.placeholder": "Parādāmais nosaukums", + "provider.custom.models.remove": "Noņemt modeli", + "provider.custom.models.add": "Pievienot modeli", + "provider.custom.headers.label": "Galvenes (nav obligāti)", + "provider.custom.headers.key.label": "Galvene", + "provider.custom.headers.key.placeholder": "Galvenes-nosaukums", + "provider.custom.headers.value.label": "Vērtība", + "provider.custom.headers.value.placeholder": "vērtība", + "provider.custom.headers.remove": "Noņemt galveni", + "provider.custom.headers.add": "Pievienot galveni", + "provider.custom.error.providerID.required": "Nepieciešams norādīt nodrošinātāja ID", + "provider.custom.error.providerID.format": "Izmantojiet mazos burtus, ciparus, domuzīmes vai pasvītras", + "provider.custom.error.providerID.exists": "Šāds nodrošinātāja ID jau pastāv", + "provider.custom.error.name.required": "Nepieciešams parādāmais nosaukums", + "provider.custom.error.baseURL.required": "Nepieciešams pamata URL", + "provider.custom.error.baseURL.format": "Jāsākas ar http:// vai https://", + "provider.custom.error.required": "Obligāti", + "provider.custom.error.duplicate": "Dublikāts", + "provider.disconnect.toast.disconnected.title": "{{provider}} atvienots", + "provider.disconnect.toast.disconnected.description": "{{provider}} modeļi vairs nav pieejami.", + "model.tag.free": "Bezmaksas", + "model.tag.latest": "Jaunākais", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "teksts", + "model.input.image": "attēls", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Atļauj: {{inputs}}", + "model.tooltip.context.label": "Konteksts", + "model.tooltip.inputs": "Ievades", + "model.tooltip.model": "Modelis", + "model.tooltip.provider": "Nodrošinātājs", + "model.tooltip.reasoning": "Spriešana", + "model.tooltip.reasoning.allowed": "Atļauta spriešana", + "model.tooltip.reasoning.none": "Nav spriešanas", + "model.tooltip.context": "Konteksta ierobežojums {{limit}}", + "common.search.placeholder": "Meklēt", + "common.goBack": "Atpakaļ", + "common.goForward": "Uz priekšu", + "common.loading": "Ielādējas", + "common.loading.ellipsis": "...", + "common.cancel": "Atcelt", + "common.clear": "Notīrīt", + "common.open": "Atvērt", + "common.connect": "Pievienoties", + "common.disconnect": "Atvienoties", + "common.continue": "Turpināt", + "common.submit": "Iesniegt", + "common.save": "Saglabāt", + "common.saving": "Saglabājas...", + "common.default": "Noklusējums", + "common.attachment": "pielikums", + "prompt.placeholder.shell": "Ievadiet čaulas komandu... {{example}}", + "prompt.placeholder.normal": 'Uzdodiet jebko... "{{example}}"', + "prompt.placeholder.simple": "Uzdodiet jebko...", + "prompt.placeholder.summarizeComments": "Kopsavilkt komentārus…", + "prompt.placeholder.summarizeComment": "Kopsavilkt komentāru…", + "prompt.mode.shell": "Čaula", + "prompt.mode.normal": "Uzdot", + "prompt.mode.shell.exit": "esc, lai izietu", + "session.child.promptDisabled": "Apakšaģenta sesijās nevar uzdot jautājumus.", + "session.child.backToParent": "Atpakaļ uz galveno sesiju.", + "prompt.example.1": "Izlabot TODO kodā", + "prompt.example.2": "Kāda ir šī projekta tehnoloģiju kaudze?", + "prompt.example.3": "Salabot bojātos testus", + "prompt.example.4": "Paskaidro, kā darbojas autentifikācija", + "prompt.example.5": "Atrodi un novērs drošības ievainojamības", + "prompt.example.6": "Pievieno vienību testus lietotāja servisam", + "prompt.example.7": "Pārstrukturē šo funkciju, lai tā būtu saprotamāka", + "prompt.example.8": "Ko nozīmē šī kļūda?", + "prompt.example.9": "Palīdzi atrast un labot šo problēmu", + "prompt.example.10": "Ģenerē API dokumentāciju", + "prompt.example.11": "Optimizē datubāzes vaicājumus", + "prompt.example.12": "Pievieno ievades validāciju", + "prompt.example.13": "Izveido jaunu komponenti...", + "prompt.example.14": "Kā izvietot šo projektu?", + "prompt.example.15": "Pārskati manu kodu labākajai praksei", + "prompt.example.16": "Pievieno kļūdu apstrādi šai funkcijai", + "prompt.example.17": "Paskaidro šo regex paraugu", + "prompt.example.18": "Pārveido šo uz TypeScript", + "prompt.example.19": "Pievieno žurnālošanu visā kodā", + "prompt.example.20": "Kuras atkarības ir novecojušas?", + "prompt.example.21": "Palīdzi uzrakstīt migrācijas skriptu", + "prompt.example.22": "Ievies kešošanu šim galapunktam", + "prompt.example.23": "Pievieno lapošanu šim sarakstam", + "prompt.example.24": "Izveido CLI komandu...", + "prompt.example.25": "Kā šeit darbojas vides mainīgie?", + "prompt.popover.emptyResults": "Nav atbilstošu rezultātu", + "prompt.popover.emptyCommands": "Nav atbilstošu komandu", + "prompt.dropzone.label": "Nomet šeit attēlus, PDF vai teksta failus", + "prompt.dropzone.file.label": "Nomet, lai @pieminētu failu", + "prompt.slash.badge.custom": "pielāgots", + "prompt.slash.badge.skill": "prasme", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktīvs", + "prompt.context.includeActiveFile": "Iekļaut aktīvo failu", + "prompt.context.removeActiveFile": "Noņemt aktīvo failu no konteksta", + "prompt.context.removeFile": "Noņemt failu no konteksta", + "prompt.action.attachFile": "Pievienot failus", + "prompt.menu.addImagesAndFiles": "Pievienot failus un citus", + "prompt.menu.imagesAndFiles": "Attēli un faili", + "prompt.menu.commands": "Komandas", + "prompt.menu.context": "Konteksts", + "prompt.menu.shellCommand": "Čaulas komanda", + "prompt.attachment.remove": "Noņemt pielikumu", + "prompt.action.send": "Sūtīt", + "prompt.action.stop": "Apturēt", + "prompt.toast.pasteUnsupported.title": "Neatbalstīts pielikums", + "prompt.toast.pasteUnsupported.description": "Šeit var pievienot tikai attēlus, PDF vai teksta failus.", + "prompt.toast.attachmentDuplicate.title": "Šis fails jau ir augšupielādēts", + "prompt.toast.modelAgentRequired.title": "Izvēlieties aģentu un modeli", + "prompt.toast.modelAgentRequired.description": "Pirms nosūtīšanas izvēlieties aģentu un modeli.", + "prompt.toast.worktreeCreateFailed.title": "Neizdevās izveidot darba koku", + "prompt.toast.sessionCreateFailed.title": "Neizdevās izveidot sesiju", + "prompt.toast.shellSendFailed.title": "Neizdevās nosūtīt čaulas komandu", + "prompt.toast.commandSendFailed.title": "Neizdevās nosūtīt komandu", + "prompt.toast.promptSendFailed.title": "Neizdevās nosūtīt pieprasījumu", + "prompt.toast.promptSendFailed.description": "Neizdevās iegūt sesiju", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{enabled}} no {{total}} ieslēgti", + "dialog.mcp.empty": "Nav konfigurētu MCP", + "dialog.lsp.empty": "LSP automātiski noteikti pēc failu tipiem", + "dialog.plugins.empty": "Spraudņi konfigurēti opencode.json", + "mcp.status.connected": "savienots", + "mcp.status.failed": "neizdevās", + "mcp.status.needs_auth": "nepieciešama autorizācija", + "mcp.status.disabled": "atspējots", + "mcp.auth.clickToAuthenticate": "Autentificējieties", + "dialog.fork.empty": "Nav ziņu, no kurām atzarot", + "dialog.directory.search.placeholder": "Meklēt mapes", + "dialog.directory.empty": "Mapes nav atrastas", + "dialog.directory.action.selectFile": "Izvēlieties failu", + "dialog.directory.action.selectFolder": "Izvēlieties mapi", + "dialog.directory.root": "Sakne", + "dialog.directory.parent": "Vecāks", + "dialog.directory.readError": "Neizdevās nolasīt šo mapi", + "app.server.unreachable": "Neizdevās sasniegt {{server}}", + "app.server.retrying": "Automātiska atkārtota mēģināšana...", + "app.server.otherServers": "Citi serveri", + "dialog.server.title": "Serveri", + "dialog.server.description": "Mainiet, pie kura OpenCode servera šī lietotne pieslēdzas.", + "dialog.server.search.placeholder": "Meklēt serverus", + "dialog.server.empty": "Vēl nav serveru", + "dialog.server.add.title": "Pievienot serveri", + "dialog.server.add.url": "Servera adrese", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Neizdevās izveidot savienojumu ar serveri", + "dialog.server.add.checking": "Pārbauda...", + "dialog.server.add.button": "Pievienot serveri", + "dialog.server.add.name": "Servera nosaukums (nav obligāti)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Lietotājvārds (nav obligāti)", + "dialog.server.add.usernamePlaceholder": "lietotājvārds", + "dialog.server.add.password": "Parole (nav obligāti)", + "dialog.server.add.passwordPlaceholder": "parole", + "dialog.server.edit.title": "Rediģēt serveri", + "dialog.server.default.title": "Noklusējuma serveris", + "dialog.server.default.description": + "Pievienoties šim serverim, palaižot lietotni, nevis startēt lokālo serveri. Nepieciešama pārstartēšana.", + "dialog.server.default.none": "Nav izvēlēts serveris", + "dialog.server.default.set": "Iestatīt pašreizējo kā noklusējumu", + "dialog.server.default.clear": "Notīrīt", + "dialog.server.action.remove": "Noņemt serveri", + "dialog.server.menu.edit": "Rediģēt", + "dialog.server.menu.default": "Iestatīt kā noklusējumu", + "dialog.server.menu.defaultRemove": "Noņemt noklusējumu", + "dialog.server.menu.delete": "Dzēst", + "dialog.server.current": "Pašreizējais serveris", + "dialog.server.status.default": "Noklusējuma", + "wsl.server.add": "Pievienot WSL serveri", + "wsl.server.addShort": "Pievienot WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL serveris", + "wsl.server.retryStart": "Mēģināt vēlreiz", + "wsl.server.updating": "Atjaunina...", + "wsl.onboarding.step.distro": "Izvēlieties distribūciju", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Pārbauda WSL...", + "wsl.onboarding.restartRequired": "Lai pabeigtu WSL instalēšanu, nepieciešama Windows pārstartēšana.", + "wsl.onboarding.ready": "WSL ir gatavs.", + "wsl.onboarding.required": "Turpināšanai nepieciešams WSL.", + "wsl.onboarding.checkingDistros": "Pārbauda distribūcijas...", + "wsl.onboarding.installingDistro": "Instalē {{distro}}...", + "wsl.onboarding.checkingDistro": "Pārbauda {{distro}}...", + "wsl.onboarding.listingDistros": "Uzskaita distribūcijas...", + "wsl.onboarding.distroReady": "{{distro}} ir gatava.", + "wsl.onboarding.distroNotInstalled": "{{distro}} vēl nav instalēta.", + "wsl.onboarding.openDistroOnce": "Atveriet {{distro}} vienu reizi, lai pabeigtu iestatīšanu.", + "wsl.onboarding.finishingDistro": "Pabeidzu {{distro}} iestatīšanu.", + "wsl.onboarding.pickDistro": "Izvēlieties distribūciju vai instalējiet kādu no zemāk redzamajām.", + "wsl.onboarding.checkingOpencode": "Pārbaudu OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Pārbaudu OpenCode distribūcijā {{distro}}...", + "wsl.onboarding.updatingOpencode": "Atjauninu OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Atjauninu OpenCode distribūcijā {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Atjauniniet OpenCode distribūcijā {{distro}}.", + "wsl.onboarding.updateOpencode": "Atjauniniet OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode ir gatavs distribūcijā {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode ir gatavs.", + "wsl.onboarding.installOpencodeIn": "Instalējiet OpenCode distribūcijā {{distro}}.", + "wsl.onboarding.installOpencode": "Instalējiet OpenCode", + "wsl.onboarding.chooseDistroFirst": "Vispirms izvēlieties distribūciju.", + "wsl.onboarding.loadFailed": "Neizdevās ielādēt WSL stāvokli.", + "wsl.onboarding.loading": "Ielādē...", + "wsl.onboarding.installedDistros": "Instalētās distribūcijas", + "wsl.onboarding.checkAgain": "Pārbaudīt vēlreiz", + "wsl.onboarding.distroStatus.ready": "Gatavs", + "wsl.onboarding.distroStatus.checking": "Pārbaudu...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode nav instalēts", + "wsl.onboarding.distroStatus.missingTools": "Trūkst bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Nav atbalstīts · Izmantojiet WSL 2", + "wsl.onboarding.needAnotherDistro": "Nepieciešama cita distribūcija?", + "wsl.onboarding.needAnotherDistroHint": "Instalējiet Linux distribūciju no WSL kataloga", + "wsl.onboarding.wslNotInstalled.title": "WSL nav instalēts", + "wsl.onboarding.wslNotInstalled.description": + "Pirms OpenCode var pievienot WSL serveri, nepieciešams WSL (Windows Subsystem for Linux)", + "wsl.onboarding.wslUnavailable.title": "WSL nav pieejams", + "wsl.onboarding.wslUnavailable.description": "OpenCode neizdevās pārbaudīt WSL šajā datorā.", + "wsl.onboarding.installWsl": "Instalēt WSL", + "wsl.onboarding.windowsRestartRequired": + "Restartējiet Windows, lai pabeigtu WSL instalēšanu, pēc tam atveriet OpenCode no jauna.", + "wsl.onboarding.next": "Tālāk", + "wsl.onboarding.refresh": "Atsvaidzināt", + "wsl.onboarding.allDistrosAdded": "Visas instalētās distribūcijas jau ir pievienotas.", + "wsl.onboarding.noDistros": "Distribūcijas vēl nav atrastas.", + "wsl.onboarding.install": "Instalēt", + "wsl.onboarding.installing": "Notiek instalēšana...", + "wsl.onboarding.installDistro": "Instalēt distribūciju", + "wsl.onboarding.searchDistros": "Meklēt distribūcijas", + "wsl.onboarding.wsl2Required": "Nepieciešams WSL 2.", + "wsl.onboarding.toolsRequired": "Šai distribūcijai nepieciešami bash un curl.", + "wsl.onboarding.openTerminal": "Atvērt termināli", + "wsl.onboarding.path": "Ceļš: {{path}}", + "wsl.onboarding.notFound": "nav atrasts", + "wsl.onboarding.version": "Versija: {{version}}", + "wsl.onboarding.unknown": "nezināms", + "wsl.onboarding.desktopVersion": "darbvirsmas {{version}}", + "wsl.onboarding.versionMismatch": "Instalētā versija neatbilst darbvirsmas lietotnes versijai.", + "wsl.onboarding.adding": "Pievieno...", + "help.tabs.toast.ariaLabel": "Iepazīstinām ar cilnēm. Organizējiet darbu un aktīvās sesijas ar cilnēm", + "help.tabs.toast.dismiss": "Aizvērt cilņu informāciju", + "help.tabs.title": "Iepazīstinām ar cilnēm", + "help.tabs.description": "Organizējiet darbu un aktīvās sesijas ar cilnēm", + "help.tabs.date": "14. jūlijs", + "help.tabs.introduction": "OpenCode Desktop tagad balstās uz cilnēm.", + "help.tabs.sessions": + "Sāciet jaunu sesiju cilnē vai atveriet esošu sesiju no jebkura projekta. Atveriet jaunu cilni, uzsākot ko jaunu, un aizveriet to, kad esat pabeidzis.", + "help.tabs.organize": + "Dažas atvērtas cilnes atvieglo aktīvo sesiju pārvaldību. Ja plānojat tās paturēt, pārdēvējiet cilnes saprotamāk.", + "help.tabs.home": + "Visas sesijas un projektus atradīsiet jaunajā sākuma ekrānā. Izvēloties sesiju, tā tiks atvērta cilnē.", + "help.tabs.persistence": "Atverot lietotni no jauna, jūsu cilnes paliek atvērtas.", + "help.tabs.worktrees": + "Jaunais dizains vēl neatbalsta Git Worktrees, tas drīzumā būs pieejams. Ja vēlaties turpināt lietot iepriekšējo izkārtojumu, varat pārslēgties starp izkārtojumiem iestatījumos. Ņemiet vērā, ka jaunais izkārtojums pēc dažām nedēļām kļūs pastāvīgs.", + "server.row.noUsername": "nav lietotājvārda", + "dialog.project.edit.title": "Rediģēt projektu", + "dialog.project.edit.name": "Nosaukums", + "dialog.project.edit.icon": "Ikona", + "dialog.project.edit.icon.alt": "Projekta ikona", + "dialog.project.edit.icon.hint": "Noklikšķiniet vai ievelciet attēlu", + "dialog.project.edit.icon.recommended": "Ieteicams: 128x128px", + "dialog.project.edit.color": "Krāsa", + "dialog.project.edit.color.select": "Izvēlieties {{color}} krāsu", + "dialog.project.edit.worktree.startup": "Darbtelpas starta skripts", + "dialog.project.edit.worktree.startup.description": "Tiek palaists pēc jaunas darbtelpas (worktree) izveides.", + "dialog.project.edit.worktree.startup.placeholder": "piem., bun install", + "dialog.releaseNotes.action.getStarted": "Sākt", + "dialog.releaseNotes.action.next": "Tālāk", + "dialog.releaseNotes.action.hideFuture": "Turpmāk nerādīt", + "dialog.releaseNotes.media.alt": "Versijas priekšskatījums", + "context.breakdown.title": "Konteksta sadalījums", + "context.breakdown.note": 'Aptuvens ievades tokenu sadalījums. "Citi" ietver rīku definīcijas un piesaisti.', + "context.breakdown.system": "Sistēma", + "context.breakdown.user": "Lietotājs", + "context.breakdown.assistant": "Asistents", + "context.breakdown.tool": "Rīku izsaukumi", + "context.breakdown.other": "Citi", + "context.systemPrompt.title": "Sistēmas uzvedne", + "context.rawMessages.title": "Neapstrādātas ziņas", + "context.export.session": "Eksportēt sesiju", + "context.stats.session": "Sesija", + "context.stats.messages": "Ziņas", + "context.stats.provider": "Pakalpojumu sniedzējs", + "context.stats.model": "Modelis", + "context.stats.limit": "Konteksta ierobežojums", + "context.stats.totalTokens": "Kopā žetonu", + "context.stats.usage": "Izlietojums", + "context.stats.inputTokens": "Ievades žetoni", + "context.stats.outputTokens": "Izvades žetoni", + "context.stats.reasoningTokens": "Loģikas žetoni", + "context.stats.cacheTokens": "Keša žetoni (lasīšana/rakstīšana)", + "context.stats.userMessages": "Lietotāja ziņas", + "context.stats.assistantMessages": "Asistenta ziņas", + "context.stats.totalCost": "Kopējās izmaksas", + "context.stats.sessionCreated": "Sesija izveidota", + "context.stats.lastActivity": "Pēdējā aktivitāte", + "context.usage.tokens": "Žetoni", + "context.usage.usage": "Izlietojums", + "context.usage.cost": "Izmaksas", + "context.usage.clickToView": "Noklikšķiniet, lai skatītu kontekstu", + "context.usage.view": "Skatīt konteksta izlietojumu", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Valoda", + "toast.language.description": "Valoda nomainīta uz {{language}}", + "toast.theme.title": "Tēma nomainīta", + "toast.scheme.title": "Krāsu shēma", + "toast.workspace.enabled.title": "Darbtelpas ieslēgtas", + "toast.workspace.enabled.description": "Sānu joslā tagad redzamas vairākas darba kopijas", + "toast.workspace.disabled.title": "Darbtelpas izslēgtas", + "toast.workspace.disabled.description": "Sānu joslā redzama tikai galvenā darba kopija", + "toast.permissions.autoaccept.on.title": "Atļaujas tiek automātiski apstiprinātas", + "toast.permissions.autoaccept.on.description": "Atļauju pieprasījumi tiks apstiprināti automātiski", + "toast.permissions.autoaccept.off.title": "Automātiska atļauju apstiprināšana apturēta", + "toast.permissions.autoaccept.off.description": "Atļauju pieprasījumiem būs nepieciešams apstiprinājums", + "toast.model.none.title": "Nav izvēlēts modelis", + "toast.model.none.description": "Pievienojiet nodrošinātāju, lai apkopotu šo sesiju", + "toast.file.loadFailed.title": "Neizdevās ielādēt failu", + "toast.file.listFailed.title": "Neizdevās uzskaitīt failus", + "toast.context.noLineSelection.title": "Nav izvēlētas rindas", + "toast.context.noLineSelection.description": "Vispirms izvēlieties rindu diapazonu faila cilnē.", + "toast.session.share.copyFailed.title": "Neizdevās kopēt URL starpliktuvē", + "toast.session.share.success.title": "Sesija kopīgota", + "toast.session.share.success.description": "Kopīgošanas URL nokopēts starpliktuvē!", + "toast.session.share.failed.title": "Neizdevās kopīgot sesiju", + "toast.session.share.failed.description": "Radās kļūda, kopīgojot sesiju", + "toast.session.unshare.success.title": "Sesija atsaistīta", + "toast.session.unshare.success.description": "Sesija veiksmīgi atsaistīta!", + "toast.session.unshare.failed.title": "Neizdevās pārtraukt sesijas kopīgošanu", + "toast.session.unshare.failed.description": "Radās kļūda, pārtraucot sesijas kopīgošanu", + "toast.session.export.success.title": "Sesija eksportēta", + "toast.session.export.success.description": "Sesija saglabāta kā {{filename}}", + "toast.session.export.failed.title": "Neizdevās eksportēt sesiju", + "toast.session.export.failed.description": "Radās kļūda, eksportējot sesiju", + "toast.session.listFailed.title": "Neizdevās ielādēt sesijas projektam {{project}}", + "toast.project.reloadFailed.title": "Neizdevās pārlādēt {{project}}", + "toast.update.title": "Pieejams atjauninājums", + "toast.update.description": "Pieejama jauna OpenCode versija ({{version}}) instalēšanai.", + "toast.update.action.installRestart": "Instalēt un restartēt", + "toast.update.action.notYet": "Vēlāk", + "error.page.title": "Kaut kas nogāja greizi", + "error.page.description": "Radās kļūda, ielādējot lietotni.", + "error.page.description.localServerStartup": "Radās kļūda, startējot lokālo serveri.", + "error.page.details.label": "Kļūdas detaļas", + "error.page.action.restart": "Restartēt", + "error.page.action.report": "Ziņot par kļūdu", + "error.page.action.reported": "Kļūda ziņota", + "error.page.action.exportLogs": "Eksportēt žurnālus", + "error.page.action.checking": "Pārbauda...", + "error.page.action.checkUpdates": "Pārbaudīt atjauninājumus", + "error.page.action.updateTo": "Atjaunināt uz {{version}}", + "error.page.circular": "[Ciklisks]", + "error.page.report.prefix": "Lūdzu, ziņojiet par šo kļūdu OpenCode komandai", + "error.page.report.discord": "Discord", + "error.page.version": "Versija: {{version}}", + "error.dev.rootNotFound": + "Saknes elements nav atrasts. Vai aizmirsāt to pievienot index.html? Vai arī id atribūts ir kļūdaini uzrakstīts?", + "error.serverSync.connectFailed": + "Neizdevās izveidot savienojumu ar serveri. Vai serveris darbojas adresē `{{url}}`?", + "error.serverSDK.noServerAvailable": "Nav pieejams neviens serveris", + "error.serverSDK.serverNotAvailable": "Serveris nav pieejams", + "error.childStore.persistedCacheCreateFailed": "Neizdevās izveidot saglabāto kešatmiņu", + "error.childStore.persistedProjectMetadataCreateFailed": "Neizdevās izveidot saglabātos projekta metadatus", + "error.childStore.persistedProjectIconCreateFailed": "Neizdevās izveidot saglabāto projekta ikonu", + "error.childStore.storeCreateFailed": "Neizdevās izveidot krātuvi", + "directory.error.invalidUrl": "Nederīgs direktorijs URL.", + "error.chain.unknown": "Nezināma kļūda", + "error.server.invalidConfiguration": "Nederīga konfigurācija", + "error.chain.causedBy": "Iemesls:", + "error.chain.apiError": "API kļūda", + "error.chain.status": "Statuss: {{status}}", + "error.chain.retryable": "Atkārtojams: {{retryable}}", + "error.chain.responseBody": "Atbildes saturs:\n{{body}}", + "error.chain.didYouMean": "Vai domājāt: {{suggestions}}", + "error.chain.modelNotFound": "Modelis nav atrasts: {{provider}}/{{model}}", + "error.chain.checkConfig": "Pārbaudiet konfigurāciju (opencode.json) pakalpojuma/modela nosaukumus", + "error.chain.mcpFailed": + 'MCP serveris "{{name}}" neizdevās. Ņemiet vērā, OpenCode vēl neatbalsta MCP autentifikāciju.', + "error.chain.providerAuthFailed": "Pakalpojuma autentifikācija neizdevās ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Neizdevās inicializēt pakalpojumu "{{provider}}". Pārbaudiet akreditācijas datus un konfigurāciju.', + "error.chain.configJsonInvalid": "Konfigurācijas fails {{path}} nav derīgs JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Konfigurācijas fails {{path}} nav derīgs JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Direktorijs "{{dir}}" failā {{path}} nav derīgs. Pārdēvējiet direktoriju uz "{{suggestion}}" vai izdzēsiet to. Tā ir bieža drukas kļūda.', + "error.chain.configFrontmatterError": "Neizdevās parsēt frontmatter failā {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfigurācijas fails {{path}} nav derīgs", + "error.chain.configInvalidWithMessage": "Konfigurācijas fails {{path}} nav derīgs: {{message}}", + "notification.permission.title": "Nepieciešama atļauja", + "notification.permission.description": "{{sessionTitle}} projektā {{projectName}} nepieciešama atļauja", + "notification.question.title": "Jautājums", + "notification.question.description": "{{sessionTitle}} projektā {{projectName}} ir jautājums", + "notification.action.goToSession": "Doties uz sesiju", + "notification.session.responseReady.title": "Atbilde gatava", + "notification.session.error.title": "Sesijas kļūda", + "notification.session.error.fallbackDescription": "Radās kļūda", + "home.recentProjects": "Nesenie projekti", + "home.empty.title": "Nav neseno projektu", + "home.empty.description": "Sāciet, atverot lokālu projektu", + "home.title": "Sākums", + "home.projects": "Projekti", + "home.project.add": "Pievienot projektu", + "home.recentlyClosed": "Nesen aizvērti", + "home.server.collapse": "Sakļaut servera projektus", + "home.server.expand": "Izvērst servera projektus", + "home.sessions.search.placeholder": "Meklēt sesijas", + "home.sessions.search.placeholder.scoped": "Meklēt sesijas {{scope}}", + "home.sessions.search.sessions": "Sesijas", + "home.sessions.search.noResults": "Sesijas ar {{query}} nav atrastas", + "home.sessions.empty": "Šeit vēl nav nekā", + "home.sessions.empty.description": "Izveidojiet sesiju, lai sāktu", + "home.sessions.group.today": "Šodien", + "home.sessions.group.yesterday": "Vakar", + "home.sessions.group.older": "Vecākas", + "home.providerTip": + "Pievienojiet vairāk nekā 75 pakalpojumu sniedzējus, lai izmantotu citus modeļus, tostarp Claude, GPT, Gemini u.c.", + "session.tab.session": "Sesija", + "session.tab.review": "Pārskats", + "session.tab.context": "Konteksts", + "session.tab.unknown": "Nezināma sesija", + "session.panel.reviewAndFiles": "Pārskats un faili", + "session.error.notFound": "Šī sesija nav atrodama", + "session.error.notFound.description": "Šī cilne norāda uz sesiju, kas vairs neeksistē šajā serverī.", + "session.error.notFound.closeTab": "Aizvērt cilni", + "session.error.serverConnection": "Nevar izveidot savienojumu ar šo serveri", + "session.review.filesChanged": "Mainīti faili: {{count}}", + "session.review.change.one": "Izmaiņa", + "session.review.change.other": "Izmaiņas", + "session.review.loadingChanges": "Notiek izmaiņu ielāde...", + "session.review.empty": "Šajā sesijā vēl nav izmaiņu", + "session.review.noVcs": "Nav atrasta Git versiju kontrole, izmaiņas netiek rādītas", + "session.review.noVcs.createGit.title": "Izveidot Git repozitoriju", + "session.review.noVcs.createGit.description": "Izseko, pārskati un atsauc izmaiņas šajā projektā", + "session.review.noVcs.createGit.actionLoading": "Notiek Git repozitorija izveide...", + "session.review.noVcs.createGit.action": "Izveidot Git repozitoriju", + "session.review.noSnapshot": + "Konfigurācijā atspējota momentuzņēmumu izsekošana, tāpēc sesijas izmaiņas nav pieejamas", + "session.review.noChanges": "Nav izmaiņu", + "session.review.noUncommittedChanges": "Vēl nav neapstiprinātu izmaiņu", + "session.review.noBranchChanges": "Vēl nav zara izmaiņu", + "session.files.selectToOpen": "Izvēlies failu atvēršanai", + "session.files.all": "Visi faili", + "session.files.empty": "Nav failu", + "session.files.binaryContent": "Binārs fails (saturs nav skatāms)", + "session.messages.renderEarlier": "Rādīt agrākos ziņojumus", + "session.messages.loadingEarlier": "Notiek agrāko ziņojumu ielāde...", + "session.messages.loadEarlier": "Ielādēt agrākos ziņojumus", + "session.messages.loading": "Notiek ziņojumu ielāde...", + "session.messages.jumpToLatest": "Pāriet uz jaunāko", + "session.context.addToContext": "Pievienot {{selection}} kontekstam", + "session.todo.title": "Darāmais", + "session.todo.collapse": "Sakļaut", + "session.todo.expand": "Izvērst", + "session.todo.progress": "{{done}} no {{total}} darbiem izpildīti", + "session.question.progress": "{{current}} no {{total}} jautājumiem", + "session.question.minimize": "Minimizēt jautājumu", + "session.question.restore": "Atjaunot jautājumu", + "session.question.pending.one": "{{count}} neatbildēts jautājums", + "session.question.pending.other": "{{count}} neatbildēti jautājumi", + "session.question.pending.zero": "{{count}} neatbildētu jautājumu", + "session.followupDock.summary.one": "{{count}} rindā gaidoša ziņa", + "session.followupDock.summary.other": "{{count}} rindā gaidošas ziņas", + "session.followupDock.summary.zero": "{{count}} rindā gaidošu ziņu", + "session.followupDock.sendNow": "Sūtīt tagad", + "session.followupDock.edit": "Rediģēt", + "session.followupDock.collapse": "Sakļaut rindā gaidošās ziņas", + "session.followupDock.expand": "Izvērst rindā gaidošās ziņas", + "session.revertDock.summary.one": "{{count}} atsaukta ziņa", + "session.revertDock.summary.other": "{{count}} atsauktas ziņas", + "session.revertDock.summary.zero": "{{count}} atsauktu ziņu", + "session.revertDock.collapse": "Sakļaut atsauktās ziņas", + "session.revertDock.expand": "Izvērst atsauktās ziņas", + "session.revertDock.restore": "Atjaunot ziņu", + "session.new.title": "Izveido jebko", + "session.new.project.new": "Jauns projekts", + "session.new.project.search": "Meklēt projektus", + "session.new.project.add": "Pievienot projektu", + "session.new.worktree.main": "Galvenā zars", + "session.new.worktree.mainWithBranch": "Galvenā zars ({{branch}})", + "session.new.worktree.create": "Izveidot jaunu darba koku", + "session.new.workspace.runIn": "Palaist sesiju iekš", + "session.new.workspace.triggerLocal": "Lokāli", + "session.new.workspace.local": "Lokālais repozitorijs", + "session.new.workspace.existing": "Darbtelpa…", + "session.new.git.none": "Nav Git", + "session.new.lastModified": "Pēdējoreiz mainīts", + "session.header.search.placeholder": "Meklēt {{project}}", + "session.header.searchFiles": "Meklēt failus", + "session.header.openIn": "Atvērt ar", + "session.header.open.action": "Atvērt ar {{app}}", + "session.header.open.ariaLabel": "Atvērt ar {{app}}", + "session.header.open.menu": "Atvēršanas opcijas", + "session.header.open.copyPath": "Kopēt ceļu", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Failu pārlūks", + "session.header.open.fileManager": "Failu pārvaldnieks", + "session.header.reveal.finder": "Parādīt Finder", + "session.header.reveal.fileExplorer": "Parādīt failu pārlūkā", + "session.header.reveal.containingFolder": "Atvērt saturējošo mapi", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminālis", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Statuss", + "status.popover.ariaLabel": "Servera konfigurācijas", + "status.popover.tab.servers": "Serveri", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Spraudņi", + "status.popover.action.manageServers": "Pārvaldīt serverus", + "session.share.popover.title": "Publicēt tīmeklī", + "session.share.popover.description.shared": + "Šī sesija ir publiski pieejama tīmeklī. Tai var piekļūt ikviens ar saiti.", + "session.share.popover.description.unshared": "Kopīgot sesiju publiski tīmeklī. Tai varēs piekļūt ikviens ar saiti.", + "session.share.action.share": "Kopīgot", + "session.share.action.publish": "Publicēt", + "session.share.action.publishing": "Publicē...", + "session.share.action.unpublish": "Noņemt no publicēšanas", + "session.share.action.unpublishing": "Noņem no publicēšanas...", + "session.share.action.view": "Skatīt", + "session.share.copy.copied": "Nokopēts", + "session.share.copy.copyLink": "Kopēt saiti", + "lsp.tooltip.none": "Nav LSP serveru", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Ielādē uzvedni...", + "terminal.loading": "Ielādē termināli...", + "terminal.title": "Terminālis", + "terminal.title.numbered": "Terminālis {{number}}", + "terminal.close": "Aizvērt termināli", + "terminal.connectionLost.title": "Savienojums zudis", + "terminal.connectionLost.abnormalClose": "WebSocket savienojums neparasti pārtraukts: {{code}}", + "terminal.connectionLost.description": + "Termināļa savienojums tika pārtraukts. Tas var notikt, ja serveris tiek restartēts.", + "terminal.connectTicket.csrfError": + "PTY savienojuma biļeti noraidīja izcelsmes vai CSRF pārbaudes. Pārbaudiet servera CORS konfigurāciju.", + "terminal.connectTicket.statusError": "PTY savienojuma biļete neizdevās ar statusu {{status}}", + "titlebar.update": "Atjaunināt", + "titlebar.updateVersion": "Atjaunināt uz {{version}}", + "common.closeTab": "Aizvērt cilni", + "common.dismiss": "Aizvērt", + "common.moreCountSuffix": " (+{{count}} vēl)", + "common.requestFailed": "Pieprasījums neizdevās", + "common.moreOptions": "Vairāk opciju", + "common.learnMore": "Uzzināt vairāk", + "common.rename": "Pārdēvēt", + "common.export": "Eksportēt", + "common.reset": "Atiestatīt", + "common.archive": "Arhivēt", + "common.delete": "Dzēst", + "common.close": "Aizvērt", + "common.edit": "Rediģēt", + "common.loadMore": "Ielādēt vairāk", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "nezināms", + "common.time.justNow": "Tikko", + "common.time.minutesAgo.short": "pirms {{count}} min", + "common.time.hoursAgo.short": "pirms {{count}} h", + "common.time.daysAgo.short": "pirms {{count}} d", + "sidebar.menu.toggle": "Pārslēgt izvēlni", + "sidebar.nav.projectsAndSessions": "Projekti un sesijas", + "sidebar.settings": "Iestatījumi", + "sidebar.help": "Palīdzība", + "sidebar.workspaces.enable": "Ieslēgt darba telpas", + "sidebar.workspaces.disable": "Izslēgt darba telpas", + "sidebar.gettingStarted.title": "Darba sākšana", + "sidebar.gettingStarted.line1": "OpenCode ietver bezmaksas modeļus, lai varētu sākt uzreiz.", + "sidebar.gettingStarted.line2": + "Pievienojiet jebkuru nodrošinātāju, lai izmantotu modeļus, t.sk. Claude, GPT, Gemini u.c.", + "sidebar.project.recentSessions": "Nesenās sesijas", + "sidebar.project.viewAllSessions": "Skatīt visas sesijas", + "sidebar.project.clearNotifications": "Notīrīt paziņojumus", + "sidebar.empty.title": "Nav atvērts neviens projekts", + "sidebar.empty.description": "Atveriet projektu, lai sāktu", + "debugBar.ariaLabel": "Izstrādes veiktspējas diagnostika", + "debugBar.na": "nav", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Pēdējā pabeigtā maršruta pāreja, kas skar sesijas lapu, mērīts no maršrutētāja sākuma līdz pirmajam attēlojumam pēc stabilizācijas.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Kustīgais kadru skaits sekundē pēdējās 5 sekundēs.", + "debugBar.frame.label": "KADRS", + "debugBar.frame.tip": "Sliktākais kadra laiks pēdējās 5 sekundēs.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Kadri virs 32 ms pēdējās 5 sekundēs.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Bloķētais laiks un garo uzdevumu skaits pēdējās 5 sekundēs. Maks. uzdevums: {{max}}.", + "debugBar.delay.label": "AIZKAVE", + "debugBar.delay.tip": "Sliktākā novērotā ievades aizkave pēdējās 5 sekundēs.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Aptuvenais mijiedarbības ilgums pēdējās 5 sekundēs. Tas ir līdzīgs INP, nevis oficiālais Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kopējā izkārtojuma nobīde pašreizējā lietotnes darbības laikā.", + "debugBar.mem.label": "ATMIŅA", + "debugBar.mem.tipUnavailable": "Izmantotā JS kaudze pret kaudzes limitu. Tikai Chromium.", + "debugBar.mem.tip": "Izmantotā JS kaudze: {{used}} no {{limit}}.", + "debugBar.focus.label": "FOKUSS", + "debugBar.focus.tip": "Piespiedu kārtā parādīt fokusa stilus visiem interaktīvajiem elementiem", + "debugBar.focus.on": "IESL.", + "debugBar.focus.off": "IZSL.", + "debugBar.direction.label": "VIRZIENS", + "debugBar.direction.tip": "Piespiedu kārtā lietot pilnu lietotnes izkārtojuma virzienu, nemainot izvēlēto valodu", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode darbvirsma", + "settings.section.desktop": "Darbvirsma", + "settings.section.server": "Serveris", + "settings.tab.general": "Vispārīgi", + "settings.tab.shortcuts": "Saīsnes", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integrācija", + "settings.desktop.wsl.description": "Palaist OpenCode serveri WSL vidē uz Windows.", + "settings.general.section.appearance": "Izskats", + "settings.general.section.advanced": "Paplašināti", + "settings.general.section.notifications": "Sistēmas paziņojumi", + "settings.general.section.updates": "Atjauninājumi", + "settings.general.section.sounds": "Skaņas efekti", + "settings.general.section.feed": "Plūsma", + "settings.general.section.display": "Displejs", + "settings.general.row.language.title": "Valoda", + "settings.general.row.language.description": "Mainīt OpenCode saskarnes valodu", + "settings.general.row.shell.title": "Termināļa čaula", + "settings.general.row.shell.description": "Čaula, ko izmanto terminālis un aģenta rīki", + "settings.general.row.shell.autoDefault": "Automātiski (noklusējums)", + "settings.general.row.shell.terminalOnly": "tikai terminālis", + "settings.general.row.appearance.title": "Izskats", + "settings.general.row.appearance.description": "Pielāgojiet, kā OpenCode izskatās jūsu ierīcē", + "settings.general.row.colorScheme.title": "Krāsu shēma", + "settings.general.row.colorScheme.description": + "Izvēlieties, vai OpenCode sekos sistēmas, gaišajai vai tumšajai tēmai", + "settings.general.row.theme.title": "Tēma", + "settings.general.row.theme.description": "Pielāgojiet OpenCode tēmu.", + "settings.general.row.font.title": "Koda fonts", + "settings.general.row.font.description": "Pielāgojiet fontu, kas tiek izmantots koda blokos", + "settings.general.row.terminalFont.title": "Termināļa fonts", + "settings.general.row.terminalFont.description": "Pielāgojiet fontu, kas tiek izmantots terminālī", + "settings.general.row.uiFont.title": "Saskarnes fonts", + "settings.general.row.uiFont.description": "Pielāgojiet fontu visā saskarnē", + "settings.general.row.followup.title": "Turpmākā darbība", + "settings.general.row.followup.description": "Izvēlieties, vai turpmākie uzvedumi tiek izpildīti uzreiz vai rindā", + "settings.general.row.followup.option.queue": "Rinda", + "settings.general.row.followup.option.steer": "Izpildīt uzreiz", + "settings.general.row.showFileTree.title": "Failu koks", + "settings.general.row.showFileTree.description": "Rādīt failu koka paneli sesijās", + "settings.general.row.showNavigation.title": "Navigācijas vadīklas", + "settings.general.row.showNavigation.description": "Rādīt atpakaļ un uz priekšu pogas darbvirsmas virsraksta joslā", + "settings.general.row.showSearch.title": "Komandu palete", + "settings.general.row.showSearch.description": "Rādīt meklēšanas un komandu paletes pogu virsraksta joslā", + "settings.general.row.showTerminal.title": "Terminālis", + "settings.general.row.showTerminal.description": "Rādīt termināļa pogu darbvirsmas virsraksta joslā", + "settings.general.row.showStatus.title": "Servera statuss", + "settings.general.row.showStatus.description": "Rādīt servera statusa pogu virsraksta joslā", + "settings.general.row.mobileTitlebarBottom.title": "Apakšējā navigācija", + "settings.general.row.mobileTitlebarBottom.description": + "Rādīt virsraksta joslu un sesiju cilnes ekrāna apakšā mobilajās ierīcēs", + "settings.general.row.showCustomAgents.title": "Rādīt aģentu", + "settings.general.row.showCustomAgents.description": + "Pārslēgties starp aģentiem redaktorā. Ja paslēpts, tiek izmantots Build aģents.", + "settings.general.row.reasoningSummaries.title": "Rādīt pamatojuma kopsavilkumus", + "settings.general.row.reasoningSummaries.description": "Rādīt modeļa pamatojuma kopsavilkumus laika joslā", + "settings.general.row.shellToolPartsExpanded.title": "Izvērst čaulas rīka daļas", + "settings.general.row.shellToolPartsExpanded.description": + "Pēc noklusējuma rādīt čaulas rīka daļas izvērstas laika joslā", + "settings.general.row.editToolPartsExpanded.title": "Izvērst rediģēšanas rīka daļas", + "settings.general.row.editToolPartsExpanded.description": + "Pēc noklusējuma rādīt rediģēšanas, rakstīšanas un labošanas rīka daļas izvērstas laika joslā", + "settings.general.row.newInterface.title": "Jauns izkārtojums", + "settings.general.row.newInterface.badge": "Jauns", + "settings.general.row.newInterface.description": + "Izmantot jauno cilņu un sākuma izkārtojumu. Ierobežotu laiku var pārslēgties starp izkārtojumiem.", + "settings.general.row.newInterfaceNotice.title": "Tagad izmantojat jauno izkārtojumu", + "settings.general.row.newInterfaceNotice.description": "Iepriekšējais izkārtojums vairs nav pieejams", + "settings.general.row.newInterfaceNotice.dismiss": "Aizvērt", + "settings.general.row.pinchZoom.title": "Tuvināt ar šķipsnu", + "settings.general.row.pinchZoom.description": "Atļaut tuvināšanu ar skārienpaliktņa šķipsnu un Ctrl-ritināšanu", + "settings.general.row.wayland.title": "Izmantot vietējo Wayland", + "settings.general.row.wayland.description": "Atspējot X11 rezerves režīmu Wayland vidē. Nepieciešama pārstartēšana.", + "settings.general.row.wayland.tooltip": + "Linux ar dažādu atsvaidzes ātrumu monitoriem vietējais Wayland var būt stabilāks.", + "settings.general.row.releaseNotes.title": "Izlaiduma piezīmes", + "settings.general.row.releaseNotes.description": "Rādīt Jaunumu logus pēc atjauninājumiem", + "settings.updates.row.startup.title": "Pārbaudīt atjauninājumus startējot", + "settings.updates.row.startup.description": "Automātiski pārbaudīt atjauninājumus, palaižot OpenCode", + "settings.updates.row.check.title": "Pārbaudīt atjauninājumus", + "settings.updates.row.check.description": "Manuāli pārbaudīt un instalēt atjauninājumus, ja pieejami", + "settings.updates.action.checkNow": "Pārbaudīt tagad", + "settings.updates.action.checking": "Pārbauda...", + "settings.updates.action.downloading": "Lejupielādē...", + "settings.updates.action.installing": "Instalē...", + "settings.updates.toast.latest.title": "Jums ir jaunākā versija", + "settings.updates.toast.latest.description": "Jūs izmantojat jaunāko OpenCode versiju.", + "sound.option.none": "Nav", + "sound.option.alert01": "Brīdinājums 01", + "sound.option.alert02": "Brīdinājums 02", + "sound.option.alert03": "Brīdinājums 03", + "sound.option.alert04": "Brīdinājums 04", + "sound.option.alert05": "Brīdinājums 05", + "sound.option.alert06": "Brīdinājums 06", + "sound.option.alert07": "Brīdinājums 07", + "sound.option.alert08": "Brīdinājums 08", + "sound.option.alert09": "Brīdinājums 09", + "sound.option.alert10": "Brīdinājums 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "Aģents", + "settings.general.notifications.agent.description": + "Rādīt sistēmas paziņojumu, kad aģents ir pabeigts vai nepieciešama uzmanība", + "settings.general.notifications.permissions.title": "Atļaujas", + "settings.general.notifications.permissions.description": "Rādīt sistēmas paziņojumu, kad nepieciešama atļauja", + "settings.general.notifications.errors.title": "Kļūdas", + "settings.general.notifications.errors.description": "Rādīt sistēmas paziņojumu, kad notiek kļūda", + "settings.general.sounds.agent.title": "Aģents", + "settings.general.sounds.agent.description": "Atskaņot skaņu, kad aģents ir pabeigts vai nepieciešama uzmanība", + "settings.general.sounds.permissions.title": "Atļaujas", + "settings.general.sounds.permissions.description": "Atskaņot skaņu, kad nepieciešama atļauja", + "settings.general.sounds.errors.title": "Kļūdas", + "settings.general.sounds.errors.description": "Atskaņot skaņu, kad notiek kļūda", + "settings.shortcuts.title": "Tastatūras saīsnes", + "settings.shortcuts.reset.button": "Atjaunot noklusējumus", + "settings.shortcuts.reset.toast.title": "Saīsnes atjaunotas", + "settings.shortcuts.reset.toast.description": "Tastatūras saīsnes ir atjaunotas uz noklusējuma vērtībām.", + "settings.shortcuts.conflict.title": "Saīsne jau tiek izmantota", + "settings.shortcuts.conflict.description": "{{keybind}} jau ir piešķirts {{titles}}.", + "settings.shortcuts.unassigned": "Nav piešķirts", + "settings.shortcuts.pressKeys": "Nospiediet taustiņus", + "settings.shortcuts.search.placeholder": "Meklēt saīsnes", + "settings.shortcuts.search.empty": "Saīsnes nav atrastas", + "settings.shortcuts.group.general": "Vispārīgi", + "settings.shortcuts.group.session": "Sesija", + "settings.shortcuts.group.navigation": "Navigācija", + "settings.shortcuts.group.modelAndAgent": "Modelis un aģents", + "settings.shortcuts.group.terminal": "Terminālis", + "settings.shortcuts.group.prompt": "Uzvedne", + "settings.providers.title": "Pakalpojumu sniedzēji", + "settings.providers.description": "Pakalpojumu iestatījumus varēs konfigurēt šeit.", + "settings.providers.section.connected": "Pievienotie pakalpojumi", + "settings.providers.connected.empty": "Nav pievienotu pakalpojumu", + "settings.providers.connected.environmentDescription": "Pievienots no jūsu vides mainīgajiem", + "settings.providers.section.popular": "Populāri pakalpojumi", + "settings.providers.custom.description": "Pievienojiet OpenAI saderīgu pakalpojumu, norādot bāzes URL.", + "settings.providers.tag.environment": "Vide", + "settings.providers.tag.config": "Konfigurācija", + "settings.providers.tag.custom": "Pielāgots", + "settings.providers.tag.other": "Citi", + "settings.models.title": "Modeļi", + "settings.models.description": "Modeļu iestatījumus varēs konfigurēt šeit.", + "settings.agents.title": "Aģenti", + "settings.agents.description": "Šeit varēs konfigurēt aģentu iestatījumus.", + "settings.commands.title": "Komandas", + "settings.commands.description": "Šeit varēs konfigurēt komandu iestatījumus.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Šeit varēs konfigurēt MCP iestatījumus.", + "settings.permissions.title": "Atļaujas", + "settings.permissions.description": "Kontrolējiet, kādus rīkus serveris var izmantot pēc noklusējuma.", + "settings.permissions.section.tools": "Rīki", + "settings.permissions.toast.updateFailed.title": "Neizdevās atjaunināt atļaujas", + "settings.permissions.action.allow": "Atļaut", + "settings.permissions.action.ask": "Jautāt", + "settings.permissions.action.deny": "Liegt", + "settings.permissions.tool.read.title": "Lasīt", + "settings.permissions.tool.read.description": "Faila lasīšana (atbilst faila ceļam)", + "settings.permissions.tool.edit.title": "Rediģēt", + "settings.permissions.tool.edit.description": "Mainīt failus, tostarp labojumus, ierakstus un ielāpus", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Atrast failus, izmantojot glob paraugus", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Meklēt failu saturu ar regulārajām izteiksmēm", + "settings.permissions.tool.list.title": "Saraksts", + "settings.permissions.tool.list.description": "Uzskaitīt failus mapē", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Izpildīt čaulas komandas", + "settings.permissions.tool.task.title": "Uzdevums", + "settings.permissions.tool.task.description": "Palaist apakšaģentus", + "settings.permissions.tool.skill.title": "Prasme", + "settings.permissions.tool.skill.description": "Ielādēt prasmi pēc nosaukuma", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Izpildīt valodas servera vaicājumus", + "settings.permissions.tool.todowrite.title": "Todo rakstīšana", + "settings.permissions.tool.todowrite.description": "Atjaunināt uzdevumu sarakstu", + "settings.permissions.tool.webfetch.title": "Web iegūšana", + "settings.permissions.tool.webfetch.description": "Iegūt saturu no URL", + "settings.permissions.tool.websearch.title": "Tīmekļa meklēšana", + "settings.permissions.tool.websearch.description": "Meklēt tīmeklī", + "settings.permissions.tool.external_directory.title": "Ārējais katalogs", + "settings.permissions.tool.external_directory.description": "Piekļūt failiem ārpus projekta mapes", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Noteikt atkārtotus rīka izsaukumus ar identisku ievadi", + "session.delete.failed.title": "Neizdevās dzēst sesiju", + "session.delete.title": "Dzēst sesiju", + "session.delete.confirm": 'Dzēst sesiju "{{name}}"?', + "session.delete.button": "Dzēst sesiju", + "workspace.new": "Jauna darba vide", + "workspace.type.local": "lokāla", + "workspace.type.sandbox": "smilškaste", + "workspace.create.failed.title": "Neizdevās izveidot darba vidi", + "workspace.delete.failed.title": "Neizdevās dzēst darba vidi", + "workspace.resetting.title": "Darba vides atiestatīšana", + "workspace.resetting.description": "Tas var aizņemt minūti.", + "workspace.reset.failed.title": "Neizdevās atiestatīt darba vidi", + "workspace.reset.success.title": "Darba vide atiestatīta", + "workspace.reset.success.description": "Darba vide tagad atbilst noklusētajam zaram.", + "workspace.error.stillPreparing": "Darba vide vēl tiek sagatavota", + "workspace.status.checking": "Pārbauda nesapludinātās izmaiņas...", + "workspace.status.error": "Nevar pārbaudīt git statusu.", + "workspace.status.clean": "Nesapludinātas izmaiņas nav atrastas.", + "workspace.status.dirty": "Šajā darba vidē ir nesapludinātas izmaiņas.", + "workspace.delete.title": "Dzēst darbvietu", + "workspace.delete.confirm": 'Dzēst darbvietu "{{name}}"?', + "workspace.delete.button": "Dzēst darbvietu", + "workspace.reset.title": "Atiestatīt darbvietu", + "workspace.reset.confirm": 'Atiestatīt darbvietu "{{name}}"?', + "workspace.reset.button": "Atiestatīt darbvietu", + "workspace.reset.archived.none": "Neviena aktīva sesija netiks arhivēta.", + "workspace.reset.archived.one": "Tiks arhivēta 1 sesija.", + "workspace.reset.archived.many": "Tiks arhivētas {{count}} sesijas.", + "workspace.reset.note": "Darbvieta tiks atiestatīta uz noklusējuma zara stāvokli.", + "dialog.usageExceeded.dontShowAgain": "Vairs nerādīt", +} diff --git a/packages/app/src/i18n/mk.ts b/packages/app/src/i18n/mk.ts new file mode 100644 index 0000000000000000000000000000000000000000..01e60baa408b5dbd574691e60b7e8d3164644345 --- /dev/null +++ b/packages/app/src/i18n/mk.ts @@ -0,0 +1,1166 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Датотека", + "desktop.menu.edit": "Уреди", + "desktop.menu.view": "Прикажи", + "desktop.menu.go": "Оди", + "desktop.menu.window": "Прозорец", + "desktop.menu.help": "Помош", + "desktop.menu.checkForUpdates": "Проверете дали има ажурирања...", + "desktop.menu.settings": "Поставки", + "desktop.menu.reloadWebview": "Вчитај повторно Webview", + "desktop.menu.restart": "Рестартирајте", + "desktop.menu.exportLogs": "Извези дневници...", + "desktop.menu.newSession": "Нова сесија", + "desktop.menu.openProject": "Отворен проект...", + "desktop.menu.newWindow": "Нов прозорец", + "desktop.menu.closeWindow": "Затвори го прозорецот", + "desktop.menu.undo": "Врати", + "desktop.menu.redo": "Повторете", + "desktop.menu.cut": "Исечете", + "desktop.menu.copy": "Копирај", + "desktop.menu.paste": "Вметни", + "desktop.menu.delete": "Избриши", + "desktop.menu.selectAll": "Изберете Сите", + "desktop.menu.toggleSidebar": "Вклучете ја страничната лента", + "desktop.menu.toggleTerminal": "Вклучете го терминалот", + "desktop.menu.toggleFileTree": "Вклучете го стеблото на датотеката", + "desktop.menu.reload": "Вчитај повторно", + "desktop.menu.toggleDeveloperTools": "Вклучете ги алатките за програмери", + "desktop.menu.actualSize": "Вистинска големина", + "desktop.menu.zoomIn": "Зумирајте", + "desktop.menu.zoomOut": "Одзумирај", + "desktop.menu.toggleFullScreen": "Вклучете го цел екран", + "desktop.menu.back": "Назад", + "desktop.menu.forward": "Напред", + "desktop.menu.previousSession": "Претходна сесија", + "desktop.menu.nextSession": "Следна сесија", + "desktop.menu.previousProject": "Претходен проект", + "desktop.menu.nextProject": "Следен проект", + "desktop.menu.minimize": "Минимизирајте", + "desktop.menu.maximize": "Максимизирај", + "desktop.menu.documentation": "OpenCode Документација", + "desktop.menu.supportForum": "Форум за поддршка", + "desktop.menu.shareFeedback": "Споделете повратни информации", + "desktop.menu.reportBug": "Пријавете бубачка", + "desktop.menu.ariaLabel": "OpenCode мени", + "desktop.updater.dialog.checkFailed.message": "Проверката за ажурирање не успеа.", + "desktop.updater.dialog.checkFailed.title": "Грешка во ажурирањето", + "desktop.updater.dialog.upToDate.message": "Ажуриран си.", + "desktop.updater.dialog.upToDate.title": "Нема ажурирања", + "desktop.updater.dialog.ready.message": "Преземено е ажурирањето {{version}}. Да се ​​рестартира сега?", + "desktop.updater.dialog.ready.title": "Подготвено за ажурирање", + "desktop.updater.dialog.restart": "Рестартирајте", + "desktop.updater.dialog.later": "Подоцна", + "desktop.recovery.action.relaunch": "Рестартирајте", + "desktop.recovery.action.exportLogs": "Извоз на дневници", + "desktop.recovery.action.keepWaiting": "Продолжете да чекате", + "desktop.recovery.action.quit": "Откажи", + "desktop.recovery.loadFailed": "OpenCode не успеа да се вчита", + "desktop.recovery.terminated": "Прозорецот OpenCode неочекувано заврши", + "desktop.recovery.unresponsive": "OpenCode не реагира", + "desktop.recovery.unresponsive.detail": + "Може да ја рестартирате апликацијата, да ги отворите дневниците или да продолжите да чекате.", + "desktop.recovery.loadFailed.detail": "Прозорец: {{window}}\nURL: {{url}}\nГрешка: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Прозорец: {{window}}\nПричина: {{reason}}\nШифра: {{code}}", + "desktop.recovery.unknown": "<непознато>", + "desktop.dialog.chooseFolder": "Изберете папка", + "desktop.dialog.chooseFile": "Изберете датотека", + "desktop.dialog.saveFile": "Зачувај датотека", + "desktop.dialog.files": "Датотеки", + "desktop.server.local": "Локален сервер", + "desktop.wsl.error.windowsOnly": "WSL е достапна само на Windows", + "desktop.wsl.error.unavailable": "WSL е недостапен", + "desktop.wsl.error.listInstalled": "Не успеа да се наведат инсталираните WSL дистрибуции", + "desktop.wsl.error.listOnline": "Не успеа да се наведат онлајн дистрибуции WSL", + "desktop.wsl.error.executeDistro": "Не може да се извршат команди во дистрибуција", + "desktop.wsl.error.installWsl": "инсталацијата WSL не успеа", + "desktop.wsl.error.installDistro": "Не успеа да се инсталира дистрибуцијата: {{distro}}", + "desktop.wsl.error.installOpencode": "инсталацијата OpenCode не успеа", + "desktop.wsl.error.alreadyAdded": "{{distro}} е веќе додаден", + "desktop.wsl.error.opencodeMissing": "opencode не е инсталиран во оваа дистрибуција", + "desktop.wsl.error.opencodeCannotRun": "opencode е инсталиран, но не може да се изврши", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode не е инсталиран во {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode ажурирањето е завршено, но {{distro}} сè уште известува {{installed}}; се очекува {{expected}}", + "desktop.wsl.error.noVersion": "нема верзија", + "desktop.wsl.error.serverExited": "WSL серверот излезе по стартувањето (код={{code}} сигнал={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL серверот излезе пред да стане здрав (шифра={{code}} сигнал={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Страничната кола за {{distro}} здравствена проверка истече по {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} истече по {{timeout}}ms", + "desktop.wsl.error.failedPort": "Не успеа да се добие порта", + "desktop.picker.error.notSelected": "Датотеката не беше избрана од избирачот", + "desktop.picker.error.sizeLimit": "Избраните прилози ја надминуваат границата {{limit}} MB", + "command.category.suggested": "Предложено", + "command.category.view": "Прикажи", + "command.category.project": "Проект", + "command.category.provider": "Провајдер", + "command.category.server": "Сервер", + "command.category.session": "Сесија", + "command.category.theme": "Тема", + "command.category.language": "Јазик", + "command.category.file": "Датотека", + "command.category.context": "Контекст", + "command.category.terminal": "Терминал", + "command.category.model": "Модел", + "command.category.mcp": "MCP", + "command.category.agent": "Агент", + "command.category.permissions": "Дозволи", + "command.category.workspace": "Работен простор", + "command.category.settings": "Поставки", + "command.logs.export": "Извезете логови", + "theme.scheme.system": "Систем", + "theme.scheme.light": "Светлина", + "theme.scheme.dark": "Темно", + "command.sidebar.toggle": "Вклучете ја страничната лента", + "command.project.open": "Отворен проект", + "command.project.previous": "Претходен проект", + "command.project.next": "Следен проект", + "command.project.index": "Префрли се на проект {{index}}", + "command.provider.connect": "Поврзете провајдер", + "command.server.switch": "Префрли сервер", + "command.settings.open": "Отворете ги поставките", + "command.session.previous": "Претходна сесија", + "command.session.next": "Следна сесија", + "command.session.previous.unseen": "Претходна непрочитана сесија", + "command.session.next.unseen": "Следна непрочитана сесија", + "command.session.archive": "Сесија за архива", + "command.palette": "Палета на команди", + "command.theme.cycle": "Тема за циклус", + "command.theme.set": "Користете ја темата: {{theme}}", + "command.theme.scheme.cycle": "Циклус шема на бои", + "command.theme.scheme.set": "Користете шема на бои: {{scheme}}", + "command.language.cycle": "Јазик на циклус", + "command.language.set": "Користете јазик: {{language}}", + "command.session.new": "Нова сесија", + "command.file.open": "Отворете ја датотеката", + "command.tab.close": "Затвори ја картичката", + "command.tab.reopenClosed": "Повторно отворете го затворениот таб", + "command.context.addSelection": "Додадете избор во контекст", + "command.context.addSelection.description": "Додадете избрани линии од тековната датотека", + "command.input.focus": "Фокусирајте го влезот", + "command.terminal.toggle": "Вклучете го терминалот", + "command.fileTree.toggle": "Вклучете го дрвото на датотеки", + "command.review.toggle": "Вклучи преглед", + "command.terminal.new": "Нов терминал", + "command.terminal.new.description": "Направете нова картичка за терминал", + "command.steps.toggle": "Вклучете ги чекорите", + "command.steps.toggle.description": "Прикажи или скриј чекори за тековната порака", + "command.message.previous": "Претходна порака", + "command.message.previous.description": "Одете на претходната корисничка порака", + "command.message.next": "Следна порака", + "command.message.next.description": "Одете на следната корисничка порака", + "command.model.choose": "Изберете модел", + "command.model.choose.description": "Изберете различен модел", + "command.mcp.toggle": "Вклучи MCPs", + "command.mcp.toggle.description": "Вклучи MCPs", + "command.agent.cycle": "Циклус агент", + "command.agent.cycle.description": "Префрлете се на следниот агент", + "command.agent.cycle.reverse": "Циклирајте го агентот наназад", + "command.agent.cycle.reverse.description": "Префрлете се на претходниот агент", + "command.model.variant.cycle": "Циклус напор за размислување", + "command.model.variant.cycle.description": "Префрлете се на следното ниво на напор", + "command.prompt.mode.shell": "Школка", + "command.prompt.mode.normal": "Прашај", + "command.permissions.autoaccept.enable": "Автоматско прифаќање дозволи", + "command.permissions.autoaccept.disable": "Стоп за автоматско прифаќање дозволи", + "command.workspace.toggle": "Вклучете работни простори", + "command.workspace.toggle.description": "Овозможете или оневозможете повеќе работни простори во страничната лента", + "command.session.undo": "Врати", + "command.session.undo.description": "Вратете ја последната порака", + "command.session.redo": "Повторете", + "command.session.redo.description": "Повторете ја последната отповикана порака", + "command.session.compact": "Компактна сесија", + "command.session.compact.description": "Сумирајте ја сесијата за да ја намалите големината на контекстот", + "command.session.fork": "Разгранување од порака", + "command.session.fork.description": "Креирајте нова сесија од претходната порака", + "command.session.share": "Споделете сесија", + "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": "Не се пронајдени резултати", + "palette.group.commands": "Наредби", + "palette.group.files": "Датотеки", + "dialog.provider.search.placeholder": "Пребарајте провајдери", + "dialog.provider.empty": "Не се пронајдени провајдери", + "dialog.provider.group.popular": "Популарни", + "dialog.provider.group.other": "Друго", + "dialog.provider.custom.label": "Прилагодено OpenAI-компатибилен провајдер", + "dialog.provider.tag.recommended": "Препорачано", + "dialog.provider.opencode.note": "Курирани модели вклучувајќи Claude, GPT, Gemini и повеќе", + "dialog.provider.opencode.tagline": "Сигурни оптимизирани модели", + "dialog.provider.opencodeGo.tagline": "Ниска цена претплата за секого", + "dialog.provider.anthropic.note": "Директен пристап до Claude модели, вклучувајќи ги Pro и Max", + "dialog.provider.copilot.note": "Модели со вештачка интелигенција за помош за кодирање преку GitHub Copilot", + "dialog.provider.openai.note": "GPT модели за брзи, способни општи задачи со вештачка интелигенција", + "dialog.provider.google.note": "Gemini модели за брзи, структурирани одговори", + "dialog.provider.openrouter.note": "Пристапете до сите поддржани модели од еден провајдер", + "dialog.provider.vercel.note": "Унифициран пристап до модели со вештачка интелигенција со паметно рутирање", + "dialog.model.select.title": "Изберете модел", + "dialog.model.search.placeholder": "Пребарајте модели", + "dialog.model.empty": "Нема резултати од моделот", + "dialog.model.manage": "Управувајте со моделите", + "dialog.model.manage.description": "Прилагодете кои модели се појавуваат во избирачот на модели.", + "dialog.model.manage.provider.toggle": "Вклучете ги сите {{provider}} модели", + "dialog.model.unpaid.freeModels.title": "Бесплатни модели обезбедени од OpenCode", + "dialog.model.unpaid.addMore.title": "Додадете повеќе модели од популарни провајдери", + "dialog.model.unpaid.viewMoreProviders": "Видете повеќе од 70 провајдери", + "dialog.provider.viewAll": "Прикажи повеќе провајдери", + "provider.connect.title": "Поврзете се {{provider}}", + "provider.connect.title.anthropicProMax": "Најавете се со Claude Pro/Max", + "provider.connect.selectMethod": "Изберете метод за најавување за {{provider}}.", + "provider.connect.method.apiKey": "клуч API", + "provider.connect.method.browser": "Прелистувач", + "provider.connect.method.headless": "Без глава", + "provider.connect.status.inProgress": "Овластувањето е во тек...", + "provider.connect.status.waiting": "Се чека овластување...", + "provider.connect.status.failed": "Овластувањето не успеа: {{error}}", + "provider.connect.apiKey.description": + "Внесете го вашиот клуч {{provider}} API за да ја поврзете вашата сметка и да користите {{provider}} модели во OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API клуч", + "provider.connect.apiKey.placeholder": "клуч API", + "provider.connect.apiKey.required": "Потребен е клуч API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ви дава пристап до куриран сет на сигурни оптимизирани модели за агенти за кодирање.", + "provider.connect.opencodeZen.line2": + "Со еден клуч API ќе добиете пристап до модели како што се Claude, GPT, Gemini, GLM и повеќе.", + "provider.connect.opencodeZen.visit.prefix": "Посетете", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "да го соберете вашиот клуч API.", + "provider.connect.oauth.code.visit.prefix": "Посетете", + "provider.connect.oauth.code.visit.link": "оваа врска", + "provider.connect.oauth.code.visit.suffix": + "да го соберете вашиот код за авторизација за да ја поврзете вашата сметка и да користите {{provider}} модели во OpenCode.", + "provider.connect.oauth.code.label": "{{method}} код за авторизација", + "provider.connect.oauth.code.placeholder": "Код за авторизација", + "provider.connect.oauth.code.required": "Потребна е шифра за авторизација", + "provider.connect.oauth.code.invalid": "Неважечки код за авторизација", + "provider.connect.oauth.auto.visit.prefix": "Посетете", + "provider.connect.oauth.auto.visit.link": "оваа врска", + "provider.connect.oauth.auto.visit.suffix": + "и внесете го кодот подолу за да ја поврзете вашата сметка и да користите {{provider}} модели во OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Код за потврда", + "provider.connect.toast.connected.title": "{{provider}} поврзан", + "provider.connect.toast.connected.description": "Моделите {{provider}} сега се достапни за употреба.", + "provider.custom.title": "Прилагоден провајдер", + "provider.custom.unavailable": "Прилагодените провајдери се недостапни на овој сервер", + "provider.custom.description.prefix": "Конфигурирајте OpenAI-компатибилен провајдер. Видете го", + "provider.custom.description.link": "документи за конфигурација на провајдерот", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ИД на провајдер", + "provider.custom.field.providerID.placeholder": "мојот провајдер", + "provider.custom.field.providerID.description": "Мали букви, бројки, цртички или долни црти", + "provider.custom.field.name.label": "Прикажано име", + "provider.custom.field.name.placeholder": "Мојот провајдер со вештачка интелигенција", + "provider.custom.field.baseURL.label": "База URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "клуч API", + "provider.custom.field.apiKey.placeholder": "клуч API", + "provider.custom.field.apiKey.description": + "Факултативно. Оставете празно ако управувате со авторизација преку заглавија.", + "provider.custom.models.label": "Модели", + "provider.custom.models.id.label": "ИД", + "provider.custom.models.id.placeholder": "модел-ид", + "provider.custom.models.name.label": "Име", + "provider.custom.models.name.placeholder": "Прикажано име", + "provider.custom.models.remove": "Отстранете го моделот", + "provider.custom.models.add": "Додадете модел", + "provider.custom.headers.label": "Заглавија (изборно)", + "provider.custom.headers.key.label": "Заглавие", + "provider.custom.headers.key.placeholder": "Заглавие-Име", + "provider.custom.headers.value.label": "Вредност", + "provider.custom.headers.value.placeholder": "вредност", + "provider.custom.headers.remove": "Отстранете го заглавието", + "provider.custom.headers.add": "Додадете заглавие", + "provider.custom.error.providerID.required": "Потребна е идентификација на провајдер", + "provider.custom.error.providerID.format": "Користете мали букви, бројки, цртички или долни црти", + "provider.custom.error.providerID.exists": "ИД на провајдер веќе постои", + "provider.custom.error.name.required": "Потребно е име за прикажување", + "provider.custom.error.baseURL.required": "Потребна е основа URL", + "provider.custom.error.baseURL.format": "Мора да започне со http:// или https://", + "provider.custom.error.required": "Задолжително", + "provider.custom.error.duplicate": "Дупликат", + "provider.disconnect.toast.disconnected.title": "{{provider}} исклучен", + "provider.disconnect.toast.disconnected.description": "{{provider}} моделите веќе не се достапни.", + "model.tag.free": "Бесплатно", + "model.tag.latest": "Најнови", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "текст", + "model.input.image": "слика", + "model.input.audio": "аудио", + "model.input.video": "видео", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Дозволува: {{inputs}}", + "model.tooltip.context.label": "Контекст", + "model.tooltip.inputs": "Влезови", + "model.tooltip.model": "Модел", + "model.tooltip.provider": "Провајдер", + "model.tooltip.reasoning": "Расудување", + "model.tooltip.reasoning.allowed": "Дозволува расудување", + "model.tooltip.reasoning.none": "Без расудување", + "model.tooltip.context": "Контекстно ограничување {{limit}}", + "common.search.placeholder": "Пребарување", + "common.goBack": "Одете назад", + "common.goForward": "Одете напред", + "common.loading": "Се вчитува", + "common.loading.ellipsis": "...", + "common.cancel": "Откажи", + "common.clear": "Јасно", + "common.open": "Отвори", + "common.connect": "Поврзете се", + "common.disconnect": "Исклучете се", + "common.continue": "Продолжи", + "common.submit": "Поднесете", + "common.save": "Зачувај", + "common.saving": "Се зачувува...", + "common.default": "Стандардно", + "common.attachment": "приврзаност", + "prompt.placeholder.shell": "Внесете школка команда... {{example}}", + "prompt.placeholder.normal": 'Прашај што било... "{{example}}"', + "prompt.placeholder.simple": "Прашај што било...", + "prompt.placeholder.summarizeComments": "Сумирајте ги коментарите…", + "prompt.placeholder.summarizeComment": "Сумирајте го коментарот…", + "prompt.mode.shell": "Школка", + "prompt.mode.normal": "Прашај", + "prompt.mode.shell.exit": "esc за излез", + "session.child.promptDisabled": "Сесиите на субагентите не можат да бидат поттикнати.", + "session.child.backToParent": "Назад на главната сесија.", + "prompt.example.1": "Поправете TODO во базата на кодови", + "prompt.example.2": "Кој е технолошкиот куп на овој проект?", + "prompt.example.3": "Поправете ги скршените тестови", + "prompt.example.4": "Објаснете како функционира автентикацијата", + "prompt.example.5": "Најдете и поправете ги безбедносните пропусти", + "prompt.example.6": "Додадете тестови на единицата за корисничката услуга", + "prompt.example.7": "Рефакторирајте ја оваа функција за да биде почитлива", + "prompt.example.8": "Што значи оваа грешка?", + "prompt.example.9": "Помогнете ми да го отстранам овој проблем", + "prompt.example.10": "Создадете API документација", + "prompt.example.11": "Оптимизирајте ги барањата за базата на податоци", + "prompt.example.12": "Додадете валидација на влезот", + "prompt.example.13": "Создадете нова компонента за...", + "prompt.example.14": "Како да го распоредам овој проект?", + "prompt.example.15": "Прегледајте го мојот код за најдобри практики", + "prompt.example.16": "Додајте справување со грешки на оваа функција", + "prompt.example.17": "Објаснете ја оваа регекс шема", + "prompt.example.18": "Претворете го ова во TypeScript", + "prompt.example.19": "Додадете логирање низ базата на кодови", + "prompt.example.20": "Кои зависности се застарени?", + "prompt.example.21": "Помогнете ми да напишам скрипта за миграција", + "prompt.example.22": "Спроведување на кеширање за оваа крајна точка", + "prompt.example.23": "Додајте страници на оваа листа", + "prompt.example.24": "Направете CLI команда за...", + "prompt.example.25": "Како функционираат променливите на животната средина овде?", + "prompt.popover.emptyResults": "Нема соодветни резултати", + "prompt.popover.emptyCommands": "Нема соодветни команди", + "prompt.dropzone.label": "Оставете слики, PDFs или текстуални датотеки овде", + "prompt.dropzone.file.label": "Спуштете се во датотеката @mention", + "prompt.slash.badge.custom": "обичај", + "prompt.slash.badge.skill": "вештина", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "активни", + "prompt.context.includeActiveFile": "Вклучете активна датотека", + "prompt.context.removeActiveFile": "Отстранете ја активната датотека од контекст", + "prompt.context.removeFile": "Отстранете ја датотеката од контекст", + "prompt.action.attachFile": "Додадете датотеки", + "prompt.menu.addImagesAndFiles": "Додадете датотеки и повеќе", + "prompt.menu.imagesAndFiles": "Слики и датотеки", + "prompt.menu.commands": "Наредби", + "prompt.menu.context": "Контекст", + "prompt.menu.shellCommand": "Команда за школка", + "prompt.attachment.remove": "Отстранете го прилогот", + "prompt.action.send": "Испрати", + "prompt.action.stop": "Стоп", + "prompt.toast.pasteUnsupported.title": "Неподдржан прилог", + "prompt.toast.pasteUnsupported.description": "Овде може да се прикачат само слики, PDFs или текстуални датотеки.", + "prompt.toast.attachmentDuplicate.title": "Оваа датотека е веќе поставена", + "prompt.toast.modelAgentRequired.title": "Изберете агент и модел", + "prompt.toast.modelAgentRequired.description": "Изберете агент и модел пред да испратите известување.", + "prompt.toast.worktreeCreateFailed.title": "Не успеа да се создаде работно дрво", + "prompt.toast.sessionCreateFailed.title": "Не успеа да се создаде сесија", + "prompt.toast.shellSendFailed.title": "Не успеа да се испрати школка команда", + "prompt.toast.commandSendFailed.title": "Не успеа да се испрати командата", + "prompt.toast.promptSendFailed.title": "Не успеа да се испрати потсетник", + "prompt.toast.promptSendFailed.description": "Не може да се врати сесијата", + "dialog.mcp.title": "MCPс", + "dialog.mcp.description": "{{enabled}} од {{total}} е овозможено", + "dialog.mcp.empty": "Не се конфигурирани MCPs", + "dialog.lsp.empty": "LSPs автоматски се открива од типовите на датотеки", + "dialog.plugins.empty": "Приклучоци конфигурирани во opencode.json", + "mcp.status.connected": "поврзани", + "mcp.status.failed": "не успеа", + "mcp.status.needs_auth": "треба авт", + "mcp.status.disabled": "оневозможено", + "mcp.auth.clickToAuthenticate": "Кликнете за автентикација", + "dialog.fork.empty": "Нема пораки од кои може да се префрлите", + "dialog.directory.search.placeholder": "Пребарајте папки", + "dialog.directory.empty": "Не се пронајдени папки", + "dialog.directory.action.selectFile": "Изберете датотека", + "dialog.directory.action.selectFolder": "Изберете папка", + "dialog.directory.root": "Корен", + "dialog.directory.parent": "Родител", + "dialog.directory.readError": "Не може да се прочита оваа папка", + "app.server.unreachable": "Не можеше да достигне {{server}}", + "app.server.retrying": "Автоматски се обидува повторно...", + "app.server.otherServers": "Други сервери", + "dialog.server.title": "Сервери", + "dialog.server.description": "Променете на кој OpenCode сервер се поврзува оваа апликација.", + "dialog.server.search.placeholder": "Пребарувајте сервери", + "dialog.server.empty": "Сè уште нема сервери", + "dialog.server.add.title": "Додај сервер", + "dialog.server.add.url": "Адреса на серверот", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Не може да се поврзе со серверот", + "dialog.server.add.checking": "Се проверува...", + "dialog.server.add.button": "Додај сервер", + "dialog.server.add.name": "Име на сервер (изборно)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Корисничко име (изборно)", + "dialog.server.add.usernamePlaceholder": "корисничко име", + "dialog.server.add.password": "Лозинка (изборно)", + "dialog.server.add.passwordPlaceholder": "лозинка", + "dialog.server.edit.title": "Уреди сервер", + "dialog.server.default.title": "Стандарден сервер", + "dialog.server.default.description": + "Поврзете се со овој сервер при стартување на апликацијата наместо да стартувате локален сервер. Потребно е рестартирање.", + "dialog.server.default.none": "Нема избран сервер", + "dialog.server.default.set": "Поставете го тековниот сервер како стандарден", + "dialog.server.default.clear": "Јасно", + "dialog.server.action.remove": "Отстранете го серверот", + "dialog.server.menu.edit": "Уреди", + "dialog.server.menu.default": "Поставете како стандардно", + "dialog.server.menu.defaultRemove": "Отстрани стандардно", + "dialog.server.menu.delete": "Избриши", + "dialog.server.current": "Тековен сервер", + "dialog.server.status.default": "Стандардно", + "wsl.server.add": "Додадете WSL сервер", + "wsl.server.addShort": "Додај WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL сервер", + "wsl.server.retryStart": "Обидете се повторно да започнете", + "wsl.server.updating": "Се ажурира...", + "wsl.onboarding.step.distro": "Изберете дистрибуција", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Се проверува WSL...", + "wsl.onboarding.restartRequired": "На Windows му треба рестартирање за да заврши инсталирањето на WSL.", + "wsl.onboarding.ready": "WSL е подготвен.", + "wsl.onboarding.required": "WSL е потребно за да продолжите.", + "wsl.onboarding.checkingDistros": "Се проверуваат дистрибуциите...", + "wsl.onboarding.installingDistro": "Се инсталира {{distro}}...", + "wsl.onboarding.checkingDistro": "Се проверува {{distro}}...", + "wsl.onboarding.listingDistros": "Дистрибуирање на огласи...", + "wsl.onboarding.distroReady": "{{distro}} е подготвен.", + "wsl.onboarding.distroNotInstalled": "{{distro}} сè уште не е инсталиран.", + "wsl.onboarding.openDistroOnce": "Отворете {{distro}} еднаш за да го завршите поставувањето.", + "wsl.onboarding.finishingDistro": "Завршување на поставувањето за {{distro}}.", + "wsl.onboarding.pickDistro": "Изберете дистрибуција или инсталирајте една подолу.", + "wsl.onboarding.checkingOpencode": "Се проверува OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Се проверува OpenCode во {{distro}}...", + "wsl.onboarding.updatingOpencode": "Се ажурира OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Се ажурира OpenCode во {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Ажурирајте OpenCode во {{distro}}.", + "wsl.onboarding.updateOpencode": "Ажурирај OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode е подготвен во {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode е подготвен.", + "wsl.onboarding.installOpencodeIn": "Инсталирајте OpenCode во {{distro}}.", + "wsl.onboarding.installOpencode": "Инсталирај OpenCode", + "wsl.onboarding.chooseDistroFirst": "Прво изберете дистрибуција.", + "wsl.onboarding.loadFailed": "Не успеа да се вчита WSL состојба.", + "wsl.onboarding.loading": "Се вчитува...", + "wsl.onboarding.installedDistros": "Инсталирани дистрибуции", + "wsl.onboarding.checkAgain": "Проверете повторно", + "wsl.onboarding.distroStatus.ready": "Подготвени", + "wsl.onboarding.distroStatus.checking": "Се проверува...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode не е инсталиран", + "wsl.onboarding.distroStatus.missingTools": "Недостасува bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Неподдржано · Користете WSL 2", + "wsl.onboarding.needAnotherDistro": "Ви треба уште една дистрибуција?", + "wsl.onboarding.needAnotherDistroHint": "Инсталирајте Linux дистрибуција од каталогот WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL не е инсталиран", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) е потребно пред OpenCode да може да додаде WSL сервер", + "wsl.onboarding.wslUnavailable.title": "WSL недостапен", + "wsl.onboarding.wslUnavailable.description": "OpenCode не можеше да го потврди WSL на оваа машина.", + "wsl.onboarding.installWsl": "Инсталирај WSL", + "wsl.onboarding.windowsRestartRequired": + "Рестартирајте го Windows за да завршите со инсталирањето WSL, а потоа повторно отворете OpenCode.", + "wsl.onboarding.next": "Следно", + "wsl.onboarding.refresh": "Освежи", + "wsl.onboarding.allDistrosAdded": "Сите инсталирани дистрибуции се веќе додадени.", + "wsl.onboarding.noDistros": "Сè уште не се откриени дистрибуции.", + "wsl.onboarding.install": "Инсталирајте", + "wsl.onboarding.installing": "Се инсталира...", + "wsl.onboarding.installDistro": "Инсталирајте distro", + "wsl.onboarding.searchDistros": "Пребарувајте дистрибуции", + "wsl.onboarding.wsl2Required": "WSL 2 е потребно.", + "wsl.onboarding.toolsRequired": "Оваа дистрибуција има потреба од bash и curl.", + "wsl.onboarding.openTerminal": "Отворете го терминалот", + "wsl.onboarding.path": "Патека: {{path}}", + "wsl.onboarding.notFound": "не е пронајден", + "wsl.onboarding.version": "Верзија: {{version}}", + "wsl.onboarding.unknown": "непознат", + "wsl.onboarding.desktopVersion": "работна површина {{version}}", + "wsl.onboarding.versionMismatch": "Инсталираната верзија не се совпаѓа со верзијата на апликацијата за десктоп.", + "wsl.onboarding.adding": "Се додава...", + "help.tabs.toast.ariaLabel": + "Ви ги претставуваме јазичињата. Организирајте ја вашата работа и активни сесии со јазичиња", + "help.tabs.toast.dismiss": "Отфрли ги информациите за картичките", + "help.tabs.title": "Ви ги претставуваме јазичињата", + "help.tabs.description": "Организирајте ја вашата работа и активни сесии со јазичиња", + "help.tabs.date": "14 јули", + "help.tabs.introduction": "OpenCode Desktop сега е изграден околу картичките.", + "help.tabs.sessions": + "Започнете нова сесија во картичка или отворете постоечка сесија од кој било од вашите проекти. Отворете нова картичка кога започнувате нешто ново и затворете ја кога ќе завршите.", + "help.tabs.organize": + "Одржувањето отворени неколку јазичиња го олеснува организирањето на вашите активни сесии. Преименувајте ги картичките во нешто незаборавно ако планирате да ги задржите наоколу.", + "help.tabs.home": + "Сите ваши сесии и проекти ќе ги најдете на новиот Почетен екран. Изборот на сесија ја отвора во таб.", + "help.tabs.persistence": "Кога повторно ќе ја отворите апликацијата, вашите картички се сè уште отворени.", + "help.tabs.worktrees": + "Новиот дизајн сè уште не поддржува Git Worktrees, тој доаѓа наскоро. Значи, ако сакате да продолжите да го користите претходниот распоред, можете да се префрлате помеѓу распоредот во Поставки. Само имајте на ум дека новиот распоред ќе стане постојан за неколку недели.", + "server.row.noUsername": "нема корисничко име", + "dialog.project.edit.title": "Уреди проект", + "dialog.project.edit.name": "Име", + "dialog.project.edit.icon": "Икона", + "dialog.project.edit.icon.alt": "Икона на проектот", + "dialog.project.edit.icon.hint": "Кликнете или повлечете слика", + "dialog.project.edit.icon.recommended": "Препорачано: 128x128px", + "dialog.project.edit.color": "Боја", + "dialog.project.edit.color.select": "Изберете {{color}} боја", + "dialog.project.edit.worktree.startup": "Скрипта за стартување на работен простор", + "dialog.project.edit.worktree.startup.description": "Работи по создавање на нов работен простор (работно дрво).", + "dialog.project.edit.worktree.startup.placeholder": "на пр. bun install", + "dialog.releaseNotes.action.getStarted": "Започнете", + "dialog.releaseNotes.action.next": "Следно", + "dialog.releaseNotes.action.hideFuture": "Не ги прикажувајте овие во иднина", + "dialog.releaseNotes.media.alt": "Преглед на издавање", + "context.breakdown.title": "Разложување на контекстот", + "context.breakdown.note": + "Приближно расчленување на влезните токени. „Друго“ вклучува дефиниции за алатки и надземни трошоци.", + "context.breakdown.system": "Систем", + "context.breakdown.user": "Корисник", + "context.breakdown.assistant": "асистент", + "context.breakdown.tool": "Повици со алатки", + "context.breakdown.other": "Друго", + "context.systemPrompt.title": "Системско известување", + "context.rawMessages.title": "Необработени пораки", + "context.export.session": "Сесија за извоз", + "context.stats.session": "Сесија", + "context.stats.messages": "Пораки", + "context.stats.provider": "Провајдер", + "context.stats.model": "Модел", + "context.stats.limit": "Контекст ограничување", + "context.stats.totalTokens": "Вкупно токени", + "context.stats.usage": "Употреба", + "context.stats.inputTokens": "Влезни токени", + "context.stats.outputTokens": "Излезни токени", + "context.stats.reasoningTokens": "Жетони за расудување", + "context.stats.cacheTokens": "Кеш токени (читање/пишување)", + "context.stats.userMessages": "Кориснички пораки", + "context.stats.assistantMessages": "Пораки за помошник", + "context.stats.totalCost": "Вкупни трошоци", + "context.stats.sessionCreated": "Создадена сесија", + "context.stats.lastActivity": "Последна активност", + "context.usage.tokens": "Жетони", + "context.usage.usage": "Употреба", + "context.usage.cost": "Цена", + "context.usage.clickToView": "Кликнете за да го видите контекстот", + "context.usage.view": "Погледнете ја употребата на контекстот", + "language.en": "Англиски јазик", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Еспањол", + "language.fr": "Француски", + "language.da": "Дански", + "language.ja": "日本語", + "language.pl": "Полски", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Норск", + "language.br": "Португалија (Бразил)", + "language.bs": "Босански", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Туркче", + "toast.language.title": "Јазик", + "toast.language.description": "Се префрли на {{language}}", + "toast.theme.title": "Темата е сменета", + "toast.scheme.title": "Шема на бои", + "toast.workspace.enabled.title": "Работните простори се овозможени", + "toast.workspace.enabled.description": "Повеќе работни дрва сега се прикажани во страничната лента", + "toast.workspace.disabled.title": "Работните простори се оневозможени", + "toast.workspace.disabled.description": "Само главното работно дрво е прикажано во страничната лента", + "toast.permissions.autoaccept.on.title": "Автоматско прифаќање дозволи", + "toast.permissions.autoaccept.on.description": "Барањата за дозвола ќе бидат автоматски одобрени", + "toast.permissions.autoaccept.off.title": "Го прекина автоматското прифаќање дозволи", + "toast.permissions.autoaccept.off.description": "Барањата за дозвола ќе бараат одобрување", + "toast.model.none.title": "Не е избран модел", + "toast.model.none.description": "Поврзете провајдер за да ја сумира оваа сесија", + "toast.file.loadFailed.title": "Не успеа да се вчита датотеката", + "toast.file.listFailed.title": "Не успеа да се наведат датотеките", + "toast.context.noLineSelection.title": "Нема избор на линија", + "toast.context.noLineSelection.description": "Прво изберете опсег на линии во картичката датотека.", + "toast.session.share.copyFailed.title": "Не успеа да се копира URL во таблата со исечоци", + "toast.session.share.success.title": "Сесијата е споделена", + "toast.session.share.success.description": "Споделете URL копиран во таблата со исечоци!", + "toast.session.share.failed.title": "Не успеа да се сподели сесијата", + "toast.session.share.failed.description": "Настана грешка при споделувањето на сесијата", + "toast.session.unshare.success.title": "Сесијата не е споделена", + "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": "Достапно е ажурирање", + "toast.update.description": "Новата верзија на OpenCode ({{version}}) сега е достапна за инсталирање.", + "toast.update.action.installRestart": "Инсталирајте и рестартирајте", + "toast.update.action.notYet": "Сè уште не", + "error.page.title": "Нешто тргна наопаку", + "error.page.description": "Се појави грешка при вчитување на апликацијата.", + "error.page.description.localServerStartup": "Се појави грешка при стартување на локалниот сервер.", + "error.page.details.label": "Детали за грешка", + "error.page.action.restart": "Рестартирајте", + "error.page.action.report": "Пријавете грешка", + "error.page.action.reported": "Пријавена грешка", + "error.page.action.exportLogs": "Извоз на дневници", + "error.page.action.checking": "Се проверува...", + "error.page.action.checkUpdates": "Проверете дали има ажурирања", + "error.page.action.updateTo": "Ажурирајте до {{version}}", + "error.page.circular": "[Кружни]", + "error.page.report.prefix": "Ве молиме пријавете ја оваа грешка на тимот OpenCode", + "error.page.report.discord": "на Discord", + "error.page.version": "Верзија: {{version}}", + "error.dev.rootNotFound": + "Не е пронајден корен елемент. Дали заборавивте да го додадете во вашата index.html? Или можеби атрибутот id е погрешно напишан?", + "error.serverSync.connectFailed": "Не може да се поврзе со серверот. Дали има сервер кој работи на `{{url}}`?", + "error.serverSDK.noServerAvailable": "Нема достапен сервер", + "error.serverSDK.serverNotAvailable": "Серверот не е достапен", + "error.childStore.persistedCacheCreateFailed": "Не успеа да се создаде постојан кеш", + "error.childStore.persistedProjectMetadataCreateFailed": "Не успеа да се создадат постојани метаподатоци за проектот", + "error.childStore.persistedProjectIconCreateFailed": "Не успеа да се создаде икона за постојан проект", + "error.childStore.storeCreateFailed": "Не успеа да се создаде продавница", + "directory.error.invalidUrl": "Неважечки директориум во URL.", + "error.chain.unknown": "Непозната грешка", + "error.server.invalidConfiguration": "Неважечка конфигурација", + "error.chain.causedBy": "Предизвикано од:", + "error.chain.apiError": "API грешка", + "error.chain.status": "Статус: {{status}}", + "error.chain.retryable": "Може да се обиде повторно: {{retryable}}", + "error.chain.responseBody": "Тело за одговор:\n{{body}}", + "error.chain.didYouMean": "Дали сакаше да каже: {{suggestions}}", + "error.chain.modelNotFound": "Моделот не е пронајден: {{provider}}/{{model}}", + "error.chain.checkConfig": "Проверете ги имињата на давателот/моделот за конфигурација (opencode.json).", + "error.chain.mcpFailed": + "MCP серверот „{{name}}“ не успеа. Забелешка, OpenCode сè уште не поддржува автентикација MCP.", + "error.chain.providerAuthFailed": "Неуспешна автентикација на провајдерот ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + "Не успеа да се иницијализира провајдерот „{{provider}}“. Проверете ги ингеренциите и конфигурацијата.", + "error.chain.configJsonInvalid": "Конфигурациската датотека на {{path}} не е валидна JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Конфигурациската датотека на {{path}} не е валидна JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + "Директориумот „{{dir}}“ во {{path}} не е валиден. Преименувајте го директориумот во „{{suggestion}}“ или отстранете го. Ова е вообичаена печатна грешка.", + "error.chain.configFrontmatterError": "Не успеа да се анализира frontmatter во {{path}}:\n{{message}}", + "error.chain.configInvalid": "Конфигурациската датотека на {{path}} е неважечка", + "error.chain.configInvalidWithMessage": "Конфигурациската датотека на {{path}} е неважечка: {{message}}", + "notification.permission.title": "Потребна е дозвола", + "notification.permission.description": "На {{sessionTitle}} во {{projectName}} му треба дозвола", + "notification.question.title": "Прашање", + "notification.question.description": "{{sessionTitle}} во {{projectName}} има прашање", + "notification.action.goToSession": "Одете на сесија", + "notification.session.responseReady.title": "Подготвен одговор", + "notification.session.error.title": "Грешка во сесијата", + "notification.session.error.fallbackDescription": "Се појави грешка", + "home.recentProjects": "Неодамнешни проекти", + "home.empty.title": "Нема неодамнешни проекти", + "home.empty.description": "Започнете со отворање на локален проект", + "home.title": "Дома", + "home.projects": "Проекти", + "home.project.add": "Додадете проект", + "home.recentlyClosed": "Неодамна затворена", + "home.server.collapse": "Собори серверски проекти", + "home.server.expand": "Проширете ги серверските проекти", + "home.sessions.search.placeholder": "Пребарувајте сесии", + "home.sessions.search.placeholder.scoped": "Пребарувајте сесии во {{scope}}", + "home.sessions.search.sessions": "Сесии", + "home.sessions.search.noResults": "Не се најдени сесии за {{query}}", + "home.sessions.empty": "Сè уште ништо овде", + "home.sessions.empty.description": "Создадете сесија за да започнете", + "home.sessions.group.today": "Денес", + "home.sessions.group.yesterday": "Вчера", + "home.sessions.group.older": "Постари", + "home.providerTip": + "Поврзете се со над 75 провајдери за да користите други модели, вклучувајќи ги Claude, GPT, Gemini, итн.", + "session.tab.session": "Сесија", + "session.tab.review": "Преглед", + "session.tab.context": "Контекст", + "session.tab.unknown": "Непозната сесија", + "session.panel.reviewAndFiles": "Преглед и датотеки", + "session.error.notFound": "Оваа сесија не може да се најде", + "session.error.notFound.description": "Оваа картичка укажува на сесија што повеќе не постои на овој сервер.", + "session.error.notFound.closeTab": "Затвори ја картичката", + "session.error.serverConnection": "Не може да се поврзе со овој сервер", + "session.review.filesChanged": "Променети датотеки {{count}}", + "session.review.change.one": "Промена", + "session.review.change.other": "Промени", + "session.review.loadingChanges": "Се вчитуваат промените...", + "session.review.empty": "Сè уште нема промени на оваа сесија", + "session.review.noVcs": "Не е откриен систем за контрола на верзијата Git, промените не се прикажани", + "session.review.noVcs.createGit.title": "Направете складиште Git", + "session.review.noVcs.createGit.description": "Следете, прегледувајте и поништете ги промените во овој проект", + "session.review.noVcs.createGit.actionLoading": "Се создава складиште Git...", + "session.review.noVcs.createGit.action": "Креирај Git складиште", + "session.review.noSnapshot": + "Следењето снимки е оневозможено во конфигурацијата, така што промените на сесиите не се достапни", + "session.review.noChanges": "Нема промени", + "session.review.noUncommittedChanges": "Сè уште нема необврзани промени", + "session.review.noBranchChanges": "Сè уште нема промени во гранката", + "session.files.selectToOpen": "Изберете датотека за отворање", + "session.files.all": "Сите датотеки", + "session.files.empty": "Нема датотеки", + "session.files.binaryContent": "Бинарна датотека (содржината не може да се прикаже)", + "session.messages.renderEarlier": "Рендерирајте ги претходните пораки", + "session.messages.loadingEarlier": "Се вчитуваат претходните пораки...", + "session.messages.loadEarlier": "Вчитај претходни пораки", + "session.messages.loading": "Се вчитуваат пораки...", + "session.messages.jumpToLatest": "Скокни до најновото", + "session.context.addToContext": "Додајте {{selection}} во контекст", + "session.todo.title": "Тодос", + "session.todo.collapse": "Колапс", + "session.todo.expand": "Прошири", + "session.todo.progress": "{{done}} од {{total}} завршени работи", + "session.question.progress": "{{current}} од {{total}} прашања", + "session.question.minimize": "Минимизирајте го прашањето", + "session.question.restore": "Врати прашање", + "session.question.pending.one": "{{count}} нерешено прашање", + "session.question.pending.other": "{{count}} нерешени прашања", + "session.followupDock.summary.one": "{{count}} порака во редица", + "session.followupDock.summary.other": "{{count}} пораки во редица", + "session.followupDock.sendNow": "Испрати сега", + "session.followupDock.edit": "Уреди", + "session.followupDock.collapse": "Собери ги пораките во редица", + "session.followupDock.expand": "Проширете ги пораките во редица", + "session.revertDock.summary.one": "{{count}} вратена порака", + "session.revertDock.summary.other": "{{count}} вратени пораки", + "session.revertDock.collapse": "Собери ги вратените пораки", + "session.revertDock.expand": "Проширете ги вратените пораки", + "session.revertDock.restore": "Врати порака", + "session.new.title": "Изгради било што", + "session.new.project.new": "Нов проект", + "session.new.project.search": "Пребарувајте проекти", + "session.new.project.add": "Додадете проект", + "session.new.worktree.main": "Главна гранка", + "session.new.worktree.mainWithBranch": "Главна филијала ({{branch}})", + "session.new.worktree.create": "Креирајте ново работно дрво", + "session.new.workspace.runIn": "Извршете ја сесијата внатре", + "session.new.workspace.triggerLocal": "Локално", + "session.new.workspace.local": "Локално складиште", + "session.new.workspace.existing": "Работен простор…", + "session.new.git.none": "Не Git", + "session.new.lastModified": "Последно изменето", + "session.header.search.placeholder": "Барај {{project}}", + "session.header.searchFiles": "Пребарувајте датотеки", + "session.header.openIn": "Отвори внатре", + "session.header.open.action": "Отвори {{app}}", + "session.header.open.ariaLabel": "Отвори во {{app}}", + "session.header.open.menu": "Отворете ги опциите", + "session.header.open.copyPath": "Копирај патека", + "session.header.open.finder": "Пронаоѓач", + "session.header.open.fileExplorer": "Истражувач на датотеки", + "session.header.open.fileManager": "Управувач со датотеки", + "session.header.reveal.finder": "Откријте во Пронаоѓач", + "session.header.reveal.fileExplorer": "Откријте во File Explorer", + "session.header.reveal.containingFolder": "Отворете ја папката што содржи", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Терминал", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Статус", + "status.popover.ariaLabel": "Конфигурации на серверот", + "status.popover.tab.servers": "Сервери", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Приклучоци", + "status.popover.action.manageServers": "Управувајте со серверите", + "session.share.popover.title": "Објави на веб", + "session.share.popover.description.shared": "Оваа сесија е јавна на веб. Достапно е за секој со врската.", + "session.share.popover.description.unshared": + "Споделете ја сесијата јавно на веб. Ќе биде достапен за секој со врската.", + "session.share.action.share": "Споделете", + "session.share.action.publish": "Објави", + "session.share.action.publishing": "Објавување...", + "session.share.action.unpublish": "Отпишете го", + "session.share.action.unpublishing": "Се откажува објавувањето...", + "session.share.action.view": "Прикажи", + "session.share.copy.copied": "Копирано", + "session.share.copy.copyLink": "Копирај ја врската", + "lsp.tooltip.none": "Нема LSP сервери", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Се вчитува известувањето...", + "terminal.loading": "Се вчитува терминал...", + "terminal.title": "Терминал", + "terminal.title.numbered": "Терминал {{number}}", + "terminal.close": "Затворете го терминалот", + "terminal.connectionLost.title": "Изгубена врска", + "terminal.connectionLost.abnormalClose": "WebSocket затворено ненормално: {{code}}", + "terminal.connectionLost.description": + "Приклучувањето на терминалот беше прекинато. Ова може да се случи кога серверот се рестартира.", + "terminal.connectTicket.csrfError": + "PTY билет за поврзување одбиен по потекло или CSRF проверки. Проверете ја конфигурацијата на серверот CORS.", + "terminal.connectTicket.statusError": "PTY билетот за поврзување не успеа со {{status}}", + "titlebar.update": "Ажурирање", + "titlebar.updateVersion": "Ажурирај {{version}}", + "common.closeTab": "Затвори ја картичката", + "common.dismiss": "Отфрли", + "common.moreCountSuffix": "(+{{count}} повеќе)", + "common.requestFailed": "Барањето не успеа", + "common.moreOptions": "Повеќе опции", + "common.learnMore": "Дознајте повеќе", + "common.rename": "Преименувај", + "common.export": "Извоз", + "common.reset": "Ресетирај", + "common.archive": "Архива", + "common.delete": "Избриши", + "common.close": "Затвори", + "common.edit": "Уреди", + "common.loadMore": "Вчитај повеќе", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "непознат", + "common.time.justNow": "Само сега", + "common.time.minutesAgo.short": "Пред {{count}}м", + "common.time.hoursAgo.short": "Пред {{count}}ч", + "common.time.daysAgo.short": "Пред {{count}}ден", + "sidebar.menu.toggle": "Вклучете го менито", + "sidebar.nav.projectsAndSessions": "Проекти и сесии", + "sidebar.settings": "Поставки", + "sidebar.help": "Помош", + "sidebar.workspaces.enable": "Овозможете работни простори", + "sidebar.workspaces.disable": "Оневозможи работни простори", + "sidebar.gettingStarted.title": "Започнување", + "sidebar.gettingStarted.line1": "OpenCode вклучува бесплатни модели за да можете веднаш да започнете.", + "sidebar.gettingStarted.line2": "Поврзете кој било провајдер за да користите модели, вкл. Claude, GPT, Gemini итн.", + "sidebar.project.recentSessions": "Неодамнешни сесии", + "sidebar.project.viewAllSessions": "Прикажи ги сите сесии", + "sidebar.project.clearNotifications": "Исчистете ги известувањата", + "sidebar.empty.title": "Нема отворени проекти", + "sidebar.empty.description": "Отворете проект за да започнете", + "debugBar.ariaLabel": "Дијагностика на перформансите на развојот", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Последна завршена транзиција на маршрута со допирање на страница со сесија, мерена од почетокот на рутерот до првата боја откако ќе се среди.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Тркалачки рамки во секунда во последните 5 секунди.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Најлошо време на слики во последните 5 секунди.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Кадри над 32 ms во последните 5 секунди.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Блокирано време и број на долги задачи во последните 5 секунди. Максимална задача: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Најлошо забележано доцнење на влезот во последните 5 секунди.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Приближно времетраење на интеракцијата во последните 5 секунди. Ова е слично на INP, а не на официјалниот Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Кумулативно поместување на распоредот за тековниот животен век на апликацијата.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Користени JS куп наспроти граница на куп. Само Chromium.", + "debugBar.mem.tip": "Користени JS куп наспроти граница на куп. {{used}} од {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Принудете ги стиловите на фокусирање на сите интерактивни елементи", + "debugBar.focus.on": "ВКЛУЧЕНО", + "debugBar.focus.off": "ИСКЛУЧЕНО", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": + "Присилете ја насоката на целосниот распоред на апликацијата без да го менувате избраниот јазик", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Десктоп", + "settings.section.server": "Сервер", + "settings.tab.general": "Општо", + "settings.tab.shortcuts": "Кратенки", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL интеграција", + "settings.desktop.wsl.description": "Стартувај го серверот OpenCode во WSL на Windows.", + "settings.general.section.appearance": "Изглед", + "settings.general.section.advanced": "Напредно", + "settings.general.section.notifications": "Системски известувања", + "settings.general.section.updates": "Ажурирања", + "settings.general.section.sounds": "Звучни ефекти", + "settings.general.section.feed": "Довод", + "settings.general.section.display": "Приказ", + "settings.general.row.language.title": "Јазик", + "settings.general.row.language.description": "Променете го јазикот на прикажување за OpenCode", + "settings.general.row.shell.title": "Терминал shell", + "settings.general.row.shell.description": "Школката ја користат алатките за терминал и агент", + "settings.general.row.shell.autoDefault": "Автоматски (стандардно)", + "settings.general.row.shell.terminalOnly": "само терминал", + "settings.general.row.appearance.title": "Изглед", + "settings.general.row.appearance.description": "Приспособете како изгледа OpenCode на вашиот уред", + "settings.general.row.colorScheme.title": "Шема на бои", + "settings.general.row.colorScheme.description": "Изберете дали OpenCode ја следи системската, светла или темна тема", + "settings.general.row.theme.title": "Тема", + "settings.general.row.theme.description": "Приспособете како OpenCode е тематизирана.", + "settings.general.row.font.title": "Фонт на код", + "settings.general.row.font.description": "Приспособете го фонтот што се користи во блоковите со кодови", + "settings.general.row.terminalFont.title": "Терминален фонт", + "settings.general.row.terminalFont.description": "Приспособете го фонтот што се користи во терминалот", + "settings.general.row.uiFont.title": "UI фонт", + "settings.general.row.uiFont.description": "Прилагодете го фонтот што се користи низ интерфејсот", + "settings.general.row.followup.title": "Последователно однесување", + "settings.general.row.followup.description": + "Изберете дали следењето ќе ве поттикне да се управува веднаш или да чекате во редица", + "settings.general.row.followup.option.queue": "Ред", + "settings.general.row.followup.option.steer": "Управувај", + "settings.general.row.showFileTree.title": "Датотека", + "settings.general.row.showFileTree.description": "Прикажи го панелот за стебло на датотеки во сесии", + "settings.general.row.showNavigation.title": "Контроли за навигација", + "settings.general.row.showNavigation.description": + "Покажете ги копчињата назад и напред во насловната лента на работната површина", + "settings.general.row.showSearch.title": "Палета на команди", + "settings.general.row.showSearch.description": + "Прикажи го копчето за пребарување и палета на команди во лентата за наслов", + "settings.general.row.showTerminal.title": "Терминал", + "settings.general.row.showTerminal.description": + "Прикажете го копчето за терминал во насловната лента на работната површина", + "settings.general.row.showStatus.title": "Статус на серверот", + "settings.general.row.showStatus.description": "Прикажи го копчето за статус на серверот во насловната лента", + "settings.general.row.mobileTitlebarBottom.title": "Долна навигација", + "settings.general.row.mobileTitlebarBottom.description": + "Поставете ја лентата за наслов и јазичињата за сесии на дното на екранот на мобилниот телефон", + "settings.general.row.showCustomAgents.title": "Покажи агент", + "settings.general.row.showCustomAgents.description": + "Префрлете се помеѓу агенти во композиторот. Кога е скриено, стандардно е Build agent.", + "settings.general.row.reasoningSummaries.title": "Прикажи резимеа за расудување", + "settings.general.row.reasoningSummaries.description": + "Прикажи резимеа на расудувањето на моделите во временската линија", + "settings.general.row.shellToolPartsExpanded.title": "Проширете ги shell делови од алатот", + "settings.general.row.shellToolPartsExpanded.description": + "Прикажи shell делови од алатката стандардно проширени во временската линија", + "settings.general.row.editToolPartsExpanded.title": "Проширете ги деловите на алатката за уредување", + "settings.general.row.editToolPartsExpanded.description": + "Прикажи ги деловите на алатката за уредување, пишување и закрпи стандардно проширени во временската линија", + "settings.general.row.newInterface.title": "Нов распоред", + "settings.general.row.newInterface.badge": "Ново", + "settings.general.row.newInterface.description": + "Користете ги новите јазичиња и распоредот на домот. Префрлете се помеѓу распоредот за ограничено време.", + "settings.general.row.newInterfaceNotice.title": "Сега користите нов распоред", + "settings.general.row.newInterfaceNotice.description": "Претходниот распоред веќе не е достапен", + "settings.general.row.newInterfaceNotice.dismiss": "Отфрли", + "settings.general.row.pinchZoom.title": "Стиснете за да зумирате", + "settings.general.row.pinchZoom.description": + "Дозволете зумирање на гестовите за стискање на подлогата и Ctrl-scroll", + "settings.general.row.wayland.title": "Користете мајчин Wayland", + "settings.general.row.wayland.description": "Оневозможи резервни X11 на Wayland. Потребно е рестартирање.", + "settings.general.row.wayland.tooltip": + "На Linux со мешани монитори со стапка на освежување, мајчин Wayland може да биде постабилен.", + "settings.general.row.releaseNotes.title": "Белешки за ослободување", + "settings.general.row.releaseNotes.description": "Прикажи скокачки прозорци „Што има ново“ по ажурирањата", + "settings.updates.row.startup.title": "Проверете дали има ажурирања при стартување", + "settings.updates.row.startup.description": "Автоматски проверувајте дали има ажурирања кога ќе се стартува OpenCode", + "settings.updates.row.check.title": "Проверете дали има ажурирања", + "settings.updates.row.check.description": "Рачно проверете дали има ажурирања и инсталирајте ако е достапно", + "settings.updates.action.checkNow": "Проверете сега", + "settings.updates.action.checking": "Се проверува...", + "settings.updates.action.downloading": "Се презема...", + "settings.updates.action.installing": "Се инсталира...", + "settings.updates.toast.latest.title": "Ажуриран си", + "settings.updates.toast.latest.description": "Ја извршувате најновата верзија на OpenCode.", + "sound.option.none": "Никој", + "sound.option.alert01": "Аларм 01", + "sound.option.alert02": "Аларм 02", + "sound.option.alert03": "Алармирање 03", + "sound.option.alert04": "Аларм 04", + "sound.option.alert05": "Аларм 05", + "sound.option.alert06": "Аларм 06", + "sound.option.alert07": "Аларм 07", + "sound.option.alert08": "Аларм 08", + "sound.option.alert09": "Алармирање 09", + "sound.option.alert10": "Предупредување 10", + "sound.option.bipbop01": "Бип-боп 01", + "sound.option.bipbop02": "Бип-боп 02", + "sound.option.bipbop03": "Бип-боп 03", + "sound.option.bipbop04": "Бип-боп 04", + "sound.option.bipbop05": "Бип-боп 05", + "sound.option.bipbop06": "Бип-боп 06", + "sound.option.bipbop07": "Бип-боп 07", + "sound.option.bipbop08": "Бип-боп 08", + "sound.option.bipbop09": "Бип-боп 09", + "sound.option.bipbop10": "Бип-боп 10", + "sound.option.staplebops01": "Стејплбопс 01", + "sound.option.staplebops02": "Стејплбопс 02", + "sound.option.staplebops03": "Стејплбопс 03", + "sound.option.staplebops04": "Стејплбопс 04", + "sound.option.staplebops05": "Стејплбопс 05", + "sound.option.staplebops06": "Стејплбопс 06", + "sound.option.staplebops07": "Стејплбопс 07", + "sound.option.nope01": "Не 01", + "sound.option.nope02": "Не 02", + "sound.option.nope03": "Не 03", + "sound.option.nope04": "Не 04", + "sound.option.nope05": "Не 05", + "sound.option.nope06": "Не 06", + "sound.option.nope07": "Не 07", + "sound.option.nope08": "Не 08", + "sound.option.nope09": "Не 09", + "sound.option.nope10": "Не 10", + "sound.option.nope11": "Не 11", + "sound.option.nope12": "Не 12", + "sound.option.yup01": "Да 01", + "sound.option.yup02": "Да 02", + "sound.option.yup03": "Да 03", + "sound.option.yup04": "Да 04", + "sound.option.yup05": "Да 05", + "sound.option.yup06": "Да 06", + "settings.general.notifications.agent.title": "Агент", + "settings.general.notifications.agent.description": + "Прикажи системско известување кога агентот е завршен или има потреба од внимание", + "settings.general.notifications.permissions.title": "Дозволи", + "settings.general.notifications.permissions.description": "Прикажи системско известување кога е потребна дозвола", + "settings.general.notifications.errors.title": "Грешки", + "settings.general.notifications.errors.description": "Прикажи системско известување кога ќе се појави грешка", + "settings.general.sounds.agent.title": "Агент", + "settings.general.sounds.agent.description": "Пуштете звук кога агентот е завршен или му треба внимание", + "settings.general.sounds.permissions.title": "Дозволи", + "settings.general.sounds.permissions.description": "Репродуцирајте звук кога е потребна дозвола", + "settings.general.sounds.errors.title": "Грешки", + "settings.general.sounds.errors.description": "Репродуцирајте звук кога ќе се појави грешка", + "settings.shortcuts.title": "Кратенки на тастатурата", + "settings.shortcuts.reset.button": "Ресетирање на стандардните", + "settings.shortcuts.reset.toast.title": "Кратенките се ресетираат", + "settings.shortcuts.reset.toast.description": "Кратенките на тастатурата се ресетирани на стандардните.", + "settings.shortcuts.conflict.title": "Кратенката веќе се користи", + "settings.shortcuts.conflict.description": "{{keybind}} е веќе доделен на {{titles}}.", + "settings.shortcuts.unassigned": "Недоделени", + "settings.shortcuts.pressKeys": "Притиснете ги копчињата", + "settings.shortcuts.search.placeholder": "Кратенки за пребарување", + "settings.shortcuts.search.empty": "Не се пронајдени кратенки", + "settings.shortcuts.group.general": "Општо", + "settings.shortcuts.group.session": "Сесија", + "settings.shortcuts.group.navigation": "Навигација", + "settings.shortcuts.group.modelAndAgent": "Модел и агент", + "settings.shortcuts.group.terminal": "Терминал", + "settings.shortcuts.group.prompt": "Прашај", + "settings.providers.title": "Даватели", + "settings.providers.description": "Поставките на провајдерот ќе може да се конфигурираат овде.", + "settings.providers.section.connected": "Поврзани провајдери", + "settings.providers.connected.empty": "Нема поврзани провајдери", + "settings.providers.connected.environmentDescription": "Поврзано од променливите на вашата околина", + "settings.providers.section.popular": "Популарни провајдери", + "settings.providers.custom.description": "Додадете OpenAI-компатибилен провајдер по база URL.", + "settings.providers.tag.environment": "Животна средина", + "settings.providers.tag.config": "Конфигурација", + "settings.providers.tag.custom": "Прилагодено", + "settings.providers.tag.other": "Друго", + "settings.models.title": "Модели", + "settings.models.description": "Поставките за моделот ќе можат да се конфигурираат овде.", + "settings.agents.title": "Агенти", + "settings.agents.description": "Овде ќе може да се конфигурираат поставките за агентот.", + "settings.commands.title": "Наредби", + "settings.commands.description": "Овде ќе може да се конфигурираат поставките за команди.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Овде ќе може да се конфигурираат поставките MCP.", + "settings.permissions.title": "Дозволи", + "settings.permissions.description": "Контролирајте кои алатки може да ги користи серверот стандардно.", + "settings.permissions.section.tools": "Алатки", + "settings.permissions.toast.updateFailed.title": "Не успеа да се ажурираат дозволите", + "settings.permissions.action.allow": "Дозволи", + "settings.permissions.action.ask": "Прашај", + "settings.permissions.action.deny": "Негирајте", + "settings.permissions.tool.read.title": "Прочитајте", + "settings.permissions.tool.read.description": "Читање датотека (се совпаѓа со патеката на датотеката)", + "settings.permissions.tool.edit.title": "Уреди", + "settings.permissions.tool.edit.description": "Изменете ги датотеките, вклучувајќи уредувања, пишувања и закрпи", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Усогласувајте ги датотеките користејќи шеми на глоб", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Пребарувајте ја содржината на датотеката користејќи регуларни изрази", + "settings.permissions.tool.list.title": "Список", + "settings.permissions.tool.list.description": "Наведете датотеки во директориумот", + "settings.permissions.tool.bash.title": "Баш", + "settings.permissions.tool.bash.description": "Извршете shell команди", + "settings.permissions.tool.task.title": "Задача", + "settings.permissions.tool.task.description": "Стартувајте под-агенти", + "settings.permissions.tool.skill.title": "Вештина", + "settings.permissions.tool.skill.description": "Вчитајте вештина по име", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Стартувај јазични прашања за серверот", + "settings.permissions.tool.todowrite.title": "Todo Напиши", + "settings.permissions.tool.todowrite.description": "Ажурирајте го списокот со задачи", + "settings.permissions.tool.webfetch.title": "Веб Преземи", + "settings.permissions.tool.webfetch.description": "Преземи содржина од URL", + "settings.permissions.tool.websearch.title": "Пребарување на веб", + "settings.permissions.tool.websearch.description": "Пребарувајте на интернет", + "settings.permissions.tool.external_directory.title": "Надворешен директориум", + "settings.permissions.tool.external_directory.description": "Пристап до датотеки надвор од директориумот на проектот", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Откријте повторени повици на алатката со идентичен влез", + "session.delete.failed.title": "Не успеа да се избрише сесијата", + "session.delete.title": "Избришете ја сесијата", + "session.delete.confirm": "Да се ​​избрише сесијата „{{name}}“?", + "session.delete.button": "Избришете ја сесијата", + "workspace.new": "Нов работен простор", + "workspace.type.local": "локални", + "workspace.type.sandbox": "песок", + "workspace.create.failed.title": "Не успеа да се создаде работен простор", + "workspace.delete.failed.title": "Не успеа да се избрише работниот простор", + "workspace.resetting.title": "Се ресетира работниот простор", + "workspace.resetting.description": "Ова може да потрае една минута.", + "workspace.reset.failed.title": "Не успеа да се ресетира работниот простор", + "workspace.reset.success.title": "Ресетирање на работниот простор", + "workspace.reset.success.description": "Работниот простор сега се совпаѓа со стандардната гранка.", + "workspace.error.stillPreparing": "Работниот простор сè уште се подготвува", + "workspace.status.checking": "Се проверува за неспоени промени...", + "workspace.status.error": "Не може да се потврди статусот на git.", + "workspace.status.clean": "Не се откриени неповрзани промени.", + "workspace.status.dirty": "Откриени несоединети промени во овој работен простор.", + "workspace.delete.title": "Избришете го работниот простор", + "workspace.delete.confirm": "Да се ​​избрише работниот простор „{{name}}“?", + "workspace.delete.button": "Избришете го работниот простор", + "workspace.reset.title": "Ресетирајте го работниот простор", + "workspace.reset.confirm": "Да се ​​ресетира работниот простор „{{name}}“?", + "workspace.reset.button": "Ресетирајте го работниот простор", + "workspace.reset.archived.none": "Нема да се архивираат активни сесии.", + "workspace.reset.archived.one": "1 сесија ќе биде архивирана.", + "workspace.reset.archived.many": "{{count}} сесиите ќе бидат архивирани.", + "workspace.reset.note": "Ова ќе го ресетира работниот простор за да одговара на стандардната гранка.", + "dialog.usageExceeded.dontShowAgain": "Не прикажувај повторно", +} diff --git a/packages/app/src/i18n/mn.ts b/packages/app/src/i18n/mn.ts new file mode 100644 index 0000000000000000000000000000000000000000..41c4fa085d1a8dfa76c946d51ffbd7b51506d2c5 --- /dev/null +++ b/packages/app/src/i18n/mn.ts @@ -0,0 +1,1168 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Файл", + "desktop.menu.edit": "Засварлах", + "desktop.menu.view": "Харах", + "desktop.menu.go": "Яв", + "desktop.menu.window": "Цонх", + "desktop.menu.help": "Туслаач", + "desktop.menu.checkForUpdates": "Шинэчлэлтүүдийг шалгана уу...", + "desktop.menu.settings": "Тохиргоо", + "desktop.menu.reloadWebview": "Дахин ачаалах Webview", + "desktop.menu.restart": "Дахин эхлүүлэх", + "desktop.menu.exportLogs": "Лог экспортлох...", + "desktop.menu.newSession": "Шинэ сесс", + "desktop.menu.openProject": "Нээлттэй төсөл...", + "desktop.menu.newWindow": "Шинэ цонх", + "desktop.menu.closeWindow": "Цонхыг хаах", + "desktop.menu.undo": "Буцаах", + "desktop.menu.redo": "Дахин хий", + "desktop.menu.cut": "Таслах", + "desktop.menu.copy": "Хуулбарлах", + "desktop.menu.paste": "Буулгах", + "desktop.menu.delete": "Устгах", + "desktop.menu.selectAll": "Бүгдийг сонгоно уу", + "desktop.menu.toggleSidebar": "Хажуугийн самбарыг сэлгэх", + "desktop.menu.toggleTerminal": "Терминал сэлгэх", + "desktop.menu.toggleFileTree": "Файлын модыг сэлгэх", + "desktop.menu.reload": "Дахин ачаалах", + "desktop.menu.toggleDeveloperTools": "Хөгжүүлэгчийн хэрэгслийг сэлгэх", + "desktop.menu.actualSize": "Бодит хэмжээ", + "desktop.menu.zoomIn": "Томруулах", + "desktop.menu.zoomOut": "Жижигрүүлэх", + "desktop.menu.toggleFullScreen": "Бүтэн дэлгэцийг асаах/унтраах", + "desktop.menu.back": "Буцах", + "desktop.menu.forward": "Урагшаа", + "desktop.menu.previousSession": "Өмнөх хуралдаан", + "desktop.menu.nextSession": "Дараагийн сесс", + "desktop.menu.previousProject": "Өмнөх төсөл", + "desktop.menu.nextProject": "Дараагийн төсөл", + "desktop.menu.minimize": "Багасгах", + "desktop.menu.maximize": "Томруулах", + "desktop.menu.documentation": "OpenCode Баримт бичиг", + "desktop.menu.supportForum": "Дэмжих форум", + "desktop.menu.shareFeedback": "Санал хүсэлтээ хуваалцах", + "desktop.menu.reportBug": "Алдаа мэдээлэх", + "desktop.menu.ariaLabel": "OpenCode цэс", + "desktop.updater.dialog.checkFailed.message": "Шинэчлэлтийг шалгаж чадсангүй.", + "desktop.updater.dialog.checkFailed.title": "Шинэчлэлтийн алдаа", + "desktop.updater.dialog.upToDate.message": "Та шинэчлэгдсэн байна.", + "desktop.updater.dialog.upToDate.title": "Шинэчлэлт байхгүй", + "desktop.updater.dialog.ready.message": "Шинэчлэлтийг {{version}} татсан. Одоо дахин эхлүүлэх үү?", + "desktop.updater.dialog.ready.title": "Шинэчлэлт бэлэн боллоо", + "desktop.updater.dialog.restart": "Дахин эхлүүлэх", + "desktop.updater.dialog.later": "Дараа нь", + "desktop.recovery.action.relaunch": "Дахин эхлүүлэх", + "desktop.recovery.action.exportLogs": "Экспортын бүртгэл", + "desktop.recovery.action.keepWaiting": "Үргэлжлүүлэн хүлээ", + "desktop.recovery.action.quit": "Гарах", + "desktop.recovery.loadFailed": "OpenCode ачаалж чадсангүй", + "desktop.recovery.terminated": "OpenCode цонх гэнэт хаагдсан", + "desktop.recovery.unresponsive": "OpenCode хариу өгөхгүй байна", + "desktop.recovery.unresponsive.detail": + "Та програмаа дахин ажиллуулж, бүртгэлийг нээж эсвэл үргэлжлүүлэн хүлээх боломжтой.", + "desktop.recovery.loadFailed.detail": "Цонх: {{window}}\nURL: {{url}}\nАлдаа: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Цонх: {{window}}\nШалтгаан: {{reason}}\nКод: {{code}}", + "desktop.recovery.unknown": "<үл мэдэгдэх>", + "desktop.dialog.chooseFolder": "Фолдер сонгоно уу", + "desktop.dialog.chooseFile": "Файл сонгоно уу", + "desktop.dialog.saveFile": "Файлыг хадгалах", + "desktop.dialog.files": "Файлууд", + "desktop.server.local": "Орон нутгийн сервер", + "desktop.wsl.error.windowsOnly": "WSL зөвхөн Windows дээр боломжтой", + "desktop.wsl.error.unavailable": "WSL боломжгүй байна", + "desktop.wsl.error.listInstalled": "Суулгасан WSL түгээлтийн жагсаалтыг гаргаж чадсангүй", + "desktop.wsl.error.listOnline": "Онлайн WSL түгээлтийн жагсаалтыг гаргаж чадсангүй", + "desktop.wsl.error.executeDistro": "Дистро дахь тушаалуудыг гүйцэтгэх боломжгүй", + "desktop.wsl.error.installWsl": "WSL суулгаж чадсангүй", + "desktop.wsl.error.installDistro": "Дистро суулгаж чадсангүй: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode суулгаж чадсангүй", + "desktop.wsl.error.alreadyAdded": "{{distro}} аль хэдийн нэмэгдсэн байна", + "desktop.wsl.error.opencodeMissing": "opencode энэ түгээлтэд суулгаагүй байна", + "desktop.wsl.error.opencodeCannotRun": "opencode суулгасан боловч ажиллуулж чадсангүй", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode-г {{distro}}-д суулгаагүй байна", + "desktop.wsl.error.updateVersion": + "OpenCode шинэчлэлт дууссан боловч {{distro}} мэдээлсэн хэвээр {{installed}}; хүлээгдэж буй {{expected}}", + "desktop.wsl.error.noVersion": "хувилбар байхгүй", + "desktop.wsl.error.serverExited": "WSL сервер эхлүүлсний дараа гарсан (код={{code}} дохио ={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL сервер эрүүл болохоосоо өмнө гарсан (код={{code}} дохио={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "{{distro}} эрүүл мэндийн үзлэгт хамрагдах хажуугийн машины хугацаа {{timeout}}мс дараа дууссан", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} хугацаа {{timeout}}мс дараа дууссан", + "desktop.wsl.error.failedPort": "Портыг авч чадсангүй", + "desktop.picker.error.notSelected": "Файлыг сонгогч сонгоогүй", + "desktop.picker.error.sizeLimit": "Сонгосон хавсралтууд {{limit}} МБ хязгаараас хэтэрсэн", + "command.category.suggested": "Санал болгосон", + "command.category.view": "Харах", + "command.category.project": "Төсөл", + "command.category.provider": "Үйлчилгээ үзүүлэгч", + "command.category.server": "Сервер", + "command.category.session": "Сесс", + "command.category.theme": "Сэдэв", + "command.category.language": "Хэл", + "command.category.file": "Файл", + "command.category.context": "Контекст", + "command.category.terminal": "Терминал", + "command.category.model": "Загвар", + "command.category.mcp": "MCP", + "command.category.agent": "Агент", + "command.category.permissions": "Зөвшөөрөл", + "command.category.workspace": "Ажлын талбар", + "command.category.settings": "Тохиргоо", + "command.logs.export": "Бүртгэлүүдийг экспортлох", + "theme.scheme.system": "Систем", + "theme.scheme.light": "Гэрэл", + "theme.scheme.dark": "Харанхуй", + "command.sidebar.toggle": "Хажуугийн самбарыг сэлгэх", + "command.project.open": "Нээлттэй төсөл", + "command.project.previous": "Өмнөх төсөл", + "command.project.next": "Дараагийн төсөл", + "command.project.index": "Төсөл рүү шилжих {{index}}", + "command.provider.connect": "Үйлчилгээ үзүүлэгчийг холбоно уу", + "command.server.switch": "Сервер солих", + "command.settings.open": "Тохиргоог нээх", + "command.session.previous": "Өмнөх сесс", + "command.session.next": "Дараагийн сесс", + "command.session.previous.unseen": "Өмнөх уншаагүй сесс", + "command.session.next.unseen": "Дараагийн уншаагүй сесс", + "command.session.archive": "Архивын сесс", + "command.palette": "Командын палитр", + "command.theme.cycle": "Циклийн сэдэв", + "command.theme.set": "Сэдвийг ашиглах: {{theme}}", + "command.theme.scheme.cycle": "Циклийн өнгөний схем", + "command.theme.scheme.set": "Өнгөний схемийг ашиглах: {{scheme}}", + "command.language.cycle": "Циклийн хэл", + "command.language.set": "Хэл ашиглах: {{language}}", + "command.session.new": "Шинэ сесс", + "command.file.open": "Файлыг нээх", + "command.tab.close": "Табыг хаах", + "command.tab.reopenClosed": "Хаагдсан табыг дахин нээнэ үү", + "command.context.addSelection": "Контекст сонголт нэмэх", + "command.context.addSelection.description": "Одоогийн файлаас сонгосон мөрүүдийг нэмнэ үү", + "command.input.focus": "Фокус оруулах", + "command.terminal.toggle": "Терминал сэлгэх", + "command.fileTree.toggle": "Файлын модыг сэлгэх", + "command.review.toggle": "Шүүмжийг асаах/унтраах", + "command.terminal.new": "Шинэ терминал", + "command.terminal.new.description": "Шинэ терминалын таб үүсгэ", + "command.steps.toggle": "Алхам сэлгэх", + "command.steps.toggle.description": "Одоогийн мессежийн алхмуудыг харуулах эсвэл нуух", + "command.message.previous": "Өмнөх зурвас", + "command.message.previous.description": "Өмнөх хэрэглэгчийн мессеж рүү очно уу", + "command.message.next": "Дараагийн зурвас", + "command.message.next.description": "Дараагийн хэрэглэгчийн мессеж рүү очно уу", + "command.model.choose": "Загвар сонгох", + "command.model.choose.description": "Өөр загвар сонгоно уу", + "command.mcp.toggle": "MCPс сэлгэх", + "command.mcp.toggle.description": "MCPс сэлгэх", + "command.agent.cycle": "Циклийн агент", + "command.agent.cycle.description": "Дараагийн агент руу шилжих", + "command.agent.cycle.reverse": "Агентийг арагш эргүүлэх", + "command.agent.cycle.reverse.description": "Өмнөх агент руу шилжих", + "command.model.variant.cycle": "Цикл сэтгэх хүчин чармайлт", + "command.model.variant.cycle.description": "Дараагийн хүчин чармайлтын түвшинд шилжинэ", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Промпт", + "command.permissions.autoaccept.enable": "Зөвшөөрлийг автоматаар хүлээн авах", + "command.permissions.autoaccept.disable": "Зөвшөөрлийг автоматаар хүлээн авахаа зогсоо", + "command.workspace.toggle": "Ажлын талбаруудыг сэлгэх", + "command.workspace.toggle.description": + "Хажуугийн самбар дээрх олон ажлын талбарыг идэвхжүүлэх эсвэл идэвхгүй болгох", + "command.session.undo": "Буцаах", + "command.session.undo.description": "Сүүлийн мессежийг буцаах", + "command.session.redo": "Дахин хий", + "command.session.redo.description": "Сүүлийн буцаагдсан мессежийг дахин хийнэ үү", + "command.session.compact": "Компакт сесс", + "command.session.compact.description": "Контекстийн хэмжээг багасгахын тулд сессийг нэгтгэн дүгнэ", + "command.session.fork": "Зурвасаас салаалах", + "command.session.fork.description": "Өмнөх зурвасаас шинэ сесс үүсгэнэ үү", + "command.session.share": "Сеанс хуваалцах", + "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": "Илэрц олдсонгүй", + "palette.group.commands": "Тушаалууд", + "palette.group.files": "Файлууд", + "dialog.provider.search.placeholder": "Үйлчилгээ үзүүлэгчдийг хайх", + "dialog.provider.empty": "Үйлчилгээ үзүүлэгч олдсонгүй", + "dialog.provider.group.popular": "Алдартай", + "dialog.provider.group.other": "Бусад", + "dialog.provider.custom.label": "Захиалгат OpenAI-тохиромжтой үйлчилгээ үзүүлэгч", + "dialog.provider.tag.recommended": "Санал болгож байна", + "dialog.provider.opencode.note": "Claude, GPT, Gemini болон бусад загваруудыг сонгон шалгаруулсан", + "dialog.provider.opencode.tagline": "Найдвартай оновчтой загварууд", + "dialog.provider.opencodeGo.tagline": "Хүн бүрт зориулсан хямд өртөгтэй захиалга", + "dialog.provider.anthropic.note": "Pro болон Max зэрэг Claude загварт шууд хандах", + "dialog.provider.copilot.note": "GitHub Copilot-ээр дамжуулан кодлоход туслах хиймэл оюун ухааны загварууд", + "dialog.provider.openai.note": "GPT AI-ийн хурдан, чадвартай ерөнхий ажлуудад зориулсан загварууд", + "dialog.provider.google.note": "Gemini хурдан, бүтэцтэй хариулт өгөх загварууд", + "dialog.provider.openrouter.note": "Нэг үйлчилгээ үзүүлэгчээс дэмжигдсэн бүх загварт хандах", + "dialog.provider.vercel.note": "Ухаалаг чиглүүлэлт бүхий AI загваруудад нэгдсэн хандалт", + "dialog.model.select.title": "Загвар сонгох", + "dialog.model.search.placeholder": "Загвар хайх", + "dialog.model.empty": "Загварын үр дүн алга", + "dialog.model.manage": "Загваруудыг удирдах", + "dialog.model.manage.description": "Загвар сонгогч дээр ямар загвар гарч ирэхийг тохируулна уу.", + "dialog.model.manage.provider.toggle": "Бүх {{provider}} загварыг сэлгэх", + "dialog.model.unpaid.freeModels.title": "OpenCode өгсөн үнэгүй загварууд", + "dialog.model.unpaid.addMore.title": "Алдартай үйлчилгээ үзүүлэгчээс илүү олон загварыг нэмнэ үү", + "dialog.model.unpaid.viewMoreProviders": "Өөр 70 гаруй үйлчилгээ үзүүлэгчийг харна уу", + "dialog.provider.viewAll": "Илүү олон үйлчилгээ үзүүлэгчийг харуулах", + "provider.connect.title": "Холбох {{provider}}", + "provider.connect.title.anthropicProMax": "Claude Pro/Max-аар нэвтэрнэ үү", + "provider.connect.selectMethod": "{{provider}}-д нэвтрэх аргыг сонгоно уу.", + "provider.connect.method.apiKey": "API түлхүүр", + "provider.connect.method.browser": "Хөтөч", + "provider.connect.method.headless": "Толгойгүй", + "provider.connect.status.inProgress": "Зөвшөөрөл хийгдэж байна...", + "provider.connect.status.waiting": "Зөвшөөрөл хүлээж байна...", + "provider.connect.status.failed": "Зөвшөөрөл амжилтгүй болсон: {{error}}", + "provider.connect.apiKey.description": + "Бүртгэлээ холбохын тулд {{provider}} API түлхүүрээ оруулаад OpenCode-д {{provider}} загварыг ашиглана уу.", + "provider.connect.apiKey.label": "{{provider}} API түлхүүр", + "provider.connect.apiKey.placeholder": "API түлхүүр", + "provider.connect.apiKey.required": "API түлхүүр шаардлагатай", + "provider.connect.opencodeZen.line1": + "OpenCode Zen нь танд кодлох агентуудад зориулсан найдвартай оновчтой загваруудын цуглуулгад хандах боломжийг олгоно.", + "provider.connect.opencodeZen.line2": + "Ганц API товчлуураар та Claude, GPT, Gemini, GLM болон бусад загваруудад хандах боломжтой болно.", + "provider.connect.opencodeZen.visit.prefix": "зочилно уу", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "API түлхүүрээ авахын тулд.", + "provider.connect.oauth.code.visit.prefix": "зочилно уу", + "provider.connect.oauth.code.visit.link": "энэ холбоос", + "provider.connect.oauth.code.visit.suffix": + "OpenCode дахь {{provider}} загваруудыг ашиглан бүртгэлээ холбохын тулд зөвшөөрлийн кодыг цуглуулах.", + "provider.connect.oauth.code.label": "{{method}} зөвшөөрлийн код", + "provider.connect.oauth.code.placeholder": "Зөвшөөрлийн код", + "provider.connect.oauth.code.required": "Зөвшөөрлийн код шаардлагатай", + "provider.connect.oauth.code.invalid": "Зөвшөөрлийн код буруу", + "provider.connect.oauth.auto.visit.prefix": "зочилно уу", + "provider.connect.oauth.auto.visit.link": "энэ холбоос", + "provider.connect.oauth.auto.visit.suffix": + "Доорх кодыг оруулаад бүртгэлээ холбож, OpenCode-д {{provider}} загварыг ашиглана уу.", + "provider.connect.oauth.auto.confirmationCode": "Баталгаажуулах код", + "provider.connect.toast.connected.title": "{{provider}} холбогдсон", + "provider.connect.toast.connected.description": "{{provider}} загваруудыг ашиглах боломжтой боллоо.", + "provider.custom.title": "Захиалгат үйлчилгээ үзүүлэгч", + "provider.custom.unavailable": "Тусгай үйлчилгээ үзүүлэгчид энэ сервер дээр боломжгүй байна", + "provider.custom.description.prefix": "OpenAI нийцтэй үйлчилгээ үзүүлэгчийг тохируулна уу. -г үзнэ үү", + "provider.custom.description.link": "үйлчилгээ үзүүлэгчийн тохиргооны баримтууд", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Үйлчилгээ үзүүлэгчийн ID", + "provider.custom.field.providerID.placeholder": "миний үйлчилгээ үзүүлэгч", + "provider.custom.field.providerID.description": "Жижиг үсэг, тоо, зураас эсвэл доогуур зураас", + "provider.custom.field.name.label": "Дэлгэцийн нэр", + "provider.custom.field.name.placeholder": "Миний AI үйлчилгээ үзүүлэгч", + "provider.custom.field.baseURL.label": "Үндсэн URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API түлхүүр", + "provider.custom.field.apiKey.placeholder": "API түлхүүр", + "provider.custom.field.apiKey.description": + "Нэмэлт. Хэрэв та гарчиг ашиглан баталгаажуулалтыг удирдаж байгаа бол хоосон орхино уу.", + "provider.custom.models.label": "Загварууд", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "загвар ID", + "provider.custom.models.name.label": "Нэр", + "provider.custom.models.name.placeholder": "Дэлгэцийн нэр", + "provider.custom.models.remove": "Загварыг арилгах", + "provider.custom.models.add": "Загвар нэмэх", + "provider.custom.headers.label": "Гарчиг (заавал биш)", + "provider.custom.headers.key.label": "Толгой хэсэг", + "provider.custom.headers.key.placeholder": "Толгой хэсэг-Нэр", + "provider.custom.headers.value.label": "Үнэ цэнэ", + "provider.custom.headers.value.placeholder": "үнэ цэнэ", + "provider.custom.headers.remove": "Толгой хэсгийг арилгах", + "provider.custom.headers.add": "Гарчиг нэмэх", + "provider.custom.error.providerID.required": "Үйлчилгээ үзүүлэгчийн ID шаардлагатай", + "provider.custom.error.providerID.format": "Жижиг үсэг, тоо, зураас эсвэл доогуур зураас ашиглана уу", + "provider.custom.error.providerID.exists": "Энэ үйлчилгээ үзүүлэгчийн ID аль хэдийн байна", + "provider.custom.error.name.required": "Дэлгэцийн нэр шаардлагатай", + "provider.custom.error.baseURL.required": "URL суурь шаардлагатай", + "provider.custom.error.baseURL.format": "http:// эсвэл https:// гэж эхлэх ёстой", + "provider.custom.error.required": "Шаардлагатай", + "provider.custom.error.duplicate": "Давхардсан", + "provider.disconnect.toast.disconnected.title": "{{provider}} салгагдсан", + "provider.disconnect.toast.disconnected.description": "{{provider}} загварууд байхгүй болсон.", + "model.tag.free": "Үнэгүй", + "model.tag.latest": "Хамгийн сүүлийн үеийн", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "текст", + "model.input.image": "зураг", + "model.input.audio": "аудио", + "model.input.video": "видео", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Зөвшөөрөгдсөн: {{inputs}}", + "model.tooltip.context.label": "Контекст", + "model.tooltip.inputs": "Оролтууд", + "model.tooltip.model": "Загвар", + "model.tooltip.provider": "Үйлчилгээ үзүүлэгч", + "model.tooltip.reasoning": "Үндэслэл", + "model.tooltip.reasoning.allowed": "Үндэслэл гаргахыг зөвшөөрдөг", + "model.tooltip.reasoning.none": "Ямар ч үндэслэлгүй", + "model.tooltip.context": "Контекст хязгаар {{limit}}", + "common.search.placeholder": "Хайх", + "common.goBack": "Буцах", + "common.goForward": "Урагшаа чиглүүлэх", + "common.loading": "Ачааж байна", + "common.loading.ellipsis": "...", + "common.cancel": "Цуцлах", + "common.clear": "Тодорхой", + "common.open": "Нээлттэй", + "common.connect": "Холбох", + "common.disconnect": "Салга", + "common.continue": "Үргэлжлүүлэх", + "common.submit": "Илгээх", + "common.save": "Хадгалах", + "common.saving": "Хадгалж байна...", + "common.default": "Өгөгдмөл", + "common.attachment": "хавсралт", + "prompt.placeholder.shell": "shell командыг оруулна уу... {{example}}", + "prompt.placeholder.normal": 'Юу ч асуугаарай... "{{example}}"', + "prompt.placeholder.simple": "Юу ч асуу...", + "prompt.placeholder.summarizeComments": "Сэтгэгдэлийг нэгтгэн дүгнэх...", + "prompt.placeholder.summarizeComment": "Сэтгэгдлээ нэгтгэн дүгнэх...", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Промпт", + "prompt.mode.shell.exit": "esc товчийг дарна уу", + "session.child.promptDisabled": "Subagent сессийг сануулах боломжгүй.", + "session.child.backToParent": "Үндсэн хуралдаан руу буцах.", + "prompt.example.1": "Кодын сан дахь TODO-г засах", + "prompt.example.2": "Энэ төслийн технологийн стек нь юу вэ?", + "prompt.example.3": "Эвдэрсэн тестийг засах", + "prompt.example.4": "Баталгаажуулалт хэрхэн ажилладагийг тайлбарлана уу", + "prompt.example.5": "Аюулгүй байдлын сул талуудыг олж засварлах", + "prompt.example.6": "Хэрэглэгчийн үйлчилгээний нэгжийн тестийг нэмнэ үү", + "prompt.example.7": "Уншихад хялбар болгохын тулд энэ функцийг дахин тохируулна уу", + "prompt.example.8": "Энэ алдаа юу гэсэн үг вэ?", + "prompt.example.9": "Энэ асуудлыг засахад тусална уу", + "prompt.example.10": "API бичиг баримт үүсгэх", + "prompt.example.11": "Өгөгдлийн сангийн асуулгыг оновчтой болгох", + "prompt.example.12": "Оролтын баталгаажуулалтыг нэмнэ үү", + "prompt.example.13": "Шинэ бүрэлдэхүүн хэсэг үүсгэх...", + "prompt.example.14": "Би энэ төслийг хэрхэн хэрэгжүүлэх вэ?", + "prompt.example.15": "Шилдэг туршлагыг авахын тулд миний кодыг шалгана уу", + "prompt.example.16": "Энэ функцэд алдаа засах аргыг нэмнэ үү", + "prompt.example.17": "Энэ regex загварыг тайлбарла", + "prompt.example.18": "Үүнийг TypeScript болгон хөрвүүлэх", + "prompt.example.19": "Кодын санд бүртгэл нэмэх", + "prompt.example.20": "Ямар хамаарал хуучирсан бэ?", + "prompt.example.21": "Шилжилтийн скрипт бичихэд тусална уу", + "prompt.example.22": "Энэ төгсгөлийн цэгт кэш хийх", + "prompt.example.23": "Энэ жагсаалтад хуудасны тэмдэглэгээ нэмнэ үү", + "prompt.example.24": "CLI тушаал үүсгэх...", + "prompt.example.25": "Энд орчны хувьсагч хэрхэн ажилладаг вэ?", + "prompt.popover.emptyResults": "Тохирох илэрц алга", + "prompt.popover.emptyCommands": "Тохирох команд алга", + "prompt.dropzone.label": "Зураг, PDFс, эсвэл текст файлуудыг энд буулгана уу", + "prompt.dropzone.file.label": "@дурдах файл руу буулгана уу", + "prompt.slash.badge.custom": "заншил", + "prompt.slash.badge.skill": "ур чадвар", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "идэвхтэй", + "prompt.context.includeActiveFile": "Идэвхтэй файлыг оруулах", + "prompt.context.removeActiveFile": "Контекстээс идэвхтэй файлыг устгана уу", + "prompt.context.removeFile": "Контекстээс файлыг устгах", + "prompt.action.attachFile": "Файл нэмэх", + "prompt.menu.addImagesAndFiles": "Файл нэмэх гэх мэт", + "prompt.menu.imagesAndFiles": "Зураг болон файлууд", + "prompt.menu.commands": "Тушаалууд", + "prompt.menu.context": "Контекст", + "prompt.menu.shellCommand": "Shell тушаал", + "prompt.attachment.remove": "Хавсралтыг устгана уу", + "prompt.action.send": "Илгээх", + "prompt.action.stop": "Зогс", + "prompt.toast.pasteUnsupported.title": "Дэмжигдээгүй хавсралт", + "prompt.toast.pasteUnsupported.description": "Энд зөвхөн зураг, PDFс, эсвэл текст файлыг хавсаргах боломжтой.", + "prompt.toast.attachmentDuplicate.title": "Энэ файлыг аль хэдийн байршуулсан байна", + "prompt.toast.modelAgentRequired.title": "Агент болон загварыг сонгоно уу", + "prompt.toast.modelAgentRequired.description": "Промпт илгээхээсээ өмнө агент болон загварыг сонгоно уу.", + "prompt.toast.worktreeCreateFailed.title": "Git worktree үүсгэж чадсангүй", + "prompt.toast.sessionCreateFailed.title": "Сесс үүсгэж чадсангүй", + "prompt.toast.shellSendFailed.title": "shell тушаалыг илгээж чадсангүй", + "prompt.toast.commandSendFailed.title": "Командыг илгээж чадсангүй", + "prompt.toast.promptSendFailed.title": "Промпт илгээж чадсангүй", + "prompt.toast.promptSendFailed.description": "Сессийг сэргээх боломжгүй", + "dialog.mcp.title": "MCPсек", + "dialog.mcp.description": "{{total}}-ийн {{enabled}}-г идэвхжүүлсэн", + "dialog.mcp.empty": "Тохируулсан MCPс байхгүй", + "dialog.lsp.empty": "LSPс файлын төрлөөс автоматаар илэрсэн", + "dialog.plugins.empty": "opencode.json-д тохируулсан залгаасууд", + "mcp.status.connected": "холбогдсон", + "mcp.status.failed": "амжилтгүй болсон", + "mcp.status.needs_auth": "auth хэрэгтэй", + "mcp.status.disabled": "тахир дутуу болсон", + "mcp.auth.clickToAuthenticate": "Баталгаажуулахын тулд товшино уу", + "dialog.fork.empty": "Салаалах зурвас алга", + "dialog.directory.search.placeholder": "Фолдер хайх", + "dialog.directory.empty": "Ямар ч фолдер олдсонгүй", + "dialog.directory.action.selectFile": "Файл сонгоно уу", + "dialog.directory.action.selectFolder": "Фолдер сонгоно уу", + "dialog.directory.root": "Үндэс", + "dialog.directory.parent": "Эцэг эх", + "dialog.directory.readError": "Энэ фолдерыг унших боломжгүй байна", + "app.server.unreachable": "{{server}}-д хүрч чадсангүй", + "app.server.retrying": "Автоматаар дахин оролдож байна...", + "app.server.otherServers": "Бусад серверүүд", + "dialog.server.title": "Серверүүд", + "dialog.server.description": "Энэ апп аль OpenCode серверт холбогдохыг сэлгэнэ үү.", + "dialog.server.search.placeholder": "Хайлтын серверүүд", + "dialog.server.empty": "Одоогоор сервер байхгүй байна", + "dialog.server.add.title": "Сервер нэмэх", + "dialog.server.add.url": "Серверийн хаяг", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Сервертэй холбогдож чадсангүй", + "dialog.server.add.checking": "Шалгаж байна...", + "dialog.server.add.button": "Сервер нэмэх", + "dialog.server.add.name": "Серверийн нэр (заавал биш)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Хэрэглэгчийн нэр (заавал биш)", + "dialog.server.add.usernamePlaceholder": "хэрэглэгчийн нэр", + "dialog.server.add.password": "Нууц үг (заавал биш)", + "dialog.server.add.passwordPlaceholder": "нууц үг", + "dialog.server.edit.title": "Сервер засах", + "dialog.server.default.title": "Өгөгдмөл сервер", + "dialog.server.default.description": + "Орон нутгийн серверийг эхлүүлэхийн оронд програм эхлүүлэх үед энэ сервертэй холбогдоно уу. Дахин эхлүүлэх шаардлагатай.", + "dialog.server.default.none": "Ямар ч сервер сонгогдоогүй байна", + "dialog.server.default.set": "Одоогийн серверийг өгөгдмөл болгож тохируулна уу", + "dialog.server.default.clear": "Тодорхой", + "dialog.server.action.remove": "Серверийг устгах", + "dialog.server.menu.edit": "Засварлах", + "dialog.server.menu.default": "Өгөгдмөл болгож тохируулах", + "dialog.server.menu.defaultRemove": "Өгөгдмөлийг арилгах", + "dialog.server.menu.delete": "Устгах", + "dialog.server.current": "Одоогийн сервер", + "dialog.server.status.default": "Өгөгдмөл", + "wsl.server.add": "WSL сервер нэмнэ үү", + "wsl.server.addShort": "WSL нэмэх", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL сервер", + "wsl.server.retryStart": "Дахин оролдох", + "wsl.server.updating": "Шинэчилж байна...", + "wsl.onboarding.step.distro": "Дистро сонгоно уу", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL-г шалгаж байна...", + "wsl.onboarding.restartRequired": "Windows-г WSL суулгаж дуусгахын тулд дахин эхлүүлэх шаардлагатай.", + "wsl.onboarding.ready": "WSL бэлэн боллоо.", + "wsl.onboarding.required": "Үргэлжлүүлэхийн тулд WSL шаардлагатай.", + "wsl.onboarding.checkingDistros": "Дистрог шалгаж байна...", + "wsl.onboarding.installingDistro": "{{distro}} суулгаж байна...", + "wsl.onboarding.checkingDistro": "{{distro}}-г шалгаж байна...", + "wsl.onboarding.listingDistros": "Жагсаалтын түгээлтүүд...", + "wsl.onboarding.distroReady": "{{distro}} бэлэн боллоо.", + "wsl.onboarding.distroNotInstalled": "{{distro}} суулгаагүй байна.", + "wsl.onboarding.openDistroOnce": "Тохиргоог дуусгахын тулд {{distro}}-г нэг удаа нээнэ үү.", + "wsl.onboarding.finishingDistro": "{{distro}}-н тохиргоог дуусгаж байна.", + "wsl.onboarding.pickDistro": "Дистро сонгох эсвэл доороос нэгийг суулгана уу.", + "wsl.onboarding.checkingOpencode": "OpenCode-г шалгаж байна...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}} дахь OpenCode-г шалгаж байна...", + "wsl.onboarding.updatingOpencode": "Шинэчилж байна OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}}-д OpenCode-г шинэчилж байна...", + "wsl.onboarding.updateOpencodeIn": "OpenCode-г {{distro}}-д шинэчилнэ үү.", + "wsl.onboarding.updateOpencode": "Шинэчлэх OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode {{distro}}-д бэлэн боллоо.", + "wsl.onboarding.opencodeReady": "OpenCode бэлэн боллоо.", + "wsl.onboarding.installOpencodeIn": "{{distro}}-д OpenCode суулгана уу.", + "wsl.onboarding.installOpencode": "OpenCode суулгах", + "wsl.onboarding.chooseDistroFirst": "Эхлээд дистро сонгоно уу.", + "wsl.onboarding.loadFailed": "WSL төлөвийг ачаалж чадсангүй.", + "wsl.onboarding.loading": "Ачааж байна...", + "wsl.onboarding.installedDistros": "Суулгасан түгээлтүүд", + "wsl.onboarding.checkAgain": "Дахин шалгана уу", + "wsl.onboarding.distroStatus.ready": "Бэлэн", + "wsl.onboarding.distroStatus.checking": "Шалгаж байна...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode суулгаагүй байна", + "wsl.onboarding.distroStatus.missingTools": "bash, curl алга", + "wsl.onboarding.distroStatus.unsupported": "Дэмжигдээгүй · WSL 2 ашиглах", + "wsl.onboarding.needAnotherDistro": "Өөр дистро хэрэгтэй байна уу?", + "wsl.onboarding.needAnotherDistroHint": "WSL каталогоос Linux түгээлтийг суулгана уу", + "wsl.onboarding.wslNotInstalled.title": "WSL суулгаагүй байна", + "wsl.onboarding.wslNotInstalled.description": + "OpenCode WSL сервер нэмэхийн өмнө WSL (Windows Subsystem for Linux) шаардлагатай", + "wsl.onboarding.wslUnavailable.title": "WSL боломжгүй", + "wsl.onboarding.wslUnavailable.description": "OpenCode энэ машин дээр WSL-г баталгаажуулж чадсангүй.", + "wsl.onboarding.installWsl": "WSL суулгах", + "wsl.onboarding.windowsRestartRequired": + "WSL-г суулгаж дуусгахын тулд Windows-г дахин эхлүүлээд OpenCode-г дахин нээнэ үү.", + "wsl.onboarding.next": "Дараа нь", + "wsl.onboarding.refresh": "Сэргээх", + "wsl.onboarding.allDistrosAdded": "Бүх суулгасан түгээлтүүд аль хэдийн нэмэгдсэн байна.", + "wsl.onboarding.noDistros": "Одоогоор ямар ч түгээлт илрээгүй байна.", + "wsl.onboarding.install": "Суулгах", + "wsl.onboarding.installing": "Суулгаж байна...", + "wsl.onboarding.installDistro": "Дистро суулгана уу", + "wsl.onboarding.searchDistros": "Дистро хайх", + "wsl.onboarding.wsl2Required": "WSL 2 шаардлагатай.", + "wsl.onboarding.toolsRequired": "Энэ дистрод bash болон curl хэрэгтэй.", + "wsl.onboarding.openTerminal": "Терминал нээх", + "wsl.onboarding.path": "Зам: {{path}}", + "wsl.onboarding.notFound": "олдсонгүй", + "wsl.onboarding.version": "Хувилбар: {{version}}", + "wsl.onboarding.unknown": "үл мэдэгдэх", + "wsl.onboarding.desktopVersion": "ширээний компьютер {{version}}", + "wsl.onboarding.versionMismatch": "Суулгасан хувилбар нь ширээний програмын хувилбартай таарахгүй байна.", + "wsl.onboarding.adding": "Нэмэж байна...", + "help.tabs.toast.ariaLabel": "Табуудыг танилцуулж байна. Таб ашиглан ажил болон идэвхтэй сешнүүдийг зохион байгуул", + "help.tabs.toast.dismiss": "Табын мэдээллийг хаах", + "help.tabs.title": "Табуудыг танилцуулж байна", + "help.tabs.description": "Таб ашиглан ажил болон идэвхтэй сешнүүдийг зохион байгуул", + "help.tabs.date": "Долдугаар сарын 14", + "help.tabs.introduction": "OpenCode Desktop одоо табуудыг тойрон бүтээгдсэн.", + "help.tabs.sessions": + "Таб дээрээс шинэ сесс эхлүүлэх эсвэл аль нэг төслөөсөө одоо байгаа сессийг нээнэ үү. Шинэ зүйл эхлүүлж байхдаа шинэ таб нээж, хийж дуусаад хаа.", + "help.tabs.organize": + "Цөөн хэдэн табыг нээлттэй байлгах нь идэвхтэй сешнүүдийг зохион байгуулахад хялбар болгодог. Хэрэв та тэдгээрийг эргэн тойронд байлгахаар төлөвлөж байгаа бол табуудыг мартагдашгүй зүйл болгон өөрчил.", + "help.tabs.home": + "Та бүх сесс болон төслүүдээ шинэ үндсэн нүүрэн дээрээс олох болно. Сешн сонгох нь таб дээр нээгдэнэ.", + "help.tabs.persistence": "Таныг програмыг дахин нээхэд таны таб нээлттэй хэвээр байна.", + "help.tabs.worktrees": + 'Шинэ загвар нь одоохондоо Git Worktrees-г дэмждэггүй, удахгүй гарах болно. Хэрэв та өмнөх бүдүүвчийг үргэлжлүүлэн ашиглахыг хүсвэл "Тохиргоо" хэсэгт байрлуулалт хооронд сэлгэх боломжтой. Хэдэн долоо хоногийн дараа шинэ зохион байгуулалт байнгын болно гэдгийг санаарай.', + "server.row.noUsername": "хэрэглэгчийн нэр байхгүй", + "dialog.project.edit.title": "Төслийг засварлах", + "dialog.project.edit.name": "Нэр", + "dialog.project.edit.icon": "Дүрс", + "dialog.project.edit.icon.alt": "Төслийн дүрс тэмдэг", + "dialog.project.edit.icon.hint": "Зураг дээр дарж эсвэл чирнэ үү", + "dialog.project.edit.icon.recommended": "Санал болгож буй: 128x128px", + "dialog.project.edit.color": "Өнгө", + "dialog.project.edit.color.select": "{{color}} өнгө сонгоно уу", + "dialog.project.edit.worktree.startup": "Ажлын талбарыг эхлүүлэх скрипт", + "dialog.project.edit.worktree.startup.description": "Шинэ ажлын талбар (Git worktree) үүсгэсний дараа ажиллана.", + "dialog.project.edit.worktree.startup.placeholder": "жишээ нь bun install", + "dialog.releaseNotes.action.getStarted": "Эхлээрэй", + "dialog.releaseNotes.action.next": "Дараа нь", + "dialog.releaseNotes.action.hideFuture": "Ирээдүйд эдгээрийг бүү харуул", + "dialog.releaseNotes.media.alt": "Хувилбарыг урьдчилан үзэх", + "context.breakdown.title": "Контекст задаргаа", + "context.breakdown.note": + 'Оролтын токенуудын ойролцоогоор задаргаа. "Бусад" нь багаж хэрэгслийн тодорхойлолт болон нэмэлт зардлыг агуулдаг.', + "context.breakdown.system": "Систем", + "context.breakdown.user": "Хэрэглэгч", + "context.breakdown.assistant": "Туслах", + "context.breakdown.tool": "Хэрэгслийн дуудлага", + "context.breakdown.other": "Бусад", + "context.systemPrompt.title": "Системийн промпт", + "context.rawMessages.title": "Түүхий мессежүүд", + "context.export.session": "Экспортын сесс", + "context.stats.session": "Сесс", + "context.stats.messages": "Мессежүүд", + "context.stats.provider": "Үйлчилгээ үзүүлэгч", + "context.stats.model": "Загвар", + "context.stats.limit": "Контекст хязгаар", + "context.stats.totalTokens": "Нийт жетон", + "context.stats.usage": "Хэрэглээ", + "context.stats.inputTokens": "Токен оруулах", + "context.stats.outputTokens": "Гаралтын токенууд", + "context.stats.reasoningTokens": "Үзэл бодлоо илэрхийлэх токенууд", + "context.stats.cacheTokens": "Кэш токенууд (унших/бичих)", + "context.stats.userMessages": "Хэрэглэгчийн мессежүүд", + "context.stats.assistantMessages": "Туслахын мессежүүд", + "context.stats.totalCost": "Нийт зардал", + "context.stats.sessionCreated": "Сеанс үүсгэсэн", + "context.stats.lastActivity": "Сүүлийн үйл ажиллагаа", + "context.usage.tokens": "Токенууд", + "context.usage.usage": "Хэрэглээ", + "context.usage.cost": "Зардал", + "context.usage.clickToView": "Контекстийг харахын тулд товшино уу", + "context.usage.view": "Контекст ашиглалтыг харах", + "language.en": "Англи", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Герман", + "language.es": "Испани", + "language.fr": "Франц", + "language.da": "Данск", + "language.ja": "日本語", + "language.pl": "Польски", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Норск", + "language.br": "Português (Бразил)", + "language.bs": "Босански", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Турк", + "toast.language.title": "Хэл", + "toast.language.description": "{{language}} руу шилжсэн", + "toast.theme.title": "Загварыг өөрчилсөн", + "toast.scheme.title": "Өнгөний схем", + "toast.workspace.enabled.title": "Ажлын талбарыг идэвхжүүлсэн", + "toast.workspace.enabled.description": "Хэд хэдэн Git worktree-г хажуугийн мөрөнд харуулав", + "toast.workspace.disabled.title": "Ажлын талбарыг идэвхгүй болгосон", + "toast.workspace.disabled.description": "Хажуугийн мөрөнд зөвхөн үндсэн Git worktree-г харуулав", + "toast.permissions.autoaccept.on.title": "Зөвшөөрлийг автоматаар хүлээн авах", + "toast.permissions.autoaccept.on.description": "Зөвшөөрлийн хүсэлт автоматаар батлагдах болно", + "toast.permissions.autoaccept.off.title": "Зөвшөөрлийг автоматаар хүлээн авахаа больсон", + "toast.permissions.autoaccept.off.description": "Зөвшөөрлийн хүсэлтэд зөвшөөрөл шаардлагатай", + "toast.model.none.title": "Ямар ч загвар сонгогдоогүй байна", + "toast.model.none.description": "Энэ сессийг дүгнэхийн тулд үйлчилгээ үзүүлэгчийг холбоно уу", + "toast.file.loadFailed.title": "Файлыг ачаалж чадсангүй", + "toast.file.listFailed.title": "Файлуудыг жагсааж чадсангүй", + "toast.context.noLineSelection.title": "Шугамын сонголт байхгүй", + "toast.context.noLineSelection.description": "Эхлээд файлын табаас мөрийн мужийг сонгоно уу.", + "toast.session.share.copyFailed.title": "URL-г санах ой руу хуулж чадсангүй", + "toast.session.share.success.title": "Сесс хуваалцсан", + "toast.session.share.success.description": "Хуваалцах URL санах ойд хуулсан!", + "toast.session.share.failed.title": "Сешн хуваалцаж чадсангүй", + "toast.session.share.failed.description": "Сешн хуваалцах явцад алдаа гарлаа", + "toast.session.unshare.success.title": "Сешн хуваалцаагүй", + "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": "Шинэчлэлт боломжтой", + "toast.update.description": "OpenCode ({{version}}) шинэ хувилбарыг одоо суулгах боломжтой.", + "toast.update.action.installRestart": "Суулгаад дахин эхлүүлнэ үү", + "toast.update.action.notYet": "Одоохондоо үгүй", + "error.page.title": "Ямар нэг алдаа гарлаа", + "error.page.description": "Програмыг ачаалах явцад алдаа гарлаа.", + "error.page.description.localServerStartup": "Дотоод серверийг эхлүүлэх явцад алдаа гарлаа.", + "error.page.details.label": "Алдааны дэлгэрэнгүй мэдээлэл", + "error.page.action.restart": "Дахин эхлүүлэх", + "error.page.action.report": "Алдаа мэдээлэх", + "error.page.action.reported": "Алдаа мэдээлсэн", + "error.page.action.exportLogs": "Экспортын бүртгэл", + "error.page.action.checking": "Шалгаж байна...", + "error.page.action.checkUpdates": "Шинэчлэлт байгаа эсэхийг шалгана уу", + "error.page.action.updateTo": "{{version}} руу шинэчлэх", + "error.page.circular": "[Тойрог]", + "error.page.report.prefix": "Энэ алдааг OpenCode багт мэдэгдэнэ үү", + "error.page.report.discord": "Discord дээр", + "error.page.version": "Хувилбар: {{version}}", + "error.dev.rootNotFound": + "Үндэс элемент олдсонгүй. Та үүнийг index.html дээрээ нэмэхээ мартсан уу? Эсвэл id атрибутыг буруу бичсэн байж магадгүй юм уу?", + "error.serverSync.connectFailed": "Сервертэй холбогдож чадсангүй. `{{url}}` дээр ажиллаж байгаа сервер байна уу?", + "error.serverSDK.noServerAvailable": "Сервер байхгүй байна", + "error.serverSDK.serverNotAvailable": "Сервер байхгүй байна", + "error.childStore.persistedCacheCreateFailed": "Тогтвортой кэшийг үүсгэж чадсангүй", + "error.childStore.persistedProjectMetadataCreateFailed": "Тогтвортой төслийн мета өгөгдлийг үүсгэж чадсангүй", + "error.childStore.persistedProjectIconCreateFailed": "Тогтвортой төслийн дүрсийг үүсгэж чадсангүй", + "error.childStore.storeCreateFailed": "Дэлгүүр үүсгэж чадсангүй", + "directory.error.invalidUrl": "URL дотор буруу лавлах.", + "error.chain.unknown": "Үл мэдэгдэх алдаа", + "error.server.invalidConfiguration": "Буруу тохиргоо", + "error.chain.causedBy": "Шалтгаан:", + "error.chain.apiError": "API алдаа", + "error.chain.status": "Статус: {{status}}", + "error.chain.retryable": "Дахин оролдох боломжтой: {{retryable}}", + "error.chain.responseBody": "Хариу өгөх байгууллага:\n{{body}}", + "error.chain.didYouMean": "Та: {{suggestions}} гэсэн үг үү", + "error.chain.modelNotFound": "Загвар олдсонгүй: {{provider}}/{{model}}", + "error.chain.checkConfig": "Өөрийн тохиргооны (opencode.json) үйлчилгээ үзүүлэгч/загварын нэрийг шалгана уу", + "error.chain.mcpFailed": + 'MCP сервер "{{name}}" амжилтгүй боллоо. OpenCode нь MCP баталгаажуулалтыг хараахан дэмждэггүйг анхаарна уу.', + "error.chain.providerAuthFailed": + "Үйлчилгээ үзүүлэгчийн баталгаажуулалт амжилтгүй болсон ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + '"{{provider}}" үйлчилгээ үзүүлэгчийг эхлүүлж чадсангүй. Итгэмжлэл болон тохиргоог шалгана уу.', + "error.chain.configJsonInvalid": "{{path}} дээрх тохиргооны файл хүчингүй байна JSON(C)", + "error.chain.configJsonInvalidWithMessage": "{{path}} дээрх тохиргооны файл буруу байна JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} доторх "{{dir}}" лавлах хүчингүй байна. Лавлах нэрийг "{{suggestion}}" болгож өөрчлөх эсвэл устгана уу. Энэ бол нийтлэг үсгийн алдаа юм.', + "error.chain.configFrontmatterError": "{{path}} дахь frontmatter-г задлан шинжилж чадсангүй:\n{{message}}", + "error.chain.configInvalid": "{{path}} дээрх тохиргооны файл буруу байна", + "error.chain.configInvalidWithMessage": "{{path}} дээрх тохиргооны файл буруу байна: {{message}}", + "notification.permission.title": "Зөвшөөрөл шаардлагатай", + "notification.permission.description": "{{projectName}} дахь {{sessionTitle}} зөвшөөрөл шаардлагатай", + "notification.question.title": "Асуулт", + "notification.question.description": "{{projectName}} дэх {{sessionTitle}} асуулт байна", + "notification.action.goToSession": "Сесс рүү оч", + "notification.session.responseReady.title": "Хариулт бэлэн", + "notification.session.error.title": "Сешн алдаа", + "notification.session.error.fallbackDescription": "Алдаа гарлаа", + "home.recentProjects": "Сүүлийн үеийн төслүүд", + "home.empty.title": "Сүүлийн үеийн төсөл байхгүй", + "home.empty.description": "Орон нутгийн төсөл нээж эхэл", + "home.title": "Гэр", + "home.projects": "Төслүүд", + "home.project.add": "Төсөл нэмэх", + "home.recentlyClosed": "Саяхан хаагдсан", + "home.server.collapse": "Серверийн төслүүдийг нураах", + "home.server.expand": "Серверийн төслүүдийг өргөжүүлэх", + "home.sessions.search.placeholder": "Сесс хайх", + "home.sessions.search.placeholder.scoped": "{{scope}} доторх сешнүүдийг хайх", + "home.sessions.search.sessions": "Хурал", + "home.sessions.search.noResults": "{{query}}-д зориулсан сесс олдсонгүй", + "home.sessions.empty": "Одоохондоо энд юу ч алга", + "home.sessions.empty.description": "Эхлэхийн тулд сесс үүсгэ", + "home.sessions.group.today": "Өнөөдөр", + "home.sessions.group.yesterday": "Өчигдөр", + "home.sessions.group.older": "Хуучин", + "home.providerTip": + "Claude, GPT, Gemini гэх мэт бусад загваруудыг ашиглахын тулд 75+ үйлчилгээ үзүүлэгчтэй холбогдоно уу.", + "session.tab.session": "Сесс", + "session.tab.review": "Хяналт", + "session.tab.context": "Контекст", + "session.tab.unknown": "Үл мэдэгдэх сесс", + "session.panel.reviewAndFiles": "Хяналт ба файлууд", + "session.error.notFound": "Энэ сесс олдохгүй байна", + "session.error.notFound.description": "Энэ таб нь энэ сервер дээр байхгүй сессийг заадаг.", + "session.error.notFound.closeTab": "Табыг хаах", + "session.error.serverConnection": "Энэ серверт холбогдох боломжгүй байна", + "session.review.filesChanged": "Файлууд өөрчлөгдсөн {{count}}", + "session.review.change.one": "Өөрчлөх", + "session.review.change.other": "Өөрчлөлтүүд", + "session.review.loadingChanges": "Өөрчлөлтүүдийг ачаалж байна...", + "session.review.empty": "Энэ хуралдаанд хараахан өөрчлөлт ороогүй байна", + "session.review.noVcs": "Git Хувилбарын хяналтын систем илрээгүй, өөрчлөлт харагдахгүй байна", + "session.review.noVcs.createGit.title": "Git репозитор үүсгэх", + "session.review.noVcs.createGit.description": "Энэ төслийн өөрчлөлтийг хянах, хянах, буцаах", + "session.review.noVcs.createGit.actionLoading": "Git репозитор үүсгэж байна...", + "session.review.noVcs.createGit.action": "Git репозитор үүсгэх", + "session.review.noSnapshot": + "Тохиромжтой хэсэгт агшин зуурын зургийг хянахыг идэвхгүй болгосон тул сессийн өөрчлөлтийг хийх боломжгүй", + "session.review.noChanges": "Өөрчлөлт байхгүй", + "session.review.noUncommittedChanges": "Одоохондоо шийдэгдээгүй өөрчлөлт байхгүй байна", + "session.review.noBranchChanges": "Одоогоор салбар өөрчлөлт ороогүй байна", + "session.files.selectToOpen": "Нээх файлаа сонгоно уу", + "session.files.all": "Бүх файлууд", + "session.files.empty": "Файл байхгүй", + "session.files.binaryContent": "Хоёртын файл (агуулга харуулах боломжгүй)", + "session.messages.renderEarlier": "Өмнөх мессежүүдийг үзүүлэх", + "session.messages.loadingEarlier": "Өмнөх мессежүүдийг ачаалж байна...", + "session.messages.loadEarlier": "Өмнөх мессежүүдийг ачаалах", + "session.messages.loading": "Мессежүүдийг ачаалж байна...", + "session.messages.jumpToLatest": "Сүүлийнх рүү шилжих", + "session.context.addToContext": "Контекст {{selection}} нэмнэ үү", + "session.todo.title": "Тодос", + "session.todo.collapse": "Нурах", + "session.todo.expand": "Өргөтгөх", + "session.todo.progress": "{{total}} хийхээс {{done}} нь дууссан", + "session.question.progress": "{{total}} асуултын {{current}}", + "session.question.minimize": "Асуултыг багасгах", + "session.question.restore": "Асуултыг сэргээх", + "session.question.pending.one": "{{count}} хүлээгдэж буй асуулт", + "session.question.pending.other": "{{count}} хүлээгдэж буй асуултууд", + "session.followupDock.summary.one": "{{count}} дараалалд орсон мессеж", + "session.followupDock.summary.other": "{{count}} дараалалд орсон мессежүүд", + "session.followupDock.sendNow": "Яг одоо илгээнэ үү", + "session.followupDock.edit": "Засварлах", + "session.followupDock.collapse": "Дараалалд байгаа мессежүүдийг буулгах", + "session.followupDock.expand": "Дараалсан мессежүүдийг дэлгэх", + "session.revertDock.summary.one": "{{count}} буцаасан мессеж", + "session.revertDock.summary.other": "{{count}} буцаасан зурвасууд", + "session.revertDock.collapse": "Буцаасан мессежүүдийг буулгах", + "session.revertDock.expand": "Буцаасан мессежүүдийг дэлгэх", + "session.revertDock.restore": "Зурвасыг сэргээх", + "session.new.title": "Юу ч барина", + "session.new.project.new": "Шинэ төсөл", + "session.new.project.search": "Төсөл хайх", + "session.new.project.add": "Төсөл нэмэх", + "session.new.worktree.main": "Үндсэн салбар", + "session.new.worktree.mainWithBranch": "Үндсэн салбар ({{branch}})", + "session.new.worktree.create": "Шинэ Git worktree үүсгэх", + "session.new.workspace.runIn": "Сессийг ажиллуулах", + "session.new.workspace.triggerLocal": "Орон нутгийн", + "session.new.workspace.local": "Орон нутгийн хадгалах газар", + "session.new.workspace.existing": "Ажлын талбар…", + "session.new.git.none": "Үгүй Git", + "session.new.lastModified": "Сүүлд өөрчилсөн", + "session.header.search.placeholder": "Хайх {{project}}", + "session.header.searchFiles": "Файл хайх", + "session.header.openIn": "Нээх", + "session.header.open.action": "Нээх {{app}}", + "session.header.open.ariaLabel": "{{app}}-д нээх", + "session.header.open.menu": "Нээлттэй сонголтууд", + "session.header.open.copyPath": "Замыг хуулах", + "session.header.open.finder": "Хайгч", + "session.header.open.fileExplorer": "Файл хөтөч", + "session.header.open.fileManager": "Файлын менежер", + "session.header.reveal.finder": "Finder дээр харуулах", + "session.header.reveal.fileExplorer": "File Explorer-д харуулах", + "session.header.reveal.containingFolder": "Фолдер агуулсан фолдерыг нээх", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Терминал", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Статус", + "status.popover.ariaLabel": "Серверийн тохиргоо", + "status.popover.tab.servers": "Серверүүд", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Нэмэлтүүд", + "status.popover.action.manageServers": "Серверүүдийг удирдах", + "session.share.popover.title": "Вэб дээр нийтлэх", + "session.share.popover.description.shared": + "Энэ сесс вэб дээр олон нийтэд нээлттэй байна. Энэ нь линктэй хүн бүрт хандах боломжтой.", + "session.share.popover.description.unshared": + "Веб дээр сессийг олон нийтэд хуваалцаарай. Энэ нь холбоостой хүн бүрт хандах боломжтой.", + "session.share.action.share": "Хуваалцах", + "session.share.action.publish": "Нийтлэх", + "session.share.action.publishing": "Нийтэлж байна...", + "session.share.action.unpublish": "Нийтлэлийг цуцлах", + "session.share.action.unpublishing": "Нийтлэлийг цуцалж байна...", + "session.share.action.view": "Харах", + "session.share.copy.copied": "Хуулсан", + "session.share.copy.copyLink": "Холбоосыг хуулах", + "lsp.tooltip.none": "LSP сервер байхгүй", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Промптыг ачаалж байна...", + "terminal.loading": "Терминал ачаалж байна...", + "terminal.title": "Терминал", + "terminal.title.numbered": "Терминал {{number}}", + "terminal.close": "Терминалыг хаах", + "terminal.connectionLost.title": "Холболт тасарсан", + "terminal.connectionLost.abnormalClose": "WebSocket хэвийн бус хаагдсан: {{code}}", + "terminal.connectionLost.description": + "Терминалын холболт тасалдсан. Энэ нь серверийг дахин эхлүүлэх үед тохиолдож болно.", + "terminal.connectTicket.csrfError": + "PTY холболтын тасалбарыг гарал үүсэл эсвэл CSRF шалгалтаар татгалзсан. Серверийн CORS тохиргоог шалгана уу.", + "terminal.connectTicket.statusError": "{{status}}-тэй PTY холбогдох тасалбар амжилтгүй боллоо", + "titlebar.update": "Шинэчлэх", + "titlebar.updateVersion": "Шинэчлэх {{version}}", + "common.closeTab": "Табыг хаах", + "common.dismiss": "Хаах", + "common.moreCountSuffix": "(+{{count}} илүү)", + "common.requestFailed": "Хүсэлт амжилтгүй боллоо", + "common.moreOptions": "Илүү олон сонголт", + "common.learnMore": "Илүү ихийг мэдэж аваарай", + "common.rename": "Нэрээ өөрчлөх", + "common.export": "Экспорт", + "common.reset": "Дахин тохируулах", + "common.archive": "Архив", + "common.delete": "Устгах", + "common.close": "Хаах", + "common.edit": "Засварлах", + "common.loadMore": "Илүү их ачаална уу", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "үл мэдэгдэх", + "common.time.justNow": "Яг одоо", + "common.time.minutesAgo.short": "{{count}}м өмнө", + "common.time.hoursAgo.short": "{{count}}цагийн өмнө", + "common.time.daysAgo.short": "{{count}} өдрийн өмнө", + "sidebar.menu.toggle": "Цэсийг сэлгэх", + "sidebar.nav.projectsAndSessions": "Төсөл ба хуралдаанууд", + "sidebar.settings": "Тохиргоо", + "sidebar.help": "Туслаач", + "sidebar.workspaces.enable": "Ажлын талбаруудыг идэвхжүүлэх", + "sidebar.workspaces.disable": "Ажлын талбаруудыг идэвхгүй болгох", + "sidebar.gettingStarted.title": "Эхлэх", + "sidebar.gettingStarted.line1": "OpenCode үнэ төлбөргүй загваруудыг багтаасан тул та шууд эхлэх боломжтой.", + "sidebar.gettingStarted.line2": + "Models, inc ашиглахын тулд дурын үйлчилгээ үзүүлэгчийг холбоно уу. Claude, GPT, Gemini гэх мэт.", + "sidebar.project.recentSessions": "Сүүлийн хуралдаанууд", + "sidebar.project.viewAllSessions": "Бүх сешнүүдийг үзэх", + "sidebar.project.clearNotifications": "Мэдэгдлийг арилгах", + "sidebar.empty.title": "Нээлттэй төсөл байхгүй", + "sidebar.empty.description": "Эхлэхийн тулд төсөл нээнэ үү", + "debugBar.ariaLabel": "Хөгжлийн гүйцэтгэлийн оношлогоо", + "debugBar.na": "үгүй", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Чиглүүлэгчийн эхлэлээс эхлээд тогтсоны дараа анхны будалт хүртэл хэмжсэн хамгийн сүүлд дууссан маршрутын шилжилтийг сессийн хуудсанд хүрнэ.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Сүүлийн 5 секундэд секундэд фрэйм ​​эргэлдэж байна.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Сүүлийн 5 секундын хамгийн муу кадрын хугацаа.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Сүүлийн 5 секундын дотор 32 мс-ээс дээш фрэйм.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Сүүлийн 5 секундэд блоклогдсон цаг болон урт ажлын тоо. Хамгийн их даалгавар: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Сүүлийн 5 секундын хамгийн муу ажиглагдсан оролтын саатал.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Сүүлийн 5 секундын харилцан үйлчлэлийн ойролцоогоор үргэлжлэх хугацаа. Энэ нь албан ёсны Web Vitals INP биш INP төстэй юм.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Одоогийн програмын ашиглалтын хугацаанд хуримтлагдсан бүдүүвчийн шилжилт.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Ашигласан JS нуруулдан, нуруулдан хязгаар. Зөвхөн Chromium.", + "debugBar.mem.tip": "Ашигласан JS нуруулдан, нуруулдан хязгаар. {{limit}}-ийн {{used}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Бүх интерактив элементүүд дээр анхаарлаа төвлөрүүлэх хэв маягийг хүчлэх", + "debugBar.focus.on": "АСААЛТТАЙ", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Сонгосон хэлийг өөрчлөхгүйгээр програмын бүдүүвчийг бүхэлд нь чиглүүлэх", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Ширээний компьютер", + "settings.section.server": "Сервер", + "settings.tab.general": "Генерал", + "settings.tab.shortcuts": "Товчлолууд", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL нэгтгэх", + "settings.desktop.wsl.description": "Windows дээр WSL доторх OpenCode серверийг ажиллуулна уу.", + "settings.general.section.appearance": "Гадаад төрх", + "settings.general.section.advanced": "Дэвшилтэт", + "settings.general.section.notifications": "Системийн мэдэгдэл", + "settings.general.section.updates": "Шинэчлэлтүүд", + "settings.general.section.sounds": "Дууны эффект", + "settings.general.section.feed": "Тэжээл", + "settings.general.section.display": "Дэлгэц", + "settings.general.row.language.title": "Хэл", + "settings.general.row.language.description": "OpenCode дэлгэцийн хэлийг өөрчлөх", + "settings.general.row.shell.title": "Терминал shell", + "settings.general.row.shell.description": "Shell нь терминал болон агентын хэрэгслүүдэд ашиглагддаг", + "settings.general.row.shell.autoDefault": "Автомат (Өгөгдмөл)", + "settings.general.row.shell.terminalOnly": "зөвхөн терминал", + "settings.general.row.appearance.title": "Гадаад төрх", + "settings.general.row.appearance.description": "OpenCode төхөөрөмж дээрээ хэрхэн харагдахыг тохируулна уу", + "settings.general.row.colorScheme.title": "Өнгөний схем", + "settings.general.row.colorScheme.description": + "OpenCode нь систем, цайвар эсвэл бараан загварт тохирох эсэхийг сонгоно уу", + "settings.general.row.theme.title": "Сэдэв", + "settings.general.row.theme.description": "OpenCode-г хэрхэн загварчлахыг тохируулна уу.", + "settings.general.row.font.title": "Кодын фонт", + "settings.general.row.font.description": "Кодын блокуудад ашигладаг фонтыг тохируулна уу", + "settings.general.row.terminalFont.title": "Терминал фонт", + "settings.general.row.terminalFont.description": "Терминалд ашигласан фонтыг тохируулна уу", + "settings.general.row.uiFont.title": "UI фонт", + "settings.general.row.uiFont.description": "Интерфэйс даяар ашигласан фонтыг тохируулна уу", + "settings.general.row.followup.title": "Дагаж мөрдөх зан үйл", + "settings.general.row.followup.description": + "Дагах сануулгууд шууд чиглүүлэх үү эсвэл дараалалд хүлээх үү гэдгийг сонгоно уу", + "settings.general.row.followup.option.queue": "Дараалал", + "settings.general.row.followup.option.steer": "Удирдах", + "settings.general.row.showFileTree.title": "Файлын мод", + "settings.general.row.showFileTree.description": "Файлын модны самбарыг сессээр харуул", + "settings.general.row.showNavigation.title": "Навигацийн удирдлага", + "settings.general.row.showNavigation.description": + "Ширээний гарчгийн мөрөнд буцах болон урагшлах товчлууруудыг харуул", + "settings.general.row.showSearch.title": "Командын палитр", + "settings.general.row.showSearch.description": "Гарчгийн мөрөнд хайлт болон командын палитр товчийг харуул", + "settings.general.row.showTerminal.title": "Терминал", + "settings.general.row.showTerminal.description": "Ширээний гарчгийн мөрөнд терминалын товчлуурыг харуул", + "settings.general.row.showStatus.title": "Серверийн төлөв", + "settings.general.row.showStatus.description": "Гарчгийн мөрөнд серверийн статусын товчлуурыг харуул", + "settings.general.row.mobileTitlebarBottom.title": "Доод навигаци", + "settings.general.row.mobileTitlebarBottom.description": + "Гар утасны дэлгэцийн доод хэсэгт гарчгийн мөр болон сешн табуудыг байрлуул", + "settings.general.row.showCustomAgents.title": "Агентийг харуулах", + "settings.general.row.showCustomAgents.description": + "Хөгжмийн зохиолч дахь агентуудын хооронд шилжих. Нуусан үед өгөгдмөл нь Build agent.", + "settings.general.row.reasoningSummaries.title": "Шалтгаануудын хураангуйг харуул", + "settings.general.row.reasoningSummaries.description": "Загварын үндэслэлийн хураангуйг цагийн хуваарьт харуул", + "settings.general.row.shellToolPartsExpanded.title": "shell хэрэгслийн хэсгүүдийг өргөжүүлэх", + "settings.general.row.shellToolPartsExpanded.description": + "Он цагийн хэлхээс дээр анхдагчаар өргөтгөсөн shell хэрэгслийн хэсгүүдийг харуул", + "settings.general.row.editToolPartsExpanded.title": "Засварлах хэрэгслийн хэсгүүдийг өргөжүүлэх", + "settings.general.row.editToolPartsExpanded.description": + "Он цагийн хэлхээс дээр анхдагчаар өргөтгөсөн засварлах, бичих, засварлах хэрэгслийн хэсгүүдийг харуул", + "settings.general.row.newInterface.title": "Шинэ зохион байгуулалт", + "settings.general.row.newInterface.badge": "Шинэ", + "settings.general.row.newInterface.description": + "Шинэ таб болон гэрийн байршлыг ашиглана уу. Хязгаарлагдмал хугацаанд бүдүүвч хооронд шилжих.", + "settings.general.row.newInterfaceNotice.title": "Та одоо шинэ бүдүүвч ашиглаж байна", + "settings.general.row.newInterfaceNotice.description": "Өмнөх загвар байхгүй болсон", + "settings.general.row.newInterfaceNotice.dismiss": "Хаах", + "settings.general.row.pinchZoom.title": "Томруулахын тулд чимхээрэй", + "settings.general.row.pinchZoom.description": + "Trackpad чимхэх, Ctrl-г гүйлгэх дохиог ашиглан томруулахыг зөвшөөрнө үү", + "settings.general.row.wayland.title": "Эх хэлийг ашиглах Wayland", + "settings.general.row.wayland.description": "Wayland дээрх X11 нөөцийг идэвхгүй болгох. Дахин эхлүүлэх шаардлагатай.", + "settings.general.row.wayland.tooltip": + "Холимог сэргээх хурдтай монитортой Linux дээр эх Wayland илүү тогтвортой байж болно.", + "settings.general.row.releaseNotes.title": "Гаргасан тэмдэглэл", + "settings.general.row.releaseNotes.description": 'Шинэчлэгдсэний дараа "Ямар шинэ зүйл" цонхыг харуул', + "settings.updates.row.startup.title": "Эхлэх үед шинэчлэлт байгаа эсэхийг шалгана уу", + "settings.updates.row.startup.description": "OpenCode эхлүүлэх үед шинэчлэлтүүдийг автоматаар шалгана уу", + "settings.updates.row.check.title": "Шинэчлэлт байгаа эсэхийг шалгана уу", + "settings.updates.row.check.description": "Шинэчлэлтүүдийг гараар шалгаж, боломжтой бол суулгана уу", + "settings.updates.action.checkNow": "Одоо шалгана уу", + "settings.updates.action.checking": "Шалгаж байна...", + "settings.updates.action.downloading": "Татаж авч байна...", + "settings.updates.action.installing": "Суулгаж байна...", + "settings.updates.toast.latest.title": "Та шинэчлэгдсэн байна", + "settings.updates.toast.latest.description": "Та OpenCode-н хамгийн сүүлийн хувилбарыг ажиллуулж байна.", + "sound.option.none": "Байхгүй", + "sound.option.alert01": "Анхааруулга 01", + "sound.option.alert02": "Анхааруулга 02", + "sound.option.alert03": "Анхааруулга 03", + "sound.option.alert04": "Анхааруулга 04", + "sound.option.alert05": "Анхааруулга 05", + "sound.option.alert06": "Анхааруулга 06", + "sound.option.alert07": "Анхааруулга 07", + "sound.option.alert08": "Анхааруулга 08", + "sound.option.alert09": "Анхааруулга 09", + "sound.option.alert10": "Анхааруулга 10", + "sound.option.bipbop01": "Бип-боп 01", + "sound.option.bipbop02": "Бип-боп 02", + "sound.option.bipbop03": "Бип-боп 03", + "sound.option.bipbop04": "Бип-боп 04", + "sound.option.bipbop05": "Бип-боп 05", + "sound.option.bipbop06": "Бип-боп 06", + "sound.option.bipbop07": "Бип-боп 07", + "sound.option.bipbop08": "Бип-боп 08", + "sound.option.bipbop09": "Бип-боп 09", + "sound.option.bipbop10": "Бип-боп 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Үгүй ээ 01", + "sound.option.nope02": "Үгүй ээ 02", + "sound.option.nope03": "Үгүй ээ 03", + "sound.option.nope04": "Үгүй ээ 04", + "sound.option.nope05": "Үгүй ээ 05", + "sound.option.nope06": "Үгүй ээ 06", + "sound.option.nope07": "Үгүй ээ 07", + "sound.option.nope08": "Үгүй ээ 08", + "sound.option.nope09": "Үгүй ээ 09", + "sound.option.nope10": "Үгүй 10", + "sound.option.nope11": "Үгүй 11", + "sound.option.nope12": "Үгүй 12", + "sound.option.yup01": "Тиймээ 01", + "sound.option.yup02": "Тиймээ 02", + "sound.option.yup03": "Тиймээ 03", + "sound.option.yup04": "Тиймээ 04", + "sound.option.yup05": "Тиймээ 05", + "sound.option.yup06": "Тиймээ 06", + "settings.general.notifications.agent.title": "Агент", + "settings.general.notifications.agent.description": + "Агент бүрэн дууссан эсвэл анхаарал хандуулах шаардлагатай үед системийн мэдэгдлийг харуулах", + "settings.general.notifications.permissions.title": "Зөвшөөрөл", + "settings.general.notifications.permissions.description": "Зөвшөөрөл шаардлагатай үед системийн мэдэгдлийг харуулах", + "settings.general.notifications.errors.title": "Алдаа", + "settings.general.notifications.errors.description": "Алдаа гарсан үед системийн мэдэгдлийг харуулах", + "settings.general.sounds.agent.title": "Агент", + "settings.general.sounds.agent.description": + "Агент бүрэн дууссан эсвэл анхаарал хандуулах шаардлагатай үед дууг тоглуулаарай", + "settings.general.sounds.permissions.title": "Зөвшөөрөл", + "settings.general.sounds.permissions.description": "Зөвшөөрөл шаардлагатай үед дуу тоглуулна", + "settings.general.sounds.errors.title": "Алдаа", + "settings.general.sounds.errors.description": "Алдаа гарсан үед дууг тоглуул", + "settings.shortcuts.title": "Гарын товчлолууд", + "settings.shortcuts.reset.button": "Өгөгдмөл рүү дахин тохируулах", + "settings.shortcuts.reset.toast.title": "Товчлолуудыг дахин тохируулсан", + "settings.shortcuts.reset.toast.description": "Гарын товчлолыг өгөгдмөл рүү дахин тохируулсан.", + "settings.shortcuts.conflict.title": "Товчлолыг аль хэдийн ашиглаж байна", + "settings.shortcuts.conflict.description": "{{keybind}} аль хэдийн {{titles}}-д оноосон байна.", + "settings.shortcuts.unassigned": "Оноогоогүй", + "settings.shortcuts.pressKeys": "Товчлуурыг дар", + "settings.shortcuts.search.placeholder": "Товчлол хайх", + "settings.shortcuts.search.empty": "Товчлол олдсонгүй", + "settings.shortcuts.group.general": "Генерал", + "settings.shortcuts.group.session": "Сесс", + "settings.shortcuts.group.navigation": "Навигац", + "settings.shortcuts.group.modelAndAgent": "Загвар өмсөгч, төлөөлөгч", + "settings.shortcuts.group.terminal": "Терминал", + "settings.shortcuts.group.prompt": "Промпт", + "settings.providers.title": "Үйлчилгээ үзүүлэгчид", + "settings.providers.description": "Үйлчилгээ үзүүлэгчийн тохиргоог энд хийх боломжтой.", + "settings.providers.section.connected": "Холбогдсон үйлчилгээ үзүүлэгчид", + "settings.providers.connected.empty": "Холбогдсон үйлчилгээ үзүүлэгч байхгүй байна", + "settings.providers.connected.environmentDescription": "Таны орчны хувьсагчдаас холбогдсон", + "settings.providers.section.popular": "Алдартай үйлчилгээ үзүүлэгчид", + "settings.providers.custom.description": "OpenAI нийцтэй үйлчилгээ үзүүлэгчийг URL үндсэн дээр нэмнэ үү.", + "settings.providers.tag.environment": "Байгаль орчин", + "settings.providers.tag.config": "Тохиргоо", + "settings.providers.tag.custom": "Захиалгат", + "settings.providers.tag.other": "Бусад", + "settings.models.title": "Загварууд", + "settings.models.description": "Загварын тохиргоог энд хийх боломжтой.", + "settings.agents.title": "Агентууд", + "settings.agents.description": "Агентын тохиргоог энд хийх боломжтой.", + "settings.commands.title": "Тушаалууд", + "settings.commands.description": "Энд командын тохиргоог хийх боломжтой.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP тохиргоог энд тохируулах боломжтой.", + "settings.permissions.title": "Зөвшөөрөл", + "settings.permissions.description": "Анхдагчаар сервер ямар хэрэгслийг ашиглаж болохыг хянах.", + "settings.permissions.section.tools": "Багаж хэрэгсэл", + "settings.permissions.toast.updateFailed.title": "Зөвшөөрлийг шинэчилж чадсангүй", + "settings.permissions.action.allow": "Зөвшөөрөх", + "settings.permissions.action.ask": "Асуу", + "settings.permissions.action.deny": "Үгүйсгэх", + "settings.permissions.tool.read.title": "Унших", + "settings.permissions.tool.read.description": "Файлыг уншиж байна (файлын замд таарч байна)", + "settings.permissions.tool.edit.title": "Засварлах", + "settings.permissions.tool.edit.description": "Засвар, бичих, засвар оруулах зэрэг файлуудыг өөрчлөх", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Глоб загвар ашиглан файлуудыг тааруулах", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Ердийн илэрхийлэл ашиглан файлын агуулгыг хайх", + "settings.permissions.tool.list.title": "Жагсаалт", + "settings.permissions.tool.list.description": "Сан доторх файлуудыг жагсаах", + "settings.permissions.tool.bash.title": "Баш", + "settings.permissions.tool.bash.description": "shell тушаалуудыг ажиллуул", + "settings.permissions.tool.task.title": "Даалгавар", + "settings.permissions.tool.task.description": "Дэд агентуудыг ажиллуул", + "settings.permissions.tool.skill.title": "Ур чадвар", + "settings.permissions.tool.skill.description": "Ур чадварыг нэрээр нь ачаална уу", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Хэлний серверийн асуулга ажиллуулах", + "settings.permissions.tool.todowrite.title": "Бичих", + "settings.permissions.tool.todowrite.description": "Хийх ажлын жагсаалтыг шинэчил", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "URL-аас контент татах", + "settings.permissions.tool.websearch.title": "Вэб хайлт", + "settings.permissions.tool.websearch.description": "Вэбээс хайх", + "settings.permissions.tool.external_directory.title": "Гадаад лавлах", + "settings.permissions.tool.external_directory.description": "Төслийн лавлахаас гадуур файлд хандах", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Ижил оролттой хэрэгслийн давтагдсан дуудлагыг илрүүлэх", + "session.delete.failed.title": "Сешн устгаж чадсангүй", + "session.delete.title": "Сеанс устгах", + "session.delete.confirm": '"{{name}}" сессийг устгах уу?', + "session.delete.button": "Сеанс устгах", + "workspace.new": "Шинэ ажлын талбар", + "workspace.type.local": "орон нутгийн", + "workspace.type.sandbox": "хамгаалагдсан хязгаарлагдмал орчин", + "workspace.create.failed.title": "Ажлын талбар үүсгэж чадсангүй", + "workspace.delete.failed.title": "Ажлын талбарыг устгаж чадсангүй", + "workspace.resetting.title": "Ажлын талбарыг дахин тохируулж байна", + "workspace.resetting.description": "Үүнд нэг минут зарцуулагдаж магадгүй.", + "workspace.reset.failed.title": "Ажлын талбарыг дахин тохируулж чадсангүй", + "workspace.reset.success.title": "Ажлын талбарыг дахин тохируулсан", + "workspace.reset.success.description": "Ажлын талбар нь одоо үндсэн салбартай таарч байна.", + "workspace.error.stillPreparing": "Ажлын байраа бэлдэж байна", + "workspace.status.checking": "Нэгтгээгүй өөрчлөлтүүдийг шалгаж байна...", + "workspace.status.error": "Git статусыг шалгах боломжгүй байна.", + "workspace.status.clean": "Нэгтгэсэн өөрчлөлт илрээгүй.", + "workspace.status.dirty": "Энэ ажлын талбарт нэгтгэгдээгүй өөрчлөлтүүд илэрсэн.", + "workspace.delete.title": "Ажлын талбарыг устгах", + "workspace.delete.confirm": '"{{name}}" ажлын талбарыг устгах уу?', + "workspace.delete.button": "Ажлын талбарыг устгах", + "workspace.reset.title": "Ажлын талбарыг дахин тохируулах", + "workspace.reset.confirm": '"{{name}}" ажлын талбарыг дахин тохируулах уу?', + "workspace.reset.button": "Ажлын талбарыг дахин тохируулах", + "workspace.reset.archived.none": "Идэвхтэй сессийг архивлахгүй.", + "workspace.reset.archived.one": "1 сессийг архивлах болно.", + "workspace.reset.archived.many": "{{count}} сессийг архивлах болно.", + "workspace.reset.note": "Энэ нь ажлын талбарыг анхдагч салбартай тааруулахын тулд дахин тохируулах болно.", + "dialog.usageExceeded.dontShowAgain": "Дахин бүү харуул", +} diff --git a/packages/app/src/i18n/ms.ts b/packages/app/src/i18n/ms.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c3783d43b7d7d2f4ea6c28fef1806c2bbf6d7d2 --- /dev/null +++ b/packages/app/src/i18n/ms.ts @@ -0,0 +1,1157 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Fail", + "desktop.menu.edit": "Sunting", + "desktop.menu.view": "Paparan", + "desktop.menu.go": "Pergi", + "desktop.menu.window": "Tetingkap", + "desktop.menu.help": "Bantuan", + "desktop.menu.checkForUpdates": "Semak Kemas Kini...", + "desktop.menu.settings": "Tetapan", + "desktop.menu.reloadWebview": "Muat Semula Paparan Web", + "desktop.menu.restart": "Mulakan Semula", + "desktop.menu.exportLogs": "Eksport Log...", + "desktop.menu.newSession": "Sesi Baharu", + "desktop.menu.openProject": "Buka Projek...", + "desktop.menu.newWindow": "Tetingkap Baharu", + "desktop.menu.closeWindow": "Tutup Tetingkap", + "desktop.menu.undo": "Nyahbuat", + "desktop.menu.redo": "Buat Semula", + "desktop.menu.cut": "Potong", + "desktop.menu.copy": "Salin", + "desktop.menu.paste": "Tampal", + "desktop.menu.delete": "Padam", + "desktop.menu.selectAll": "Pilih Semua", + "desktop.menu.toggleSidebar": "Togol Bar Sisi", + "desktop.menu.toggleTerminal": "Togol Terminal", + "desktop.menu.toggleFileTree": "Togol Pokok Fail", + "desktop.menu.reload": "Muat Semula", + "desktop.menu.toggleDeveloperTools": "Togol Alat Pembangun", + "desktop.menu.actualSize": "Saiz Sebenar", + "desktop.menu.zoomIn": "Zum Masuk", + "desktop.menu.zoomOut": "Kecilkan Zum", + "desktop.menu.toggleFullScreen": "Togol Skrin Penuh", + "desktop.menu.back": "Kembali", + "desktop.menu.forward": "Ke Hadapan", + "desktop.menu.previousSession": "Sesi Sebelumnya", + "desktop.menu.nextSession": "Sesi Seterusnya", + "desktop.menu.previousProject": "Projek Sebelumnya", + "desktop.menu.nextProject": "Projek Seterusnya", + "desktop.menu.minimize": "Minimumkan", + "desktop.menu.maximize": "Maksimumkan", + "desktop.menu.documentation": "Dokumentasi OpenCode", + "desktop.menu.supportForum": "Forum Sokongan", + "desktop.menu.shareFeedback": "Kongsi Maklum Balas", + "desktop.menu.reportBug": "Laporkan Pepijat", + "desktop.menu.ariaLabel": "Menu OpenCode", + "desktop.updater.dialog.checkFailed.message": "Semakan kemas kini gagal.", + "desktop.updater.dialog.checkFailed.title": "Ralat Kemas Kini", + "desktop.updater.dialog.upToDate.message": "Anda menggunakan versi terkini.", + "desktop.updater.dialog.upToDate.title": "Tiada Kemas Kini", + "desktop.updater.dialog.ready.message": "Kemas kini {{version}} telah dimuat turun. Mulakan semula sekarang?", + "desktop.updater.dialog.ready.title": "Kemas Kini Sedia", + "desktop.updater.dialog.restart": "Mulakan Semula", + "desktop.updater.dialog.later": "Nanti", + "desktop.recovery.action.relaunch": "Lancarkan Semula", + "desktop.recovery.action.exportLogs": "Eksport Log", + "desktop.recovery.action.keepWaiting": "Teruskan Menunggu", + "desktop.recovery.action.quit": "Keluar", + "desktop.recovery.loadFailed": "OpenCode gagal dimuatkan", + "desktop.recovery.terminated": "Tetingkap OpenCode ditamatkan secara tidak dijangka", + "desktop.recovery.unresponsive": "OpenCode tidak memberi respons", + "desktop.recovery.unresponsive.detail": "Anda boleh lancarkan semula aplikasi, buka log, atau terus menunggu.", + "desktop.recovery.loadFailed.detail": "Tetingkap: {{window}}\nURL: {{url}}\nRalat: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Tetingkap: {{window}}\nSebab: {{reason}}\nKod: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Pilih folder", + "desktop.dialog.chooseFile": "Pilih fail", + "desktop.dialog.saveFile": "Simpan fail", + "desktop.dialog.files": "Fail", + "desktop.server.local": "Pelayan Tempatan", + "desktop.wsl.error.windowsOnly": "WSL hanya tersedia di Windows", + "desktop.wsl.error.unavailable": "WSL tidak tersedia", + "desktop.wsl.error.listInstalled": "Gagal menyenaraikan distro WSL yang dipasang", + "desktop.wsl.error.listOnline": "Gagal menyenaraikan distro WSL dalam talian", + "desktop.wsl.error.executeDistro": "Tidak dapat melaksanakan arahan dalam distro", + "desktop.wsl.error.installWsl": "Pemasangan WSL gagal", + "desktop.wsl.error.installDistro": "Gagal memasang distro: {{distro}}", + "desktop.wsl.error.installOpencode": "Pemasangan OpenCode gagal", + "desktop.wsl.error.alreadyAdded": "{{distro}} telah pun ditambah", + "desktop.wsl.error.opencodeMissing": "opencode tidak dipasang dalam distro ini", + "desktop.wsl.error.opencodeCannotRun": "opencode dipasang tetapi tidak dapat dijalankan", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode tidak dipasang dalam {{distro}}", + "desktop.wsl.error.updateVersion": + "Kemas kini OpenCode selesai tetapi {{distro}} masih melaporkan {{installed}}; sepatutnya {{expected}}", + "desktop.wsl.error.noVersion": "tiada versi", + "desktop.wsl.error.serverExited": "Pelayan WSL telah keluar selepas permulaan (kod={{code}} isyarat={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Pelayan WSL keluar sebelum menjadi sihat (kod={{code}} isyarat={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Semakan kesihatan Sidecar untuk {{distro}} tamat masa selepas {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} tamat masa selepas {{timeout}}ms", + "desktop.wsl.error.failedPort": "Gagal mendapatkan port", + "desktop.picker.error.notSelected": "Fail tidak dipilih oleh pemilih", + "desktop.picker.error.sizeLimit": "Lampiran yang dipilih melebihi had {{limit}} MB", + "command.category.suggested": "Dicadangkan", + "command.category.view": "Paparan", + "command.category.project": "Projek", + "command.category.provider": "Penyedia", + "command.category.server": "Pelayan", + "command.category.session": "Sesi", + "command.category.theme": "Tema", + "command.category.language": "Bahasa", + "command.category.file": "Fail", + "command.category.context": "Konteks", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Ejen", + "command.category.permissions": "Kebenaran", + "command.category.workspace": "Ruang kerja", + "command.category.settings": "Tetapan", + "command.logs.export": "Eksport log", + "theme.scheme.system": "Sistem", + "theme.scheme.light": "Cerah", + "theme.scheme.dark": "Gelap", + "command.sidebar.toggle": "Togol bar sisi", + "command.project.open": "Buka projek", + "command.project.previous": "Projek sebelum", + "command.project.next": "Projek seterusnya", + "command.project.index": "Tukar ke projek {{index}}", + "command.provider.connect": "Sambung penyedia", + "command.server.switch": "Tukar pelayan", + "command.settings.open": "Buka tetapan", + "command.session.previous": "Sesi sebelum", + "command.session.next": "Sesi seterusnya", + "command.session.previous.unseen": "Sesi belum dibaca sebelumnya", + "command.session.next.unseen": "Sesi belum dibaca seterusnya", + "command.session.archive": "Arkibkan sesi", + "command.palette": "Palet arahan", + "command.theme.cycle": "Tukar tema", + "command.theme.set": "Guna tema: {{theme}}", + "command.theme.scheme.cycle": "Tukar skema warna", + "command.theme.scheme.set": "Guna skema warna: {{scheme}}", + "command.language.cycle": "Tukar bahasa", + "command.language.set": "Guna bahasa: {{language}}", + "command.session.new": "Sesi baharu", + "command.file.open": "Buka fail", + "command.tab.close": "Tutup tab", + "command.tab.reopenClosed": "Buka semula tab tertutup", + "command.context.addSelection": "Tambah pilihan ke konteks", + "command.context.addSelection.description": "Tambah baris terpilih dari fail semasa", + "command.input.focus": "Fokus pada input", + "command.terminal.toggle": "Togol terminal", + "command.fileTree.toggle": "Togol pokok fail", + "command.review.toggle": "Togol semakan", + "command.terminal.new": "Terminal baharu", + "command.terminal.new.description": "Cipta tab terminal baharu", + "command.steps.toggle": "Togol langkah", + "command.steps.toggle.description": "Papar atau sembunyi langkah untuk mesej semasa", + "command.message.previous": "Mesej sebelumnya", + "command.message.previous.description": "Pergi ke mesej pengguna sebelumnya", + "command.message.next": "Mesej seterusnya", + "command.message.next.description": "Pergi ke mesej pengguna seterusnya", + "command.model.choose": "Pilih model", + "command.model.choose.description": "Pilih model lain", + "command.mcp.toggle": "Togol MCP", + "command.mcp.toggle.description": "Togol MCP", + "command.agent.cycle": "Pusing ejen", + "command.agent.cycle.description": "Tukar ke ejen seterusnya", + "command.agent.cycle.reverse": "Pusing ejen ke belakang", + "command.agent.cycle.reverse.description": "Tukar ke ejen sebelumnya", + "command.model.variant.cycle": "Pusing tahap usaha", + "command.model.variant.cycle.description": "Tukar ke tahap usaha seterusnya", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Arahan", + "command.permissions.autoaccept.enable": "Terima kebenaran secara automatik", + "command.permissions.autoaccept.disable": "Henti terima kebenaran automatik", + "command.workspace.toggle": "Togol ruang kerja", + "command.workspace.toggle.description": "Aktif atau nyahaktifkan pelbagai ruang kerja di bar sisi", + "command.session.undo": "Undur", + "command.session.undo.description": "Batalkan mesej terakhir", + "command.session.redo": "Buat semula", + "command.session.redo.description": "Buat semula mesej yang telah diundur", + "command.session.compact": "Padatkan sesi", + "command.session.compact.description": "Ringkaskan sesi untuk mengurangkan saiz konteks", + "command.session.fork": "Cabang dari mesej", + "command.session.fork.description": "Cipta sesi baharu daripada mesej terdahulu", + "command.session.share": "Kongsi sesi", + "command.session.share.description": "Kongsi sesi ini dan salin URL ke papan klip", + "command.session.unshare": "Henti kongsi sesi", + "command.session.unshare.description": "Berhenti berkongsi sesi ini", + "command.session.export": "Eksport sesi", + "command.session.export.description": "Eksport transkrip penuh sesi sebagai JSON", + "palette.search.placeholder": "Cari fail, arahan, dan sesi", + "palette.search.placeholder.home": "Cari arahan dan sesi", + "palette.empty": "Tiada hasil ditemui", + "palette.group.commands": "Arahan", + "palette.group.files": "Fail", + "dialog.provider.search.placeholder": "Cari penyedia", + "dialog.provider.empty": "Tiada penyedia ditemui", + "dialog.provider.group.popular": "Popular", + "dialog.provider.group.other": "Lain-lain", + "dialog.provider.custom.label": "Penyedia serasi OpenAI tersuai", + "dialog.provider.tag.recommended": "Disyorkan", + "dialog.provider.opencode.note": "Model terpilih termasuk Claude, GPT, Gemini dan banyak lagi", + "dialog.provider.opencode.tagline": "Model dioptimumkan yang boleh dipercayai", + "dialog.provider.opencodeGo.tagline": "Langganan kos rendah untuk semua", + "dialog.provider.anthropic.note": "Akses terus ke model Claude, termasuk Pro dan Max", + "dialog.provider.copilot.note": "Model AI untuk bantuan pengekodan melalui GitHub Copilot", + "dialog.provider.openai.note": "Model GPT untuk tugasan AI umum yang pantas dan berkeupayaan", + "dialog.provider.google.note": "Model Gemini untuk respons pantas dan berstruktur", + "dialog.provider.openrouter.note": "Akses semua model yang disokong daripada satu penyedia", + "dialog.provider.vercel.note": "Akses bersatu ke model AI dengan penghalaan pintar", + "dialog.model.select.title": "Pilih model", + "dialog.model.search.placeholder": "Cari model", + "dialog.model.empty": "Tiada hasil model", + "dialog.model.manage": "Urus model", + "dialog.model.manage.description": "Sesuaikan model yang dipaparkan dalam pemilih model.", + "dialog.model.manage.provider.toggle": "Togol semua model {{provider}}", + "dialog.model.unpaid.freeModels.title": "Model percuma yang disediakan oleh OpenCode", + "dialog.model.unpaid.addMore.title": "Tambah lebih banyak model daripada penyedia popular", + "dialog.model.unpaid.viewMoreProviders": "Lihat 70+ lagi penyedia", + "dialog.provider.viewAll": "Tunjukkan lebih banyak penyedia", + "provider.connect.title": "Sambungkan {{provider}}", + "provider.connect.title.anthropicProMax": "Log masuk dengan Claude Pro/Max", + "provider.connect.selectMethod": "Pilih kaedah log masuk untuk {{provider}}.", + "provider.connect.method.apiKey": "Kunci API", + "provider.connect.method.browser": "Pelayar", + "provider.connect.method.headless": "Tanpa antara muka", + "provider.connect.status.inProgress": "Kebenaran sedang diproses...", + "provider.connect.status.waiting": "Menunggu kebenaran...", + "provider.connect.status.failed": "Kebenaran gagal: {{error}}", + "provider.connect.apiKey.description": + "Masukkan kunci API {{provider}} anda untuk menghubungkan akaun anda dan menggunakan model {{provider}} dalam OpenCode.", + "provider.connect.apiKey.label": "Kunci API {{provider}}", + "provider.connect.apiKey.placeholder": "Kunci API", + "provider.connect.apiKey.required": "Kunci API diperlukan", + "provider.connect.opencodeZen.line1": + "OpenCode Zen memberi anda akses kepada set model terpilih yang boleh dipercayai dan dioptimumkan untuk ejen pengekodan.", + "provider.connect.opencodeZen.line2": + "Dengan satu kunci API, anda akan mendapat akses kepada model seperti Claude, GPT, Gemini, GLM dan banyak lagi.", + "provider.connect.opencodeZen.visit.prefix": "Layari", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "untuk mendapatkan kunci API anda.", + "provider.connect.oauth.code.visit.prefix": "Layari", + "provider.connect.oauth.code.visit.link": "pautan ini", + "provider.connect.oauth.code.visit.suffix": + " untuk mendapatkan kod kebenaran anda bagi menghubungkan akaun dan menggunakan model {{provider}} dalam OpenCode.", + "provider.connect.oauth.code.label": "Kod kebenaran {{method}}", + "provider.connect.oauth.code.placeholder": "Kod kebenaran", + "provider.connect.oauth.code.required": "Kod kebenaran diperlukan", + "provider.connect.oauth.code.invalid": "Kod kebenaran tidak sah", + "provider.connect.oauth.auto.visit.prefix": "Layari", + "provider.connect.oauth.auto.visit.link": "pautan ini", + "provider.connect.oauth.auto.visit.suffix": + " dan masukkan kod di bawah untuk menghubungkan akaun anda dan menggunakan model {{provider}} dalam OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Kod pengesahan", + "provider.connect.toast.connected.title": "{{provider}} telah disambungkan", + "provider.connect.toast.connected.description": "Model {{provider}} kini tersedia untuk digunakan.", + "provider.custom.title": "Penyedia tersuai", + "provider.custom.unavailable": "Penyedia tersuai tidak tersedia pada pelayan ini", + "provider.custom.description.prefix": "Konfigurasikan penyedia serasi OpenAI. Lihat", + "provider.custom.description.link": "dokumen konfigurasi penyedia", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID Penyedia", + "provider.custom.field.providerID.placeholder": "penyediaSaya", + "provider.custom.field.providerID.description": "Huruf kecil, nombor, tanda sempang, atau garis bawah", + "provider.custom.field.name.label": "Nama paparan", + "provider.custom.field.name.placeholder": "Penyedia AI Saya", + "provider.custom.field.baseURL.label": "URL asas", + "provider.custom.field.baseURL.placeholder": "https://api.penyediaSaya.com/v1", + "provider.custom.field.apiKey.label": "Kunci API", + "provider.custom.field.apiKey.placeholder": "Kunci API", + "provider.custom.field.apiKey.description": + "Pilihan. Biarkan kosong jika anda mengurus pengesahan melalui pengepala.", + "provider.custom.models.label": "Model", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "id-model", + "provider.custom.models.name.label": "Nama", + "provider.custom.models.name.placeholder": "Nama Paparan", + "provider.custom.models.remove": "Buang model", + "provider.custom.models.add": "Tambah model", + "provider.custom.headers.label": "Pengepala (pilihan)", + "provider.custom.headers.key.label": "Pengepala", + "provider.custom.headers.key.placeholder": "Nama-Pengepala", + "provider.custom.headers.value.label": "Nilai", + "provider.custom.headers.value.placeholder": "nilai", + "provider.custom.headers.remove": "Buang pengepala", + "provider.custom.headers.add": "Tambah pengepala", + "provider.custom.error.providerID.required": "ID Penyedia diperlukan", + "provider.custom.error.providerID.format": "Gunakan huruf kecil, nombor, tanda sempang, atau garis bawah", + "provider.custom.error.providerID.exists": "ID penyedia itu sudah wujud", + "provider.custom.error.name.required": "Nama paparan diperlukan", + "provider.custom.error.baseURL.required": "URL asas diperlukan", + "provider.custom.error.baseURL.format": "Mesti bermula dengan http:// atau https://", + "provider.custom.error.required": "Diperlukan", + "provider.custom.error.duplicate": "Pendua", + "provider.disconnect.toast.disconnected.title": "{{provider}} telah diputuskan", + "provider.disconnect.toast.disconnected.description": "Model {{provider}} tidak lagi tersedia.", + "model.tag.free": "Percuma", + "model.tag.latest": "Terkini", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "teks", + "model.input.image": "imej", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Membenarkan: {{inputs}}", + "model.tooltip.context.label": "Konteks", + "model.tooltip.inputs": "Input", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Penyedia", + "model.tooltip.reasoning": "Penalaran", + "model.tooltip.reasoning.allowed": "Benarkan penalaran", + "model.tooltip.reasoning.none": "Tiada penalaran", + "model.tooltip.context": "Had konteks {{limit}}", + "common.search.placeholder": "Cari", + "common.goBack": "Navigasi ke belakang", + "common.goForward": "Navigasi ke hadapan", + "common.loading": "Memuatkan", + "common.loading.ellipsis": "...", + "common.cancel": "Batal", + "common.clear": "Kosongkan", + "common.open": "Buka", + "common.connect": "Sambung", + "common.disconnect": "Putuskan sambungan", + "common.continue": "Teruskan", + "common.submit": "Hantar", + "common.save": "Simpan", + "common.saving": "Sedang menyimpan...", + "common.default": "Lalai", + "common.attachment": "lampiran", + "prompt.placeholder.shell": "Masukkan arahan shell... {{example}}", + "prompt.placeholder.normal": 'Tanya apa-apa... "{{example}}"', + "prompt.placeholder.simple": "Tanya apa-apa...", + "prompt.placeholder.summarizeComments": "Ringkaskan komen…", + "prompt.placeholder.summarizeComment": "Ringkaskan komen…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Arahan", + "prompt.mode.shell.exit": "esc untuk keluar", + "session.child.promptDisabled": "Sesi subagen tidak boleh diprompt.", + "session.child.backToParent": "Kembali ke sesi utama.", + "prompt.example.1": "Betulkan TODO dalam pangkalan kod", + "prompt.example.2": "Apakah timbunan teknologi projek ini?", + "prompt.example.3": "Betulkan ujian yang gagal", + "prompt.example.4": "Terangkan cara pengesahan berfungsi", + "prompt.example.5": "Cari dan baiki kelemahan keselamatan", + "prompt.example.6": "Tambah ujian unit untuk perkhidmatan pengguna", + "prompt.example.7": "Refaktor fungsi ini supaya lebih mudah dibaca", + "prompt.example.8": "Apa maksud ralat ini?", + "prompt.example.9": "Bantu saya nyahpepijat isu ini", + "prompt.example.10": "Jana dokumentasi API", + "prompt.example.11": "Optimumkan pertanyaan pangkalan data", + "prompt.example.12": "Tambah pengesahan input", + "prompt.example.13": "Cipta komponen baharu untuk...", + "prompt.example.14": "Bagaimana untuk melaksanakan projek ini?", + "prompt.example.15": "Semak kod saya untuk amalan terbaik", + "prompt.example.16": "Tambah pengendalian ralat pada fungsi ini", + "prompt.example.17": "Terangkan corak regex ini", + "prompt.example.18": "Tukar ini kepada TypeScript", + "prompt.example.19": "Tambah log di seluruh pangkalan kod", + "prompt.example.20": "Apakah kebergantungan yang sudah lapuk?", + "prompt.example.21": "Bantu saya tulis skrip migrasi", + "prompt.example.22": "Laksanakan cache untuk titik akhir ini", + "prompt.example.23": "Tambah penomboran halaman pada senarai ini", + "prompt.example.24": "Cipta arahan CLI untuk...", + "prompt.example.25": "Bagaimana pembolehubah persekitaran berfungsi di sini?", + "prompt.popover.emptyResults": "Tiada hasil yang sepadan", + "prompt.popover.emptyCommands": "Tiada arahan yang sepadan", + "prompt.dropzone.label": "Lepaskan imej, PDF, atau fail teks di sini", + "prompt.dropzone.file.label": "Lepaskan untuk @mention fail", + "prompt.slash.badge.custom": "suai", + "prompt.slash.badge.skill": "kemahiran", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktif", + "prompt.context.includeActiveFile": "Sertakan fail aktif", + "prompt.context.removeActiveFile": "Buang fail aktif daripada konteks", + "prompt.context.removeFile": "Buang fail daripada konteks", + "prompt.action.attachFile": "Tambah fail", + "prompt.menu.addImagesAndFiles": "Tambah fail dan lain-lain", + "prompt.menu.imagesAndFiles": "Imej dan fail", + "prompt.menu.commands": "Arahan", + "prompt.menu.context": "Konteks", + "prompt.menu.shellCommand": "Arahan shell", + "prompt.attachment.remove": "Buang lampiran", + "prompt.action.send": "Hantar", + "prompt.action.stop": "Henti", + "prompt.toast.pasteUnsupported.title": "Lampiran tidak disokong", + "prompt.toast.pasteUnsupported.description": "Hanya imej, PDF, atau fail teks boleh dilampirkan di sini.", + "prompt.toast.attachmentDuplicate.title": "Fail ini telah dimuat naik", + "prompt.toast.modelAgentRequired.title": "Pilih ejen dan model", + "prompt.toast.modelAgentRequired.description": "Pilih ejen dan model sebelum menghantar prompt.", + "prompt.toast.worktreeCreateFailed.title": "Gagal mencipta worktree", + "prompt.toast.sessionCreateFailed.title": "Gagal mencipta sesi", + "prompt.toast.shellSendFailed.title": "Gagal menghantar arahan shell", + "prompt.toast.commandSendFailed.title": "Gagal menghantar arahan", + "prompt.toast.promptSendFailed.title": "Gagal menghantar prompt", + "prompt.toast.promptSendFailed.description": "Tidak dapat mendapatkan sesi", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{enabled}} daripada {{total}} diaktifkan", + "dialog.mcp.empty": "Tiada MCP dikonfigurasi", + "dialog.lsp.empty": "LSP dikesan secara automatik daripada jenis fail", + "dialog.plugins.empty": "Plugin dikonfigurasi dalam opencode.json", + "mcp.status.connected": "bersambung", + "mcp.status.failed": "gagal", + "mcp.status.needs_auth": "perlu pengesahan", + "mcp.status.disabled": "dilumpuhkan", + "mcp.auth.clickToAuthenticate": "Klik untuk pengesahan", + "dialog.fork.empty": "Tiada mesej untuk dicabang", + "dialog.directory.search.placeholder": "Cari folder", + "dialog.directory.empty": "Tiada folder ditemui", + "dialog.directory.action.selectFile": "Pilih fail", + "dialog.directory.action.selectFolder": "Pilih folder", + "dialog.directory.root": "Akar", + "dialog.directory.parent": "Induk", + "dialog.directory.readError": "Tidak dapat membaca folder ini", + "app.server.unreachable": "Tidak dapat menghubungi {{server}}", + "app.server.retrying": "Mencuba semula secara automatik...", + "app.server.otherServers": "Pelayan lain", + "dialog.server.title": "Pelayan", + "dialog.server.description": "Tukar pelayan OpenCode yang disambungkan oleh aplikasi ini.", + "dialog.server.search.placeholder": "Cari pelayan", + "dialog.server.empty": "Tiada pelayan lagi", + "dialog.server.add.title": "Tambah pelayan", + "dialog.server.add.url": "Alamat pelayan", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Tidak dapat menyambung ke pelayan", + "dialog.server.add.checking": "Sedang menyemak...", + "dialog.server.add.button": "Tambah pelayan", + "dialog.server.add.name": "Nama pelayan (pilihan)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Nama pengguna (pilihan)", + "dialog.server.add.usernamePlaceholder": "nama pengguna", + "dialog.server.add.password": "Kata laluan (pilihan)", + "dialog.server.add.passwordPlaceholder": "kata laluan", + "dialog.server.edit.title": "Edit pelayan", + "dialog.server.default.title": "Pelayan lalai", + "dialog.server.default.description": + "Sambung ke pelayan ini semasa pelancaran aplikasi dan bukannya memulakan pelayan setempat. Memerlukan mula semula.", + "dialog.server.default.none": "Tiada pelayan dipilih", + "dialog.server.default.set": "Tetapkan pelayan semasa sebagai lalai", + "dialog.server.default.clear": "Kosongkan", + "dialog.server.action.remove": "Buang pelayan", + "dialog.server.menu.edit": "Sunting", + "dialog.server.menu.default": "Tetapkan sebagai lalai", + "dialog.server.menu.defaultRemove": "Buang lalai", + "dialog.server.menu.delete": "Padam", + "dialog.server.current": "Pelayan Semasa", + "dialog.server.status.default": "Lalai", + "wsl.server.add": "Tambah pelayan WSL", + "wsl.server.addShort": "Tambah WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "Pelayan WSL", + "wsl.server.retryStart": "Cuba mula semula", + "wsl.server.updating": "Mengemas kini...", + "wsl.onboarding.step.distro": "Pilih distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Memeriksa WSL...", + "wsl.onboarding.restartRequired": "Windows perlu dimulakan semula untuk melengkapkan pemasangan WSL.", + "wsl.onboarding.ready": "WSL sedia digunakan.", + "wsl.onboarding.required": "WSL diperlukan untuk meneruskan.", + "wsl.onboarding.checkingDistros": "Memeriksa distro...", + "wsl.onboarding.installingDistro": "Memasang {{distro}}...", + "wsl.onboarding.checkingDistro": "Memeriksa {{distro}}...", + "wsl.onboarding.listingDistros": "Menyenaraikan distro...", + "wsl.onboarding.distroReady": "{{distro}} sedia digunakan.", + "wsl.onboarding.distroNotInstalled": "{{distro}} belum dipasang.", + "wsl.onboarding.openDistroOnce": "Buka {{distro}} sekali untuk melengkapkan tetapan.", + "wsl.onboarding.finishingDistro": "Sedang melengkapkan tetapan untuk {{distro}}.", + "wsl.onboarding.pickDistro": "Pilih distro atau pasang satu di bawah.", + "wsl.onboarding.checkingOpencode": "Sedang menyemak OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Sedang menyemak OpenCode dalam {{distro}}...", + "wsl.onboarding.updatingOpencode": "Sedang mengemas kini OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Sedang mengemas kini OpenCode dalam {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Kemas kini OpenCode dalam {{distro}}.", + "wsl.onboarding.updateOpencode": "Kemas kini OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode sedia digunakan dalam {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode sedia digunakan.", + "wsl.onboarding.installOpencodeIn": "Pasang OpenCode dalam {{distro}}.", + "wsl.onboarding.installOpencode": "Pasang OpenCode", + "wsl.onboarding.chooseDistroFirst": "Pilih distro terlebih dahulu.", + "wsl.onboarding.loadFailed": "Gagal memuatkan status WSL.", + "wsl.onboarding.loading": "Memuatkan...", + "wsl.onboarding.installedDistros": "Distro yang telah dipasang", + "wsl.onboarding.checkAgain": "Semak semula", + "wsl.onboarding.distroStatus.ready": "Sedia", + "wsl.onboarding.distroStatus.checking": "Sedang menyemak...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode belum dipasang", + "wsl.onboarding.distroStatus.missingTools": "bash, curl tiada", + "wsl.onboarding.distroStatus.unsupported": "Tidak disokong · Guna WSL 2", + "wsl.onboarding.needAnotherDistro": "Perlukan distro lain?", + "wsl.onboarding.needAnotherDistroHint": "Pasang distribusi Linux daripada katalog WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL belum dipasang", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) diperlukan sebelum OpenCode boleh menambah pelayan WSL", + "wsl.onboarding.wslUnavailable.title": "WSL tidak tersedia", + "wsl.onboarding.wslUnavailable.description": "OpenCode tidak dapat mengesahkan WSL pada mesin ini.", + "wsl.onboarding.installWsl": "Pasang WSL", + "wsl.onboarding.windowsRestartRequired": + "Mulakan semula Windows untuk menyiapkan pemasangan WSL, kemudian buka semula OpenCode.", + "wsl.onboarding.next": "Seterusnya", + "wsl.onboarding.refresh": "Segar semula", + "wsl.onboarding.allDistrosAdded": "Semua distro yang dipasang telah pun ditambah.", + "wsl.onboarding.noDistros": "Tiada distro dikesan lagi.", + "wsl.onboarding.install": "Pasang", + "wsl.onboarding.installing": "Sedang memasang...", + "wsl.onboarding.installDistro": "Pasang distro", + "wsl.onboarding.searchDistros": "Cari distro", + "wsl.onboarding.wsl2Required": "WSL 2 diperlukan.", + "wsl.onboarding.toolsRequired": "Distro ini memerlukan bash dan curl.", + "wsl.onboarding.openTerminal": "Buka terminal", + "wsl.onboarding.path": "Laluan: {{path}}", + "wsl.onboarding.notFound": "tidak ditemui", + "wsl.onboarding.version": "Versi: {{version}}", + "wsl.onboarding.unknown": "tidak diketahui", + "wsl.onboarding.desktopVersion": "desktop {{version}}", + "wsl.onboarding.versionMismatch": "Versi yang dipasang tidak sepadan dengan versi aplikasi desktop.", + "wsl.onboarding.adding": "Sedang menambah...", + "help.tabs.toast.ariaLabel": "Pengenalan Tab. Susun kerja dan sesi aktif anda dengan tab", + "help.tabs.toast.dismiss": "Tutup maklumat Tab", + "help.tabs.title": "Pengenalan Tab", + "help.tabs.description": "Susun kerja dan sesi aktif anda dengan tab", + "help.tabs.date": "14 Julai", + "help.tabs.introduction": "OpenCode Desktop kini dibina berasaskan tab.", + "help.tabs.sessions": + "Mulakan sesi baharu dalam tab, atau buka sesi sedia ada daripada mana-mana projek anda. Buka tab baharu apabila anda memulakan sesuatu yang baru, dan tutup apabila selesai.", + "help.tabs.organize": + "Membuka beberapa tab memudahkan anda menyusun sesi aktif. Namakan semula tab kepada sesuatu yang mudah diingati jika anda ingin menyimpannya.", + "help.tabs.home": + "Anda akan menemui semua sesi dan projek anda di skrin Laman Utama yang baharu. Memilih sesi akan membukanya dalam tab.", + "help.tabs.persistence": "Apabila anda membuka semula aplikasi, tab anda masih terbuka.", + "help.tabs.worktrees": + "Reka bentuk baharu belum menyokong Git Worktrees, ia akan hadir tidak lama lagi. Jika anda ingin terus menggunakan susun atur sebelum ini, anda boleh bertukar antara susun atur dalam Tetapan. Harap maklum bahawa susun atur baharu akan menjadi kekal dalam beberapa minggu lagi.", + "server.row.noUsername": "tiada nama pengguna", + "dialog.project.edit.title": "Edit projek", + "dialog.project.edit.name": "Nama", + "dialog.project.edit.icon": "Ikon", + "dialog.project.edit.icon.alt": "Ikon projek", + "dialog.project.edit.icon.hint": "Klik atau seret imej", + "dialog.project.edit.icon.recommended": "Disyorkan: 128x128px", + "dialog.project.edit.color": "Warna", + "dialog.project.edit.color.select": "Pilih warna {{color}}", + "dialog.project.edit.worktree.startup": "Skrip permulaan ruang kerja", + "dialog.project.edit.worktree.startup.description": "Akan dijalankan selepas mencipta ruang kerja (worktree) baharu.", + "dialog.project.edit.worktree.startup.placeholder": "cth. bun install", + "dialog.releaseNotes.action.getStarted": "Mula", + "dialog.releaseNotes.action.next": "Seterusnya", + "dialog.releaseNotes.action.hideFuture": "Jangan paparkan ini pada masa hadapan", + "dialog.releaseNotes.media.alt": "Pratonton keluaran", + "context.breakdown.title": "Perincian Konteks", + "context.breakdown.note": 'Anggaran perincian token input. "Lain-lain" termasuk definisi alat dan lebihan.', + "context.breakdown.system": "Sistem", + "context.breakdown.user": "Pengguna", + "context.breakdown.assistant": "Pembantu", + "context.breakdown.tool": "Panggilan Alat", + "context.breakdown.other": "Lain-lain", + "context.systemPrompt.title": "Arahan Sistem", + "context.rawMessages.title": "Mesej mentah", + "context.export.session": "Eksport sesi", + "context.stats.session": "Sesi", + "context.stats.messages": "Mesej", + "context.stats.provider": "Penyedia", + "context.stats.model": "Model", + "context.stats.limit": "Had Konteks", + "context.stats.totalTokens": "Jumlah Token", + "context.stats.usage": "Penggunaan", + "context.stats.inputTokens": "Token Input", + "context.stats.outputTokens": "Token Output", + "context.stats.reasoningTokens": "Token Penaakulan", + "context.stats.cacheTokens": "Token Cache (baca/tulis)", + "context.stats.userMessages": "Mesej Pengguna", + "context.stats.assistantMessages": "Mesej Pembantu", + "context.stats.totalCost": "Jumlah Kos", + "context.stats.sessionCreated": "Sesi Dicipta", + "context.stats.lastActivity": "Aktiviti Terakhir", + "context.usage.tokens": "Token", + "context.usage.usage": "Penggunaan", + "context.usage.cost": "Kos", + "context.usage.clickToView": "Klik untuk lihat konteks", + "context.usage.view": "Lihat penggunaan konteks", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Bahasa", + "toast.language.description": "Bertukar kepada {{language}}", + "toast.theme.title": "Tema telah ditukar", + "toast.scheme.title": "Skema warna", + "toast.workspace.enabled.title": "Ruang kerja diaktifkan", + "toast.workspace.enabled.description": "Beberapa worktree kini dipaparkan di bar sisi", + "toast.workspace.disabled.title": "Ruang kerja dinyahaktifkan", + "toast.workspace.disabled.description": "Hanya worktree utama dipaparkan di bar sisi", + "toast.permissions.autoaccept.on.title": "Kebenaran diterima secara automatik", + "toast.permissions.autoaccept.on.description": "Permintaan kebenaran akan diluluskan secara automatik", + "toast.permissions.autoaccept.off.title": "Penerimaan kebenaran automatik dihentikan", + "toast.permissions.autoaccept.off.description": "Permintaan kebenaran memerlukan kelulusan", + "toast.model.none.title": "Tiada model dipilih", + "toast.model.none.description": "Sambungkan penyedia untuk meringkaskan sesi ini", + "toast.file.loadFailed.title": "Gagal memuat fail", + "toast.file.listFailed.title": "Gagal menyenaraikan fail", + "toast.context.noLineSelection.title": "Tiada baris dipilih", + "toast.context.noLineSelection.description": "Pilih julat baris dalam tab fail terlebih dahulu.", + "toast.session.share.copyFailed.title": "Gagal menyalin URL ke papan klip", + "toast.session.share.success.title": "Sesi dikongsi", + "toast.session.share.success.description": "URL perkongsian telah disalin ke papan klip!", + "toast.session.share.failed.title": "Gagal berkongsi sesi", + "toast.session.share.failed.description": "Ralat berlaku semasa berkongsi sesi", + "toast.session.unshare.success.title": "Sesi dibatalkan perkongsian", + "toast.session.unshare.success.description": "Perkongsian sesi berjaya dibatalkan!", + "toast.session.unshare.failed.title": "Gagal menghentikan perkongsian sesi", + "toast.session.unshare.failed.description": "Ralat berlaku semasa berkongsi semula sesi", + "toast.session.export.success.title": "Sesi dieksport", + "toast.session.export.success.description": "Sesi disimpan ke {{filename}}", + "toast.session.export.failed.title": "Gagal mengeksport sesi", + "toast.session.export.failed.description": "Ralat berlaku semasa mengeksport sesi", + "toast.session.listFailed.title": "Gagal memuatkan sesi untuk {{project}}", + "toast.project.reloadFailed.title": "Gagal memuat semula {{project}}", + "toast.update.title": "Kemas kini tersedia", + "toast.update.description": "Versi baharu OpenCode ({{version}}) kini tersedia untuk dipasang.", + "toast.update.action.installRestart": "Pasang dan mulakan semula", + "toast.update.action.notYet": "Belum lagi", + "error.page.title": "Sesuatu telah berlaku", + "error.page.description": "Ralat berlaku semasa memuatkan aplikasi.", + "error.page.description.localServerStartup": "Ralat berlaku semasa memulakan pelayan setempat.", + "error.page.details.label": "Butiran Ralat", + "error.page.action.restart": "Mulakan semula", + "error.page.action.report": "Laporkan Ralat", + "error.page.action.reported": "Ralat Dilaporkan", + "error.page.action.exportLogs": "Eksport Log", + "error.page.action.checking": "Sedang menyemak...", + "error.page.action.checkUpdates": "Semak kemas kini", + "error.page.action.updateTo": "Kemas kini ke {{version}}", + "error.page.circular": "[Bersepusing]", + "error.page.report.prefix": "Sila laporkan ralat ini kepada pasukan OpenCode", + "error.page.report.discord": "di Discord", + "error.page.version": "Versi: {{version}}", + "error.dev.rootNotFound": + "Elemen root tidak ditemui. Adakah anda terlupa menambahnya ke index.html anda? Atau mungkin atribut id telah tersalah eja?", + "error.serverSync.connectFailed": "Tidak dapat menyambung ke pelayan. Adakah pelayan sedang berjalan di `{{url}}`?", + "error.serverSDK.noServerAvailable": "Tiada pelayan tersedia", + "error.serverSDK.serverNotAvailable": "Pelayan tidak tersedia", + "error.childStore.persistedCacheCreateFailed": "Gagal mencipta cache berterusan", + "error.childStore.persistedProjectMetadataCreateFailed": "Gagal mencipta metadata projek berterusan", + "error.childStore.persistedProjectIconCreateFailed": "Gagal mencipta ikon projek berterusan", + "error.childStore.storeCreateFailed": "Gagal mencipta stor", + "directory.error.invalidUrl": "Direktori dalam URL tidak sah.", + "error.chain.unknown": "Ralat tidak diketahui", + "error.server.invalidConfiguration": "Konfigurasi tidak sah", + "error.chain.causedBy": "Disebabkan oleh:", + "error.chain.apiError": "Ralat API", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Boleh dicuba semula: {{retryable}}", + "error.chain.responseBody": "Badan respons:\n{{body}}", + "error.chain.didYouMean": "Adakah anda maksudkan: {{suggestions}}", + "error.chain.modelNotFound": "Model tidak ditemui: {{provider}}/{{model}}", + "error.chain.checkConfig": "Semak konfigurasi anda (opencode.json) nama penyedia/model", + "error.chain.mcpFailed": 'Pelayan MCP "{{name}}" gagal. Nota, OpenCode belum menyokong pengesahan MCP.', + "error.chain.providerAuthFailed": "Pengesahan penyedia gagal ({{provider}}): {{message}}", + "error.chain.providerInitFailed": 'Gagal memulakan penyedia "{{provider}}". Sila semak kelayakan dan konfigurasi.', + "error.chain.configJsonInvalid": "Fail konfigurasi di {{path}} bukan JSON(C) yang sah", + "error.chain.configJsonInvalidWithMessage": "Fail konfigurasi di {{path}} bukan JSON(C) yang sah: {{message}}", + "error.chain.configDirectoryTypo": + 'Direktori "{{dir}}" dalam {{path}} tidak sah. Namakan semula direktori kepada "{{suggestion}}" atau padamkan. Ini adalah kesilapan ejaan yang biasa.', + "error.chain.configFrontmatterError": "Gagal menghurai frontmatter dalam {{path}}:\n{{message}}", + "error.chain.configInvalid": "Fail konfigurasi di {{path}} tidak sah", + "error.chain.configInvalidWithMessage": "Fail konfigurasi di {{path}} tidak sah: {{message}}", + "notification.permission.title": "Kebenaran diperlukan", + "notification.permission.description": "{{sessionTitle}} dalam {{projectName}} memerlukan kebenaran", + "notification.question.title": "Soalan", + "notification.question.description": "{{sessionTitle}} dalam {{projectName}} mempunyai soalan", + "notification.action.goToSession": "Pergi ke sesi", + "notification.session.responseReady.title": "Respons sedia", + "notification.session.error.title": "Ralat sesi", + "notification.session.error.fallbackDescription": "Berlaku ralat", + "home.recentProjects": "Projek terkini", + "home.empty.title": "Tiada projek terkini", + "home.empty.description": "Mulakan dengan membuka projek tempatan", + "home.title": "Laman Utama", + "home.projects": "Projek", + "home.project.add": "Tambah projek", + "home.recentlyClosed": "Baru ditutup", + "home.server.collapse": "Runtuhkan projek pelayan", + "home.server.expand": "Kembangkan projek pelayan", + "home.sessions.search.placeholder": "Cari sesi", + "home.sessions.search.placeholder.scoped": "Cari sesi dalam {{scope}}", + "home.sessions.search.sessions": "Sesi", + "home.sessions.search.noResults": "Tiada sesi dijumpai untuk {{query}}", + "home.sessions.empty": "Belum ada apa-apa di sini", + "home.sessions.empty.description": "Cipta sesi untuk bermula", + "home.sessions.group.today": "Hari ini", + "home.sessions.group.yesterday": "Semalam", + "home.sessions.group.older": "Lama", + "home.providerTip": + "Sambung ke lebih 75 penyedia untuk menggunakan model lain, termasuk Claude, GPT, Gemini dan lain-lain", + "session.tab.session": "Sesi", + "session.tab.review": "Semak", + "session.tab.context": "Konteks", + "session.tab.unknown": "Sesi Tidak Diketahui", + "session.panel.reviewAndFiles": "Semak dan fail", + "session.error.notFound": "Sesi ini tidak dapat ditemui", + "session.error.notFound.description": "Tab ini merujuk kepada sesi yang tidak lagi wujud pada pelayan ini.", + "session.error.notFound.closeTab": "Tutup Tab", + "session.error.serverConnection": "Tidak dapat berhubung dengan pelayan ini", + "session.review.filesChanged": "{{count}} Fail Diubah", + "session.review.change.one": "Perubahan", + "session.review.change.other": "Perubahan", + "session.review.loadingChanges": "Memuat perubahan...", + "session.review.empty": "Belum ada perubahan dalam sesi ini", + "session.review.noVcs": "Tiada Sistem Kawalan Versi Git dikesan, perubahan tidak dipaparkan", + "session.review.noVcs.createGit.title": "Cipta repositori Git", + "session.review.noVcs.createGit.description": "Jejak, semak dan undur perubahan dalam projek ini", + "session.review.noVcs.createGit.actionLoading": "Mencipta repositori Git...", + "session.review.noVcs.createGit.action": "Cipta repositori Git", + "session.review.noSnapshot": "Penjejakan snapshot dilumpuhkan dalam tetapan, jadi perubahan sesi tidak tersedia", + "session.review.noChanges": "Tiada perubahan", + "session.review.noUncommittedChanges": "Belum ada perubahan belum komited", + "session.review.noBranchChanges": "Belum ada perubahan cawangan", + "session.files.selectToOpen": "Pilih fail untuk dibuka", + "session.files.all": "Semua fail", + "session.files.empty": "Tiada fail", + "session.files.binaryContent": "Fail binari (kandungan tidak dapat dipaparkan)", + "session.messages.renderEarlier": "Papar mesej terdahulu", + "session.messages.loadingEarlier": "Memuat mesej terdahulu...", + "session.messages.loadEarlier": "Muat mesej terdahulu", + "session.messages.loading": "Memuat mesej...", + "session.messages.jumpToLatest": "Lompat ke terkini", + "session.context.addToContext": "Tambah {{selection}} ke konteks", + "session.todo.title": "Senarai Tugasan", + "session.todo.collapse": "Runtuhkan", + "session.todo.expand": "Kembangkan", + "session.todo.progress": "{{done}} daripada {{total}} tugasan selesai", + "session.question.progress": "{{current}} daripada {{total}} soalan", + "session.question.minimize": "Minimumkan soalan", + "session.question.restore": "Pulihkan soalan", + "session.question.pending.one": "{{count}} soalan belum dijawab", + "session.question.pending.other": "{{count}} soalan belum dijawab", + "session.followupDock.summary.one": "{{count}} mesej dalam giliran", + "session.followupDock.summary.other": "{{count}} mesej dalam giliran", + "session.followupDock.sendNow": "Hantar sekarang", + "session.followupDock.edit": "Sunting", + "session.followupDock.collapse": "Runtuhkan mesej dalam giliran", + "session.followupDock.expand": "Kembangkan mesej dalam giliran", + "session.revertDock.summary.one": "{{count}} mesej telah dipulangkan semula", + "session.revertDock.summary.other": "{{count}} mesej telah dipulangkan semula", + "session.revertDock.collapse": "Runtuhkan mesej dipulangkan semula", + "session.revertDock.expand": "Kembangkan mesej dipulangkan semula", + "session.revertDock.restore": "Pulihkan mesej", + "session.new.title": "Bina apa sahaja", + "session.new.project.new": "Projek baharu", + "session.new.project.search": "Cari projek", + "session.new.project.add": "Tambah projek", + "session.new.worktree.main": "Cawangan utama", + "session.new.worktree.mainWithBranch": "Cawangan utama ({{branch}})", + "session.new.worktree.create": "Cipta worktree baharu", + "session.new.workspace.runIn": "Jalankan sesi dalam", + "session.new.workspace.triggerLocal": "Setempat", + "session.new.workspace.local": "Repositori setempat", + "session.new.workspace.existing": "Ruang kerja…", + "session.new.git.none": "Tiada Git", + "session.new.lastModified": "Terakhir diubah suai", + "session.header.search.placeholder": "Cari {{project}}", + "session.header.searchFiles": "Cari fail", + "session.header.openIn": "Buka dalam", + "session.header.open.action": "Buka {{app}}", + "session.header.open.ariaLabel": "Buka dalam {{app}}", + "session.header.open.menu": "Pilihan buka", + "session.header.open.copyPath": "Salin laluan", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Penjelajah Fail", + "session.header.open.fileManager": "Pengurus Fail", + "session.header.reveal.finder": "Tunjuk dalam Finder", + "session.header.reveal.fileExplorer": "Tunjuk dalam Penjelajah Fail", + "session.header.reveal.containingFolder": "Buka folder mengandungi", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Konfigurasi pelayan", + "status.popover.tab.servers": "Pelayan", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugin", + "status.popover.action.manageServers": "Urus pelayan", + "session.share.popover.title": "Terbit di web", + "session.share.popover.description.shared": + "Sesi ini adalah awam di web. Ia boleh diakses oleh sesiapa sahaja yang mempunyai pautan.", + "session.share.popover.description.unshared": + "Kongsi sesi secara awam di web. Ia akan boleh diakses oleh sesiapa sahaja yang mempunyai pautan.", + "session.share.action.share": "Kongsi", + "session.share.action.publish": "Terbit", + "session.share.action.publishing": "Menerbitkan...", + "session.share.action.unpublish": "Batalkan terbitan", + "session.share.action.unpublishing": "Membatalkan terbitan...", + "session.share.action.view": "Lihat", + "session.share.copy.copied": "Disalin", + "session.share.copy.copyLink": "Salin pautan", + "lsp.tooltip.none": "Tiada pelayan LSP", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Memuatkan prompt...", + "terminal.loading": "Memuatkan terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Tutup terminal", + "terminal.connectionLost.title": "Sambungan Terputus", + "terminal.connectionLost.abnormalClose": "WebSocket ditutup secara tidak normal: {{code}}", + "terminal.connectionLost.description": + "Sambungan terminal telah terganggu. Ini boleh berlaku apabila pelayan dimulakan semula.", + "terminal.connectTicket.csrfError": + "Tiket sambungan PTY ditolak oleh semakan asal atau CSRF. Sila semak tetapan CORS pelayan.", + "terminal.connectTicket.statusError": "Tiket sambungan PTY gagal dengan {{status}}", + "titlebar.update": "Kemas kini", + "titlebar.updateVersion": "Kemas kini {{version}}", + "common.closeTab": "Tutup tab", + "common.dismiss": "Abaikan", + "common.moreCountSuffix": " (+{{count}} lagi)", + "common.requestFailed": "Permintaan gagal", + "common.moreOptions": "Pilihan lanjut", + "common.learnMore": "Ketahui lebih lanjut", + "common.rename": "Tukar nama", + "common.export": "Eksport", + "common.reset": "Tetapkan semula", + "common.archive": "Arkib", + "common.delete": "Padam", + "common.close": "Tutup", + "common.edit": "Sunting", + "common.loadMore": "Muatkan lagi", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "tidak diketahui", + "common.time.justNow": "Baru sahaja", + "common.time.minutesAgo.short": "{{count}}m lalu", + "common.time.hoursAgo.short": "{{count}}j lalu", + "common.time.daysAgo.short": "{{count}}h lalu", + "sidebar.menu.toggle": "Togol menu", + "sidebar.nav.projectsAndSessions": "Projek dan sesi", + "sidebar.settings": "Tetapan", + "sidebar.help": "Bantuan", + "sidebar.workspaces.enable": "Aktifkan ruang kerja", + "sidebar.workspaces.disable": "Nyahaktifkan ruang kerja", + "sidebar.gettingStarted.title": "Bermula", + "sidebar.gettingStarted.line1": "OpenCode merangkumi model percuma supaya anda boleh bermula serta-merta.", + "sidebar.gettingStarted.line2": + "Sambungkan mana-mana penyedia untuk menggunakan model, termasuk Claude, GPT, Gemini dan lain-lain.", + "sidebar.project.recentSessions": "Sesi terkini", + "sidebar.project.viewAllSessions": "Lihat semua sesi", + "sidebar.project.clearNotifications": "Kosongkan notifikasi", + "sidebar.empty.title": "Tiada projek dibuka", + "sidebar.empty.description": "Buka projek untuk bermula", + "debugBar.ariaLabel": "Diagnostik prestasi pembangunan", + "debugBar.na": "tiada", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Peralihan laluan terakhir yang melibatkan halaman sesi, diukur dari permulaan router hingga cat pertama selepas ia stabil.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Bingkai sesaat bergolek dalam 5 saat terakhir.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Masa bingkai terburuk dalam 5 saat terakhir.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Bingkai melebihi 32ms dalam 5 saat terakhir.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Masa tersekat dan kiraan tugasan panjang dalam 5 saat terakhir. Tugasan maksimum: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Kelewatan input terburuk yang diperhatikan dalam 5 saat terakhir.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Anggaran tempoh interaksi dalam 5 saat terakhir. Ini adalah seperti INP, bukan INP rasmi Web Vitals.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Anjakan tataletak kumulatif sepanjang hayat aplikasi semasa.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Longgokan JS digunakan berbanding had longgokan. Hanya untuk Chromium.", + "debugBar.mem.tip": "Longgokan JS digunakan berbanding had longgokan. {{used}} daripada {{limit}}.", + "debugBar.focus.label": "FOKUS", + "debugBar.focus.tip": "Paksa gaya fokus pada semua elemen interaktif", + "debugBar.focus.on": "AKTIF", + "debugBar.focus.off": "NYAHTAKTIF", + "debugBar.direction.label": "ARAH", + "debugBar.direction.tip": "Paksa arah tataletak aplikasi penuh tanpa menukar bahasa terpilih", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Desktop", + "settings.section.server": "Pelayan", + "settings.tab.general": "Umum", + "settings.tab.shortcuts": "Pintasan", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Integrasi WSL", + "settings.desktop.wsl.description": "Jalankan pelayan OpenCode dalam WSL di Windows.", + "settings.general.section.appearance": "Penampilan", + "settings.general.section.advanced": "Lanjutan", + "settings.general.section.notifications": "Pemberitahuan sistem", + "settings.general.section.updates": "Kemas kini", + "settings.general.section.sounds": "Kesan bunyi", + "settings.general.section.feed": "Suapan", + "settings.general.section.display": "Paparan", + "settings.general.row.language.title": "Bahasa", + "settings.general.row.language.description": "Tukar bahasa paparan untuk OpenCode", + "settings.general.row.shell.title": "Shell terminal", + "settings.general.row.shell.description": "Shell yang digunakan oleh terminal dan alat ejen", + "settings.general.row.shell.autoDefault": "Auto (Lalai)", + "settings.general.row.shell.terminalOnly": "terminal sahaja", + "settings.general.row.appearance.title": "Penampilan", + "settings.general.row.appearance.description": "Sesuaikan rupa OpenCode pada peranti anda", + "settings.general.row.colorScheme.title": "Skema warna", + "settings.general.row.colorScheme.description": "Pilih sama ada OpenCode mengikut sistem, tema cerah, atau gelap", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Sesuaikan tema OpenCode.", + "settings.general.row.font.title": "Fon Kod", + "settings.general.row.font.description": "Sesuaikan fon yang digunakan dalam blok kod", + "settings.general.row.terminalFont.title": "Fon Terminal", + "settings.general.row.terminalFont.description": "Sesuaikan fon yang digunakan dalam terminal", + "settings.general.row.uiFont.title": "Fon Antara Muka", + "settings.general.row.uiFont.description": "Sesuaikan fon yang digunakan di seluruh antara muka", + "settings.general.row.followup.title": "Tingkah laku susulan", + "settings.general.row.followup.description": + "Pilih sama ada arahan susulan dilaksanakan serta-merta atau menunggu dalam barisan", + "settings.general.row.followup.option.queue": "Barisan", + "settings.general.row.followup.option.steer": "Segera", + "settings.general.row.showFileTree.title": "Pokok fail", + "settings.general.row.showFileTree.description": "Papar panel pokok fail dalam sesi", + "settings.general.row.showNavigation.title": "Kawalan navigasi", + "settings.general.row.showNavigation.description": "Papar butang kembali dan ke hadapan pada bar tajuk desktop", + "settings.general.row.showSearch.title": "Palet arahan", + "settings.general.row.showSearch.description": "Papar butang carian dan palet arahan pada bar tajuk", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Papar butang terminal pada bar tajuk desktop", + "settings.general.row.showStatus.title": "Status pelayan", + "settings.general.row.showStatus.description": "Papar butang status pelayan pada bar tajuk", + "settings.general.row.mobileTitlebarBottom.title": "Navigasi bawah", + "settings.general.row.mobileTitlebarBottom.description": + "Letakkan bar tajuk dan tab sesi di bahagian bawah skrin pada peranti mudah alih", + "settings.general.row.showCustomAgents.title": "Papar ejen", + "settings.general.row.showCustomAgents.description": + "Tukar antara ejen dalam penyusun. Jika disembunyikan, lalai kepada ejen Build.", + "settings.general.row.reasoningSummaries.title": "Papar ringkasan penaakulan", + "settings.general.row.reasoningSummaries.description": "Papar ringkasan penaakulan model dalam garis masa", + "settings.general.row.shellToolPartsExpanded.title": "Kembangkan bahagian alat shell", + "settings.general.row.shellToolPartsExpanded.description": + "Papar bahagian alat shell dikembangkan secara lalai dalam garis masa", + "settings.general.row.editToolPartsExpanded.title": "Kembangkan bahagian alat suntingan", + "settings.general.row.editToolPartsExpanded.description": + "Papar bahagian alat sunting, tulis, dan tampal dikembangkan secara lalai dalam garis masa", + "settings.general.row.newInterface.title": "Susun atur baharu", + "settings.general.row.newInterface.badge": "Baharu", + "settings.general.row.newInterface.description": + "Guna tab dan susun atur Laman Utama yang baharu. Tukar antara susun atur untuk masa terhad.", + "settings.general.row.newInterfaceNotice.title": "Anda kini menggunakan susun atur baharu", + "settings.general.row.newInterfaceNotice.description": "Susun atur sebelum ini tidak lagi tersedia", + "settings.general.row.newInterfaceNotice.dismiss": "Tutup", + "settings.general.row.pinchZoom.title": "Cubit untuk zum", + "settings.general.row.pinchZoom.description": "Benarkan gerak isyarat cubit pada pad jejak dan Ctrl-tatal untuk zum", + "settings.general.row.wayland.title": "Guna Wayland asli", + "settings.general.row.wayland.description": "Lumpuhkan sandaran X11 pada Wayland. Memerlukan mula semula.", + "settings.general.row.wayland.tooltip": + "Di Linux dengan monitor kadar segar campuran, Wayland asli boleh jadi lebih stabil.", + "settings.general.row.releaseNotes.title": "Nota keluaran", + "settings.general.row.releaseNotes.description": "Papar tetingkap Apa Yang Baharu selepas kemas kini", + "settings.updates.row.startup.title": "Semak kemas kini semasa permulaan", + "settings.updates.row.startup.description": "Semak kemas kini secara automatik apabila OpenCode dilancarkan", + "settings.updates.row.check.title": "Semak kemas kini", + "settings.updates.row.check.description": "Semak kemas kini secara manual dan pasang jika tersedia", + "settings.updates.action.checkNow": "Semak sekarang", + "settings.updates.action.checking": "Menyemak...", + "settings.updates.action.downloading": "Memuat turun...", + "settings.updates.action.installing": "Sedang memasang...", + "settings.updates.toast.latest.title": "Anda sudah terkini", + "settings.updates.toast.latest.description": "Anda sedang menggunakan versi terkini OpenCode.", + "sound.option.none": "Tiada", + "sound.option.alert01": "Amaran 01", + "sound.option.alert02": "Amaran 02", + "sound.option.alert03": "Amaran 03", + "sound.option.alert04": "Amaran 04", + "sound.option.alert05": "Amaran 05", + "sound.option.alert06": "Amaran 06", + "sound.option.alert07": "Amaran 07", + "sound.option.alert08": "Amaran 08", + "sound.option.alert09": "Amaran 09", + "sound.option.alert10": "Amaran 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "Ejen", + "settings.general.notifications.agent.description": + "Tunjukkan pemberitahuan sistem apabila ejen selesai atau memerlukan perhatian", + "settings.general.notifications.permissions.title": "Kebenaran", + "settings.general.notifications.permissions.description": + "Tunjukkan pemberitahuan sistem apabila kebenaran diperlukan", + "settings.general.notifications.errors.title": "Ralat", + "settings.general.notifications.errors.description": "Tunjukkan pemberitahuan sistem apabila ralat berlaku", + "settings.general.sounds.agent.title": "Ejen", + "settings.general.sounds.agent.description": "Mainkan bunyi apabila ejen selesai atau memerlukan perhatian", + "settings.general.sounds.permissions.title": "Kebenaran", + "settings.general.sounds.permissions.description": "Mainkan bunyi apabila kebenaran diperlukan", + "settings.general.sounds.errors.title": "Ralat", + "settings.general.sounds.errors.description": "Mainkan bunyi apabila ralat berlaku", + "settings.shortcuts.title": "Pintasan papan kekunci", + "settings.shortcuts.reset.button": "Tetapkan semula kepada lalai", + "settings.shortcuts.reset.toast.title": "Pintasan ditetapkan semula", + "settings.shortcuts.reset.toast.description": "Pintasan papan kekunci telah ditetapkan semula kepada lalai.", + "settings.shortcuts.conflict.title": "Pintasan sudah digunakan", + "settings.shortcuts.conflict.description": "{{keybind}} telah pun ditetapkan kepada {{titles}}.", + "settings.shortcuts.unassigned": "Tidak ditetapkan", + "settings.shortcuts.pressKeys": "Tekan kekunci", + "settings.shortcuts.search.placeholder": "Cari pintasan", + "settings.shortcuts.search.empty": "Tiada pintasan dijumpai", + "settings.shortcuts.group.general": "Umum", + "settings.shortcuts.group.session": "Sesi", + "settings.shortcuts.group.navigation": "Navigasi", + "settings.shortcuts.group.modelAndAgent": "Model dan ejen", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Arahan", + "settings.providers.title": "Penyedia", + "settings.providers.description": "Tetapan penyedia boleh dikonfigurasi di sini.", + "settings.providers.section.connected": "Penyedia yang disambungkan", + "settings.providers.connected.empty": "Tiada penyedia yang disambungkan", + "settings.providers.connected.environmentDescription": "Disambungkan daripada pembolehubah persekitaran anda", + "settings.providers.section.popular": "Penyedia popular", + "settings.providers.custom.description": "Tambah penyedia serasi OpenAI melalui URL asas.", + "settings.providers.tag.environment": "Persekitaran", + "settings.providers.tag.config": "Konfigurasi", + "settings.providers.tag.custom": "Tersuai", + "settings.providers.tag.other": "Lain-lain", + "settings.models.title": "Model", + "settings.models.description": "Tetapan model boleh dikonfigurasi di sini.", + "settings.agents.title": "Ejen", + "settings.agents.description": "Tetapan ejen boleh dikonfigurasi di sini.", + "settings.commands.title": "Arahan", + "settings.commands.description": "Tetapan arahan boleh dikonfigurasi di sini.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Tetapan MCP boleh dikonfigurasi di sini.", + "settings.permissions.title": "Kebenaran", + "settings.permissions.description": "Kawal alat yang boleh digunakan oleh pelayan secara lalai.", + "settings.permissions.section.tools": "Alat", + "settings.permissions.toast.updateFailed.title": "Gagal mengemas kini kebenaran", + "settings.permissions.action.allow": "Benarkan", + "settings.permissions.action.ask": "Tanya", + "settings.permissions.action.deny": "Tolak", + "settings.permissions.tool.read.title": "Baca", + "settings.permissions.tool.read.description": "Membaca fail (padanan laluan fail)", + "settings.permissions.tool.edit.title": "Sunting", + "settings.permissions.tool.edit.description": "Ubah fail, termasuk suntingan, penulisan, dan tampalan", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Padankan fail menggunakan corak glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Cari kandungan fail menggunakan ungkapan biasa", + "settings.permissions.tool.list.title": "Senarai", + "settings.permissions.tool.list.description": "Senaraikan fail dalam folder", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Jalankan arahan shell", + "settings.permissions.tool.task.title": "Tugas", + "settings.permissions.tool.task.description": "Lancarkan sub-ejen", + "settings.permissions.tool.skill.title": "Kemahiran", + "settings.permissions.tool.skill.description": "Muatkan kemahiran mengikut nama", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Jalankan pertanyaan pelayan bahasa", + "settings.permissions.tool.todowrite.title": "Tulis Todo", + "settings.permissions.tool.todowrite.description": "Kemas kini senarai tugasan", + "settings.permissions.tool.webfetch.title": "Ambil Web", + "settings.permissions.tool.webfetch.description": "Ambil kandungan dari URL", + "settings.permissions.tool.websearch.title": "Carian Web", + "settings.permissions.tool.websearch.description": "Cari di web", + "settings.permissions.tool.external_directory.title": "Direktori Luaran", + "settings.permissions.tool.external_directory.description": "Akses fail di luar direktori projek", + "settings.permissions.tool.doom_loop.title": "Gelung Doom", + "settings.permissions.tool.doom_loop.description": "Kesan panggilan alat berulang dengan input yang sama", + "session.delete.failed.title": "Gagal memadam sesi", + "session.delete.title": "Padam sesi", + "session.delete.confirm": 'Padam sesi "{{name}}"?', + "session.delete.button": "Padam sesi", + "workspace.new": "Ruang kerja baharu", + "workspace.type.local": "setempat", + "workspace.type.sandbox": "kotak pasir", + "workspace.create.failed.title": "Gagal mencipta ruang kerja", + "workspace.delete.failed.title": "Gagal memadam ruang kerja", + "workspace.resetting.title": "Menetap semula ruang kerja", + "workspace.resetting.description": "Ini mungkin mengambil masa sebentar.", + "workspace.reset.failed.title": "Gagal menetap semula ruang kerja", + "workspace.reset.success.title": "Ruang kerja telah ditetap semula", + "workspace.reset.success.description": "Ruang kerja kini sepadan dengan cawangan lalai.", + "workspace.error.stillPreparing": "Ruang kerja masih sedang disediakan", + "workspace.status.checking": "Memeriksa perubahan belum digabungkan...", + "workspace.status.error": "Tidak dapat mengesahkan status git.", + "workspace.status.clean": "Tiada perubahan belum digabungkan dikesan.", + "workspace.status.dirty": "Perubahan belum digabungkan dikesan dalam ruang kerja ini.", + "workspace.delete.title": "Padam ruang kerja", + "workspace.delete.confirm": 'Padam ruang kerja "{{name}}"?', + "workspace.delete.button": "Padam ruang kerja", + "workspace.reset.title": "Tetap semula ruang kerja", + "workspace.reset.confirm": 'Tetap semula ruang kerja "{{name}}"?', + "workspace.reset.button": "Tetap semula ruang kerja", + "workspace.reset.archived.none": "Tiada sesi aktif akan diarkibkan.", + "workspace.reset.archived.one": "1 sesi akan diarkibkan.", + "workspace.reset.archived.many": "{{count}} sesi akan diarkibkan.", + "workspace.reset.note": "Ini akan menetap semula ruang kerja agar sepadan dengan cawangan lalai.", + "dialog.usageExceeded.dontShowAgain": "Jangan tunjukkan lagi", +} diff --git a/packages/app/src/i18n/my.ts b/packages/app/src/i18n/my.ts new file mode 100644 index 0000000000000000000000000000000000000000..6046f337ff068a930eee4c761b0a11df7fed9d54 --- /dev/null +++ b/packages/app/src/i18n/my.ts @@ -0,0 +1,1176 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ဖိုင်", + "desktop.menu.edit": "တည်းဖြတ်ပါ။", + "desktop.menu.view": "ကြည့်ရန်", + "desktop.menu.go": "သွားပါ။", + "desktop.menu.window": "ဝင်းဒိုး", + "desktop.menu.help": "အကူအညီ", + "desktop.menu.checkForUpdates": "အပ်ဒိတ်များအတွက် စစ်ဆေးပါ...", + "desktop.menu.settings": "ဆက်တင်များ", + "desktop.menu.reloadWebview": "Webview ကို ပြန်လည်စတင်ပါ။", + "desktop.menu.restart": "ပြန်လည်စတင်ပါ။", + "desktop.menu.exportLogs": "မှတ်တမ်းများ ထုတ်ယူရန်...", + "desktop.menu.newSession": "အပိုင်းအသစ်", + "desktop.menu.openProject": "ပရောဂျက်ကို ဖွင့်ပါ...", + "desktop.menu.newWindow": "ဝင်းဒိုးအသစ်", + "desktop.menu.closeWindow": "Window ကိုပိတ်ပါ။", + "desktop.menu.undo": "မလုပ်တော့ပါ။", + "desktop.menu.redo": "ပြန်လုပ်ပါ။", + "desktop.menu.cut": "ဖြတ်ပါ။", + "desktop.menu.copy": "မိတ္တူ", + "desktop.menu.paste": "ကူးထည့်ပါ။", + "desktop.menu.delete": "ဖျက်ပါ", + "desktop.menu.selectAll": "အားလုံးကို ရွေးပါ။", + "desktop.menu.toggleSidebar": "ဘေးဘားကို ပြောင်းရန်", + "desktop.menu.toggleTerminal": "Terminal ပြောင်းရန်", + "desktop.menu.toggleFileTree": "ဖိုင်သစ်ပြောင်းရန်", + "desktop.menu.reload": "ပြန်လည်စတင်ပါ။", + "desktop.menu.toggleDeveloperTools": "ဆော့ဖ်ဝဲရေးသားသူ တူးလ်များကို ပြောင်းရန်", + "desktop.menu.actualSize": "အမှန်တကယ်အရွယ်အစား", + "desktop.menu.zoomIn": "ဇူးမ်ချဲ့ပါ။", + "desktop.menu.zoomOut": "ချုံ့ပါ", + "desktop.menu.toggleFullScreen": "မျက်နှာပြင်အပြည့် ပြောင်းရန်", + "desktop.menu.back": "နောက်ပြန်", + "desktop.menu.forward": "ရှေ့သို့", + "desktop.menu.previousSession": "ယခင်ဆက်ရှင်", + "desktop.menu.nextSession": "နောက်ဆက်ရှင်", + "desktop.menu.previousProject": "ယခင်ပရောဂျက်", + "desktop.menu.nextProject": "နောက်တစ်ခု ပရောဂျက်", + "desktop.menu.minimize": "လျှော့ပါ။", + "desktop.menu.maximize": "အကြီးချဲ့ပါ။", + "desktop.menu.documentation": "OpenCode စာရွက်စာတမ်း", + "desktop.menu.supportForum": "ပံ့ပိုးမှုဖိုရမ်", + "desktop.menu.shareFeedback": "အကြံပြုချက်ကို မျှဝေပါ။", + "desktop.menu.reportBug": "ချွတ်ယွင်းချက်တစ်ခုကို သတင်းပို့ပါ။", + "desktop.menu.ariaLabel": "OpenCode မီနူး", + "desktop.updater.dialog.checkFailed.message": "အပ်ဒိတ်စစ်ဆေးမှု မအောင်မြင်ပါ။", + "desktop.updater.dialog.checkFailed.title": "အပ်ဒိတ် အမှား", + "desktop.updater.dialog.upToDate.message": "သင်သည် ခေတ်မီနေပါသည်။", + "desktop.updater.dialog.upToDate.title": "အပ်ဒိတ်များမရှိပါ။", + "desktop.updater.dialog.ready.message": "{{version}} အပ်ဒိတ်ကို ဒေါင်းလုဒ်လုပ်ထားသည်။ ယခု ပြန်စမလား။", + "desktop.updater.dialog.ready.title": "အပ်ဒိတ်အဆင်သင့်ဖြစ်ပါပြီ။", + "desktop.updater.dialog.restart": "ပြန်လည်စတင်ပါ။", + "desktop.updater.dialog.later": "နောက်ပိုင်း", + "desktop.recovery.action.relaunch": "ပြန်လည်စတင်ပါ။", + "desktop.recovery.action.exportLogs": "မှတ်တမ်းများ ထုတ်ယူရန်", + "desktop.recovery.action.keepWaiting": "ဆက်စောင့်ပါ။", + "desktop.recovery.action.quit": "ထွက်လိုက်ပါ။", + "desktop.recovery.loadFailed": "OpenCode ကို ဖွင့်၍မရပါ။", + "desktop.recovery.terminated": "OpenCode ဝင်းဒိုးသည် မမျှော်လင့်ဘဲ ပိတ်သွားသည်။", + "desktop.recovery.unresponsive": "OpenCode သည် တုံ့ပြန်ခြင်းမရှိပါ။", + "desktop.recovery.unresponsive.detail": + "သင်သည် အက်ပ်ကို ပြန်လည်စတင်နိုင်သည်၊ မှတ်တမ်းများကို ဖွင့်နိုင်သည် သို့မဟုတ် ဆက်လက်စောင့်ဆိုင်းနိုင်သည်။", + "desktop.recovery.loadFailed.detail": "Window- {{window}}\nURL- {{url}}\nအမှား- {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Window- {{window}}\nအကြောင်းရင်း- {{reason}}\nကုဒ်- {{code}}", + "desktop.recovery.unknown": "<အမည်မသိ>", + "desktop.dialog.chooseFolder": "ဖိုင်တွဲတစ်ခုကို ရွေးပါ။", + "desktop.dialog.chooseFile": "ဖိုင်တစ်ခုကို ရွေးပါ။", + "desktop.dialog.saveFile": "ဖိုင်ကို သိမ်းဆည်းပါ။", + "desktop.dialog.files": "ဖိုင်များ", + "desktop.server.local": "ပြည်တွင်းဆာဗာ", + "desktop.wsl.error.windowsOnly": "WSL ကို Windows တွင်သာ ရနိုင်သည်။", + "desktop.wsl.error.unavailable": "WSL ကို မရနိုင်ပါ။", + "desktop.wsl.error.listInstalled": "ထည့်သွင်းထားသော WSL ကွဲပြားမှုများကို စာရင်းပြုစုရန် မအောင်မြင်ပါ။", + "desktop.wsl.error.listOnline": "အွန်လိုင်း WSL ကွဲပြားမှုများကို စာရင်းပြုစုရန် မအောင်မြင်ပါ။", + "desktop.wsl.error.executeDistro": "distro တွင် command များကို လုပ်ဆောင်၍မရပါ။", + "desktop.wsl.error.installWsl": "WSL ထည့်သွင်းခြင်း မအောင်မြင်ပါ။", + "desktop.wsl.error.installDistro": "distro ကို ထည့်သွင်းရန် မအောင်မြင်ပါ- {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode ထည့်သွင်းခြင်း မအောင်မြင်ပါ။", + "desktop.wsl.error.alreadyAdded": "{{distro}} ကို ထည့်သွင်းပြီးဖြစ်သည်။", + "desktop.wsl.error.opencodeMissing": "opencode ကို ဤ distro တွင် ထည့်သွင်းမထားပါ။", + "desktop.wsl.error.opencodeCannotRun": "opencode ကို ထည့်သွင်းထားသော်လည်း မလည်ပတ်နိုင်ပါ။", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode ကို {{distro}} တွင် ထည့်သွင်းမထားပါ။", + "desktop.wsl.error.updateVersion": + "OpenCode အပ်ဒိတ် ပြီးဆုံးသွားသော်လည်း {{distro}} သည် {{installed}} မှ ဆက်လက်တင်ပြနေပါသည်။ {{expected}} မျှော်လင့်ထားသည်။", + "desktop.wsl.error.noVersion": "ဗားရှင်းမရှိပါ။", + "desktop.wsl.error.serverExited": "စတင်ပြီးနောက် WSL ဆာဗာမှ ထွက်ခဲ့သည် (ကုဒ်={{code}} အချက်ပြ={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "ကျန်းမာရေးမကောင်းမီ WSL ဆာဗာမှ ထွက်ခဲ့သည် (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "{{distro}} ကျန်းမာရေးစစ်ဆေးမှုအတွက် ဆိုက်ကားဆရာ {{timeout}}ms ပြီးနောက် အချိန်ကုန်သွားသည်", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} {{timeout}}ms ပြီးနောက် အချိန်ကုန်သွားသည်", + "desktop.wsl.error.failedPort": "ဆိပ်ကမ်းကို ရယူရန် မအောင်မြင်ပါ။", + "desktop.picker.error.notSelected": "ဖိုင်ကို ရွေးချယ်သူမှ ရွေးချယ်မထားပါ။", + "desktop.picker.error.sizeLimit": "ရွေးချယ်ထားသော ပူးတွဲပါဖိုင်များသည် {{limit}} MB ကန့်သတ်ချက်ထက် ကျော်လွန်ပါသည်။", + "command.category.suggested": "အကြံပြုထားသည်။", + "command.category.view": "ကြည့်ရန်", + "command.category.project": "ပရောဂျက်", + "command.category.provider": "ပံ့ပိုးပေးသူ", + "command.category.server": "ဆာဗာ", + "command.category.session": "အပိုင်း", + "command.category.theme": "အပြင်အဆင်", + "command.category.language": "ဘာသာစကား", + "command.category.file": "ဖိုင်", + "command.category.context": "ဆက်စပ်အကြောင်းအရာ", + "command.category.terminal": "Terminal", + "command.category.model": "မော်ဒယ်", + "command.category.mcp": "MCP", + "command.category.agent": "အေးဂျင့်", + "command.category.permissions": "ခွင့်ပြုချက်များ", + "command.category.workspace": "အလုပ်နေရာ", + "command.category.settings": "ဆက်တင်များ", + "command.logs.export": "မှတ်တမ်းများကို ထုတ်ယူပါ။", + "theme.scheme.system": "စနစ်", + "theme.scheme.light": "အလင်း", + "theme.scheme.dark": "အမှောင်", + "command.sidebar.toggle": "ဘေးဘားကို ပြောင်းရန်", + "command.project.open": "ပရောဂျက်ကို ဖွင့်ပါ။", + "command.project.previous": "ယခင်ပရောဂျက်", + "command.project.next": "နောက်ပရောဂျက်", + "command.project.index": "ပရောဂျက် {{index}} သို့ ပြောင်းပါ။", + "command.provider.connect": "ဝန်ဆောင်မှုပေးသူကို ချိတ်ဆက်ပါ။", + "command.server.switch": "ဆာဗာပြောင်းပါ။", + "command.settings.open": "ဆက်တင်များကိုဖွင့်ပါ။", + "command.session.previous": "ယခင်စက်ရှင်", + "command.session.next": "နောက်ဆက်ရှင်", + "command.session.previous.unseen": "ယခင် မဖတ်ရသေးသော စက်ရှင်", + "command.session.next.unseen": "နောက်တစ်ခု မဖတ်ရသေးသော စက်ရှင်", + "command.session.archive": "မှတ်တမ်း အပိုင်း", + "command.palette": "အမိန့်များစာရင်း", + "command.theme.cycle": "Cycle အပြင်အဆင်", + "command.theme.set": "အပြင်အဆင်ကို အသုံးပြုပါ- {{theme}}", + "command.theme.scheme.cycle": "စက်ဝိုင်းအရောင်ပုံစံ", + "command.theme.scheme.set": "အရောင်အစီအစဉ်- {{scheme}} ကို အသုံးပြုပါ။", + "command.language.cycle": "စက်ဝိုင်းဘာသာစကား", + "command.language.set": "ဘာသာစကားကို အသုံးပြုပါ- {{language}}", + "command.session.new": "စက်ရှင်အသစ်", + "command.file.open": "ဖိုင်ကိုဖွင့်ပါ။", + "command.tab.close": "တဘ်ကို ပိတ်ပါ။", + "command.tab.reopenClosed": "ပိတ်ထားသော တက်ဘ်ကို ပြန်ဖွင့်ပါ။", + "command.context.addSelection": "ရွေးချယ်ထားသည်ကို ဆက်စပ်အကြောင်းအရာထဲ ထည့်ပါ။", + "command.context.addSelection.description": "လက်ရှိဖိုင်မှ ရွေးချယ်ထားသော လိုင်းများကို ထည့်ပါ။", + "command.input.focus": "အာရုံစူးစိုက်မှု ထည့်သွင်းမှု", + "command.terminal.toggle": "Terminal ကို ပြောင်းပါ", + "command.fileTree.toggle": "ဖိုင်သစ်ပြောင်းရန်", + "command.review.toggle": "သုံးသပ်ချက်ကို ပြောင်းရန်", + "command.terminal.new": "Terminal အသစ်", + "command.terminal.new.description": "terminal တက်ဘ်အသစ်တစ်ခု ဖန်တီးပါ။", + "command.steps.toggle": "အဆင့်များကို ပြောင်းရန်", + "command.steps.toggle.description": "လက်ရှိမက်ဆေ့ဂျ်အတွက် အဆင့်များကို ပြပါ သို့မဟုတ် ဝှက်ပါ။", + "command.message.previous": "ယခင်မက်ဆေ့ဂျ်", + "command.message.previous.description": "ယခင်အသုံးပြုသူ မက်ဆေ့ချ်သို့ သွားပါ။", + "command.message.next": "နောက်မက်ဆေ့ဂျ်", + "command.message.next.description": "နောက်အသုံးပြုသူ မက်ဆေ့ဂျ်သို့ သွားပါ။", + "command.model.choose": "မော်ဒယ်ကို ရွေးပါ။", + "command.model.choose.description": "မတူညီသော မော်ဒယ်ကို ရွေးပါ။", + "command.mcp.toggle": "MCP များကို ပြောင်းရန်", + "command.mcp.toggle.description": "MCP များကို ပြောင်းရန်", + "command.agent.cycle": "သံသရာအေးဂျင့်", + "command.agent.cycle.description": "နောက်အေးဂျင့်သို့ ပြောင်းပါ။", + "command.agent.cycle.reverse": "နောက်ပြန်လှည့်သည့် အေးဂျင့်စက်ဝန်း", + "command.agent.cycle.reverse.description": "ယခင်အေးဂျင့်သို့ ပြောင်းပါ။", + "command.model.variant.cycle": "သံသရာတွေးတောအားထုတ်မှု", + "command.model.variant.cycle.description": "နောက်ထပ်ကြိုးစားမှုအဆင့်သို့ ပြောင်းပါ။", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "ခွင့်ပြုချက်များကို အလိုအလျောက် လက်ခံသည်။", + "command.permissions.autoaccept.disable": "ခွင့်ပြုချက်များကို အလိုအလျောက်လက်ခံခြင်းကို ရပ်ပါ။", + "command.workspace.toggle": "အလုပ်နေရာများ ပြောင်းရန်", + "command.workspace.toggle.description": "ဘေးဘားရှိ အလုပ်နေရာများစွာကို ဖွင့်ပါ သို့မဟုတ် ပိတ်ပါ။", + "command.session.undo": "မလုပ်တော့ပါ။", + "command.session.undo.description": "နောက်ဆုံးမက်ဆေ့ဂျ်ကို ပြန်ဖျက်ပါ။", + "command.session.redo": "ပြန်လုပ်ပါ။", + "command.session.redo.description": "နောက်ဆုံးမပြီးသော မက်ဆေ့ဂျ်ကို ပြန်လုပ်ပါ။", + "command.session.compact": "ကျစ်လစ်သိပ်သည်းသော စက်ရှင်", + "command.session.compact.description": "အကြောင်းအရာအရွယ်အစားကို လျှော့ချရန် စက်ရှင်ကို အကျဉ်းချုံ့ပါ။", + "command.session.fork": "မက်ဆေ့ဂျ်မှ ခက်ရင်း", + "command.session.fork.description": "ယခင်မက်ဆေ့ဂျ်မှ ဆက်ရှင်အသစ်တစ်ခု ဖန်တီးပါ။", + "command.session.share": "စက်ရှင်ကို မျှဝေပါ။", + "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": "ရလဒ်များမတွေ့ပါ။", + "palette.group.commands": "အမိန့်များ", + "palette.group.files": "ဖိုင်များ", + "dialog.provider.search.placeholder": "ရှာဖွေပေးသူများ", + "dialog.provider.empty": "ဝန်ဆောင်မှုပေးသူများကို မတွေ့ပါ။", + "dialog.provider.group.popular": "လူကြိုက်များသည်။", + "dialog.provider.group.other": "အခြား", + "dialog.provider.custom.label": "စိတ်ကြိုက် OpenAI-သဟဇာတ ပံ့ပိုးပေးသူ", + "dialog.provider.tag.recommended": "အကြံပြုထားသည်။", + "dialog.provider.opencode.note": "Claude၊ GPT၊ Gemini နှင့် အခြားအရာများ အပါအဝင် ရွေးချယ်ထားသော မော်ဒယ်များ", + "dialog.provider.opencode.tagline": "ယုံကြည်စိတ်ချရသော အကောင်းဆုံးပြင်ဆင်ထားသော မော်ဒယ်များ", + "dialog.provider.opencodeGo.tagline": "လူတိုင်းအတွက် ကုန်ကျစရိတ်သက်သာသော စာရင်းသွင်းမှု", + "dialog.provider.anthropic.note": "Pro နှင့် Max အပါအဝင် Claude မော်ဒယ်များသို့ တိုက်ရိုက်ဝင်ရောက်ခွင့်", + "dialog.provider.copilot.note": "GitHub Copilot မှတစ်ဆင့် ကုဒ်ရေးနည်းအကူအညီအတွက် AI မော်ဒယ်များ", + "dialog.provider.openai.note": "လျင်မြန်ပြီး လုပ်ဆောင်နိုင်သော အထွေထွေ AI လုပ်ဆောင်ချက်များအတွက် GPT မော်ဒယ်များ", + "dialog.provider.google.note": "မြန်ဆန်ပြီး ဖွဲ့စည်းတည်ဆောက်ထားသော တုံ့ပြန်မှုများအတွက် Gemini မော်ဒယ်များ", + "dialog.provider.openrouter.note": "ပံ့ပိုးပေးသူတစ်ခုတည်းမှ ပံ့ပိုးပေးထားသော မော်ဒယ်အားလုံးကို ဝင်ရောက်ကြည့်ရှုပါ။", + "dialog.provider.vercel.note": "စမတ်လမ်းကြောင်းဖြင့် AI မော်ဒယ်များသို့ တစ်စုတစ်စည်းတည်း ဝင်ရောက်ခွင့်", + "dialog.model.select.title": "မော်ဒယ်ကို ရွေးပါ။", + "dialog.model.search.placeholder": "မော်ဒယ်များကို ရှာဖွေပါ။", + "dialog.model.empty": "မော်ဒယ်ရလဒ်များ မရှိပါ။", + "dialog.model.manage": "မော်ဒယ်များကို စီမံပါ။", + "dialog.model.manage.description": "မော်ဒယ်ရွေးချယ်မှုတွင် မည်သည့်မော်ဒယ်များကို စိတ်ကြိုက်ပြင်ဆင်ပါ။", + "dialog.model.manage.provider.toggle": "{{provider}} မော်ဒယ်အားလုံးကို ပြောင်းရန်", + "dialog.model.unpaid.freeModels.title": "OpenCode မှ ပံ့ပိုးပေးသော အခမဲ့မော်ဒယ်များ", + "dialog.model.unpaid.addMore.title": "လူကြိုက်များသော ဝန်ဆောင်မှုပေးသူများထံမှ နောက်ထပ်မော်ဒယ်များကို ထည့်ပါ။", + "dialog.model.unpaid.viewMoreProviders": "နောက်ထပ်ပံ့ပိုးပေးသူ 70+ ကိုကြည့်ပါ။", + "dialog.provider.viewAll": "နောက်ထပ်ဝန်ဆောင်မှုပေးသူများကို ပြပါ။", + "provider.connect.title": "{{provider}} ကို ချိတ်ဆက်ပါ။", + "provider.connect.title.anthropicProMax": "Claude Pro/Max ဖြင့် ဝင်ရောက်ပါ။", + "provider.connect.selectMethod": "{{provider}} အတွက် အကောင့်ဝင်နည်းလမ်းကို ရွေးပါ။", + "provider.connect.method.apiKey": "API သော့", + "provider.connect.method.browser": "ဘရောက်ဆာ", + "provider.connect.method.headless": "ဦးခေါင်းမဲ့", + "provider.connect.status.inProgress": "ခွင့်ပြုချက် လုပ်ဆောင်နေသည်...", + "provider.connect.status.waiting": "ခွင့်ပြုချက်ကို စောင့်ဆိုင်းနေသည်...", + "provider.connect.status.failed": "ခွင့်ပြုချက် မအောင်မြင်ပါ- {{error}}", + "provider.connect.apiKey.description": + "သင့်အကောင့်ကို ချိတ်ဆက်ရန်နှင့် OpenCode တွင် {{provider}} မော်ဒယ်များကို အသုံးပြုရန် သင်၏ {{provider}} API သော့ကို ထည့်သွင်းပါ။", + "provider.connect.apiKey.label": "{{provider}} API သော့", + "provider.connect.apiKey.placeholder": "API သော့", + "provider.connect.apiKey.required": "API ကီး လိုအပ်သည်။", + "provider.connect.opencodeZen.line1": + "OpenCode Zen သည် သင့်အား coding အေးဂျင့်များအတွက် ယုံကြည်စိတ်ချရသော အကောင်းဆုံးပုံစံများဖြင့် စုစည်းထားသော မော်ဒယ်များကို ဝင်ရောက်ခွင့်ပေးသည်။", + "provider.connect.opencodeZen.line2": + "API သော့တစ်ခုတည်းဖြင့် သင်သည် Claude၊ GPT၊ Gemini၊ GLM နှင့် အခြားအရာများကဲ့သို့သော မော်ဒယ်များသို့ ဝင်ရောက်ခွင့်ရရှိမည်ဖြစ်သည်။", + "provider.connect.opencodeZen.visit.prefix": "လည်ပတ်ပါ။ \nသင်၏ API သော့ကိုစုဆောင်းရန်", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " သို့သွား၍ သင်၏ API key ကို ရယူပါ။", + "provider.connect.oauth.code.visit.prefix": "လည်ပတ်ပါ။", + "provider.connect.oauth.code.visit.link": "ဤလင့်ခ်", + "provider.connect.oauth.code.visit.suffix": + " သို့သွားပြီး သင့်အကောင့်ကို ချိတ်ဆက်ကာ OpenCode တွင် {{provider}} မော်ဒယ်များကို အသုံးပြုရန် ခွင့်ပြုချက်ကုဒ်ကို ရယူပါ။", + "provider.connect.oauth.code.label": "{{method}} ခွင့်ပြုချက်ကုဒ်", + "provider.connect.oauth.code.placeholder": "ခွင့်ပြုချက်ကုဒ်", + "provider.connect.oauth.code.required": "ခွင့်ပြုချက်ကုဒ် လိုအပ်သည်။", + "provider.connect.oauth.code.invalid": "တရားဝင်ခွင့်ပြုချက်ကုဒ်", + "provider.connect.oauth.auto.visit.prefix": "လည်ပတ်ပါ။", + "provider.connect.oauth.auto.visit.link": "ဤလင့်ခ်", + "provider.connect.oauth.auto.visit.suffix": + "နှင့် သင့်အကောင့်ကို ချိတ်ဆက်ပြီး OpenCode တွင် {{provider}} မော်ဒယ်များကို အသုံးပြုရန် အောက်ပါကုဒ်ကို ရိုက်ထည့်ပါ။", + "provider.connect.oauth.auto.confirmationCode": "အတည်ပြုကုဒ်", + "provider.connect.toast.connected.title": "{{provider}} ချိတ်ဆက်ထားသည်။", + "provider.connect.toast.connected.description": "{{provider}} မော်ဒယ်များကို ယခုအသုံးပြုနိုင်ပါပြီ။", + "provider.custom.title": "စိတ်ကြိုက်ဝန်ဆောင်မှုပေးသည်။", + "provider.custom.unavailable": "စိတ်ကြိုက်ဝန်ဆောင်မှုပေးသူများကို ဤဆာဗာတွင် မရရှိနိုင်ပါ။", + "provider.custom.description.prefix": "OpenAI-သဟဇာတ ပံ့ပိုးပေးသူကို စီစဉ်သတ်မှတ်ပါ။ ကြည့်ပါ။", + "provider.custom.description.link": "ပံ့ပိုးပေးသူ config docs", + "provider.custom.description.suffix": "။", + "provider.custom.field.providerID.label": "ပံ့ပိုးပေးသူ ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "စာလုံးသေးများ၊ နံပါတ်များ၊ တုံးတိုများ သို့မဟုတ် အောက်မှတ်များ", + "provider.custom.field.name.label": "ပြသမှုအမည်", + "provider.custom.field.name.placeholder": "ကျွန်ုပ်၏ AI ပံ့ပိုးပေးသူ", + "provider.custom.field.baseURL.label": "အခြေခံ URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API သော့", + "provider.custom.field.apiKey.placeholder": "API သော့", + "provider.custom.field.apiKey.description": + "ရွေးချယ်နိုင်သည် ခေါင်းစီးများမှတစ်ဆင့် စစ်မှန်ကြောင်း စီမံခန့်ခွဲပါက ဗလာချန်ထားပါ။", + "provider.custom.models.label": "မော်ဒယ်များ", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "အမည်", + "provider.custom.models.name.placeholder": "ပြသအမည်", + "provider.custom.models.remove": "မော်ဒယ်ကို ဖယ်ရှားပါ။", + "provider.custom.models.add": "မော်ဒယ်ကို ထည့်ပါ။", + "provider.custom.headers.label": "ခေါင်းစီးများ (ချန်လှပ်ထားနိုင်သည်)", + "provider.custom.headers.key.label": "ခေါင်းစီး", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "တန်ဖိုး", + "provider.custom.headers.value.placeholder": "တန်ဖိုး", + "provider.custom.headers.remove": "ခေါင်းစီးကို ဖယ်ရှားပါ။", + "provider.custom.headers.add": "ခေါင်းစီးထည့်ပါ။", + "provider.custom.error.providerID.required": "ပံ့ပိုးပေးသူ ID လိုအပ်သည်။", + "provider.custom.error.providerID.format": + "စာလုံးသေးများ၊ နံပါတ်များ၊ တုံးတိုများ သို့မဟုတ် အောက်မှတ်များကို သုံးပါ။", + "provider.custom.error.providerID.exists": "ထိုပံ့ပိုးပေးသူ ID ရှိပြီးဖြစ်သည်။", + "provider.custom.error.name.required": "ပြသမှုအမည် လိုအပ်သည်။", + "provider.custom.error.baseURL.required": "အခြေခံ URL လိုအပ်သည်။", + "provider.custom.error.baseURL.format": "http:// သို့မဟုတ် https:// ဖြင့် စတင်ရပါမည်။", + "provider.custom.error.required": "လိုအပ်သည်။", + "provider.custom.error.duplicate": "ပွားပါ။", + "provider.disconnect.toast.disconnected.title": "{{provider}} ချိတ်ဆက်မှု ပြတ်တောက်သွားသည်။", + "provider.disconnect.toast.disconnected.description": "{{provider}} မော်ဒယ်များကို မရနိုင်တော့ပါ။", + "model.tag.free": "အခမဲ့", + "model.tag.latest": "နောက်ဆုံးထွက်", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "စာသား", + "model.input.image": "ပုံ", + "model.input.audio": "အသံ", + "model.input.video": "ဗီဒီယို", + "model.input.pdf": "pdf", + "model.tooltip.allows": "ခွင့်ပြုသည်- {{inputs}}", + "model.tooltip.context.label": "ဆက်စပ်အကြောင်းအရာ", + "model.tooltip.inputs": "ထည့်သွင်းမှုများ", + "model.tooltip.model": "မော်ဒယ်", + "model.tooltip.provider": "ပံ့ပိုးပေးသူ", + "model.tooltip.reasoning": "ကျိုးကြောင်းဆင်ခြင်ခြင်း။", + "model.tooltip.reasoning.allowed": "ကျိုးကြောင်းဆင်ခြင်မှုကို ခွင့်ပြုသည်။", + "model.tooltip.reasoning.none": "အကြောင်းပြချက်မရှိပါ။", + "model.tooltip.context": "ဆက်စပ်အကြောင်းအရာ ကန့်သတ်ချက် {{limit}}", + "common.search.placeholder": "ရှာရန်", + "common.goBack": "ပြန်သွားပါ။", + "common.goForward": "ရှေ့သို့သွားပါ။", + "common.loading": "တင်နေသည်။", + "common.loading.ellipsis": "...", + "common.cancel": "မလုပ်တော့", + "common.clear": "ရှင်းလင်းသည်။", + "common.open": "ဖွင့်ပါ။", + "common.connect": "ချိတ်ဆက်ပါ။", + "common.disconnect": "အဆက်ဖြတ်ပါ။", + "common.continue": "ဆက်ရန်", + "common.submit": "တင်ပြပါ။", + "common.save": "သိမ်းဆည်းပါ။", + "common.saving": "သိမ်းဆည်းနေသည်...", + "common.default": "မူရင်း", + "common.attachment": "တွယ်တာမှု", + "prompt.placeholder.shell": "shell command ကိုရိုက်ထည့်ပါ... {{example}}", + "prompt.placeholder.normal": 'ဘာမဆိုမေးပါ... "{{example}}"', + "prompt.placeholder.simple": "ဘာမဆိုမေးပါ...", + "prompt.placeholder.summarizeComments": "မှတ်ချက်များကို အကျဉ်းချုပ်ပါ...", + "prompt.placeholder.summarizeComment": "မှတ်ချက်ကို အကျဉ်းချုပ်ပါ...", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "esc", + "session.child.promptDisabled": "Subagent ဆက်ရှင်များကို တောင်းဆို၍မရပါ။", + "session.child.backToParent": "ပင်မစက်ရှင်သို့ ပြန်သွားရန်။", + "prompt.example.1": "ကုဒ်ဘေ့စ်တွင် TODO ကိုပြင်ပါ။", + "prompt.example.2": "ဤပရောဂျက်၏ နည်းပညာအစုအပုံကား အဘယ်နည်း။", + "prompt.example.3": "ပျက်စီးနေသော စမ်းသပ်မှုများကို ပြင်ပါ။", + "prompt.example.4": "အထောက်အထားစိစစ်ခြင်း အလုပ်လုပ်ပုံကို ရှင်းပြပါ။", + "prompt.example.5": "လုံခြုံရေး အားနည်းချက်များကို ရှာဖွေပြီး ပြင်ဆင်ပါ။", + "prompt.example.6": "သုံးစွဲသူဝန်ဆောင်မှုအတွက် ယူနစ်စမ်းသပ်မှုများကို ထည့်ပါ။", + "prompt.example.7": "ပိုမိုဖတ်ရှုနိုင်စေရန် ဤလုပ်ဆောင်ချက်ကို ပြန်လည်ပြင်ဆင်ပါ။", + "prompt.example.8": "ဤအမှားက ဘာကိုဆိုလိုသနည်း။", + "prompt.example.9": "ဤပြဿနာကို ကျွန်ုပ်အား ကူညီဖြေရှင်းပါ။", + "prompt.example.10": "API စာရွက်စာတမ်းကို ဖန်တီးပါ။", + "prompt.example.11": "ဒေတာဘေ့စ်မေးခွန်းများကို ပိုမိုကောင်းမွန်အောင်ပြုလုပ်ပါ။", + "prompt.example.12": "ထည့်သွင်းမှုအတည်ပြုချက်ကို ထည့်ပါ။", + "prompt.example.13": "အတွက် အစိတ်အပိုင်းအသစ်တစ်ခု ဖန်တီးပါ...", + "prompt.example.14": "ဤပရောဂျက်ကို မည်သို့အသုံးချရမည်နည်း။", + "prompt.example.15": "အကောင်းဆုံးအလေ့အကျင့်များအတွက် ကျွန်ုပ်၏ကုဒ်ကို ပြန်လည်သုံးသပ်ပါ။", + "prompt.example.16": "ဤလုပ်ဆောင်ချက်သို့ အမှားအယွင်းများကို ကိုင်တွယ်ထည့်ပါ။", + "prompt.example.17": "ဤ regex ပုံစံကို ရှင်းပြပါ။", + "prompt.example.18": "၎င်းကို TypeScript သို့ ပြောင်းပါ။", + "prompt.example.19": "ကုဒ်ဘေ့စ်တစ်လျှောက်တွင် မှတ်တမ်းထည့်ပါ။", + "prompt.example.20": "မည်သည့်မှီခိုမှုများ ခေတ်နောက်ကျနေသနည်း။", + "prompt.example.21": "ရွှေ့ပြောင်းခြင်းဆိုင်ရာ ဇာတ်ညွှန်းရေးရန် ကူညီပေးပါ။", + "prompt.example.22": "ဤအဆုံးမှတ်အတွက် ကက်ရှ်ကို အကောင်အထည်ဖော်ပါ။", + "prompt.example.23": "ဤစာရင်းသို့ pagination ထည့်ပါ။", + "prompt.example.24": "အတွက် CLI အမိန့်ကို ဖန်တီးပါ...", + "prompt.example.25": "ဤနေရာတွင် ပတ်ဝန်းကျင် ကိန်းရှင်များ မည်သို့အလုပ်လုပ်သနည်း။", + "prompt.popover.emptyResults": "တူညီသောရလဒ်များမရှိပါ။", + "prompt.popover.emptyCommands": "တူညီသော ညွှန်ကြားချက်များ မရှိပါ။", + "prompt.dropzone.label": "ပုံများ၊ PDF များ သို့မဟုတ် စာသားဖိုင်များကို ဤနေရာတွင် ချလိုက်ပါ။", + "prompt.dropzone.file.label": "@ဖော်ပြချက်ဖိုင်သို့ ချလိုက်ပါ။", + "prompt.slash.badge.custom": "စိတ်ကြိုက်", + "prompt.slash.badge.skill": "ကျွမ်းကျင်မှု", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "တက်ကြွသည်။", + "prompt.context.includeActiveFile": "လက်ရှိအသုံးပြုနေသောဖိုင်ကို ထည့်သွင်းပါ။", + "prompt.context.removeActiveFile": "အသုံးပြုနေသောဖိုင်ကို ဆက်စပ်အကြောင်းအရာမှ ဖယ်ရှားပါ။", + "prompt.context.removeFile": "ဆက်စပ်အကြောင်းအရာမှ ဖိုင်ကို ဖယ်ရှားပါ။", + "prompt.action.attachFile": "ဖိုင်များထည့်ပါ။", + "prompt.menu.addImagesAndFiles": "ဖိုင်များနှင့် အခြားအရာများကို ထည့်ပါ။", + "prompt.menu.imagesAndFiles": "ရုပ်ပုံများနှင့် ဖိုင်များ", + "prompt.menu.commands": "အမိန့်များ", + "prompt.menu.context": "ဆက်စပ်အကြောင်းအရာ", + "prompt.menu.shellCommand": "Shell အမိန့်", + "prompt.attachment.remove": "ပူးတွဲပါဖိုင်ကို ဖယ်ရှားပါ။", + "prompt.action.send": "ပို့ပါ။", + "prompt.action.stop": "ရပ်ပါ။", + "prompt.toast.pasteUnsupported.title": "ပူးတွဲပါဖိုင်ကို ပံ့ပိုးမထားပါ။", + "prompt.toast.pasteUnsupported.description": + "ရုပ်ပုံများ၊ PDF များ သို့မဟုတ် စာသားဖိုင်များကိုသာ ဤနေရာတွင် ပူးတွဲနိုင်ပါသည်။", + "prompt.toast.attachmentDuplicate.title": "ဤဖိုင်ကို အပ်လုဒ်လုပ်ပြီးပါပြီ။", + "prompt.toast.modelAgentRequired.title": "အေးဂျင့်နှင့် မော်ဒယ်ကို ရွေးပါ။", + "prompt.toast.modelAgentRequired.description": "Prompt မပို့မီ အေးဂျင့်နှင့် မော်ဒယ်ကို ရွေးပါ။", + "prompt.toast.worktreeCreateFailed.title": "Git worktree ဖန်တီး၍မရပါ။", + "prompt.toast.sessionCreateFailed.title": "စက်ရှင်ကို ဖန်တီး၍မရပါ။", + "prompt.toast.shellSendFailed.title": "shell command ကို ပို့၍မရပါ။", + "prompt.toast.commandSendFailed.title": "အမိန့်ပေးပို့ရန် မအောင်မြင်ပါ။", + "prompt.toast.promptSendFailed.title": "Prompt ပေးပို့ရန် မအောင်မြင်ပါ။", + "prompt.toast.promptSendFailed.description": "စက်ရှင်ကို ထုတ်ယူ၍မရပါ။", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} ၏ {{total}} ဖွင့်ထားသည်။", + "dialog.mcp.empty": "MCP များကို ပြင်ဆင်သတ်မှတ်ထားခြင်းမရှိပါ။", + "dialog.lsp.empty": "ဖိုင်အမျိုးအစားများမှ LSP များကို အလိုအလျောက်တွေ့ရှိသည်။", + "dialog.plugins.empty": "opencode.json တွင် ပြင်ဆင်ထားသော ပလပ်အင်များ", + "mcp.status.connected": "ချိတ်ဆက်ထားသည်။", + "mcp.status.failed": "မအောင်မြင်ပါ။", + "mcp.status.needs_auth": "အထောက်အထား လိုအပ်သည်။", + "mcp.status.disabled": "ပိတ်ထားသည်။", + "mcp.auth.clickToAuthenticate": "စစ်မှန်ကြောင်းသက်သေပြရန် နှိပ်ပါ။", + "dialog.fork.empty": "လမ်းခွဲရန် မက်ဆေ့ချ်များ မရှိပါ။", + "dialog.directory.search.placeholder": "ဖိုင်တွဲများကို ရှာပါ။", + "dialog.directory.empty": "ဖိုင်တွဲများကို ရှာမတွေ့ပါ။", + "dialog.directory.action.selectFile": "ဖိုင်ကို ရွေးပါ။", + "dialog.directory.action.selectFolder": "ဖိုင်တွဲကို ရွေးပါ။", + "dialog.directory.root": "အမြစ်", + "dialog.directory.parent": "မိဘ", + "dialog.directory.readError": "ဤဖိုင်တွဲကို ဖတ်၍မရပါ။", + "app.server.unreachable": "{{server}} သို့ မရောက်ရှိနိုင်ပါ။", + "app.server.retrying": "အလိုအလျောက် ပြန်ကြိုးစားနေသည်...", + "app.server.otherServers": "အခြားဆာဗာများ", + "dialog.server.title": "ဆာဗာများ", + "dialog.server.description": "ဤအက်ပ်နှင့်ချိတ်ဆက်သည့် OpenCode ဆာဗာကို ပြောင်းပါ။", + "dialog.server.search.placeholder": "ဆာဗာများကို ရှာဖွေပါ။", + "dialog.server.empty": "ဆာဗာများ မရှိသေးပါ။", + "dialog.server.add.title": "ဆာဗာထည့်ပါ။", + "dialog.server.add.url": "ဆာဗာလိပ်စာ", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "ဆာဗာသို့ ချိတ်ဆက်၍မရပါ။", + "dialog.server.add.checking": "စစ်ဆေးနေသည်...", + "dialog.server.add.button": "ဆာဗာထည့်ပါ။", + "dialog.server.add.name": "ဆာဗာအမည် (ချန်လှပ်ထားနိုင်သည်)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "အသုံးပြုသူအမည် (ချန်လှပ်ထားနိုင်သည်)", + "dialog.server.add.usernamePlaceholder": "အသုံးပြုသူအမည်", + "dialog.server.add.password": "စကားဝှက် (ချန်လှပ်ထားနိုင်သည်)", + "dialog.server.add.passwordPlaceholder": "စကားဝှက်", + "dialog.server.edit.title": "ဆာဗာကို တည်းဖြတ်ပါ။", + "dialog.server.default.title": "မူရင်းဆာဗာ", + "dialog.server.default.description": + "ဒေသတွင်းဆာဗာကို စတင်မည့်အစား အက်ပ်စတင်ခြင်းတွင် ဤဆာဗာသို့ ချိတ်ဆက်ပါ။ ပြန်လည်စတင်ရန် လိုအပ်သည်။", + "dialog.server.default.none": "ဆာဗာကို ရွေးချယ်ထားခြင်းမရှိပါ။", + "dialog.server.default.set": "လက်ရှိဆာဗာကို မူရင်းအဖြစ် သတ်မှတ်ပါ။", + "dialog.server.default.clear": "ရှင်းလင်းသည်။", + "dialog.server.action.remove": "ဆာဗာကို ဖယ်ရှားပါ။", + "dialog.server.menu.edit": "တည်းဖြတ်ရန်", + "dialog.server.menu.default": "မူရင်းအဖြစ် သတ်မှတ်ပါ။", + "dialog.server.menu.defaultRemove": "မူရင်းကို ဖယ်ရှားပါ။", + "dialog.server.menu.delete": "ဖျက်ပါ", + "dialog.server.current": "လက်ရှိ ဆာဗာ", + "dialog.server.status.default": "မူရင်း", + "wsl.server.add": "WSL ဆာဗာကို ထည့်ပါ။", + "wsl.server.addShort": "WSL ထည့်ပါ။", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL ဆာဗာ", + "wsl.server.retryStart": "ပြန်လည်စတင်ကြည့်ပါ။", + "wsl.server.updating": "အပ်ဒိတ်လုပ်နေသည်...", + "wsl.onboarding.step.distro": "distro ကို ရွေးပါ။", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL ကို စစ်ဆေးနေသည်...", + "wsl.onboarding.restartRequired": "WSL ထည့်သွင်းခြင်းကို အပြီးသတ်ရန် Windows ကို ပြန်လည်စတင်ရန် လိုအပ်သည်။", + "wsl.onboarding.ready": "WSL အဆင်သင့်ဖြစ်ပါပြီ။", + "wsl.onboarding.required": "ရှေ့ဆက်ရန် WSL လိုအပ်သည်။", + "wsl.onboarding.checkingDistros": "ကွဲပြားမှုများကို စစ်ဆေးနေသည်...", + "wsl.onboarding.installingDistro": "{{distro}} ကို ထည့်သွင်းနေသည်...", + "wsl.onboarding.checkingDistro": "{{distro}} ကို စစ်ဆေးနေသည်...", + "wsl.onboarding.listingDistros": "ကွဲပြားမှုများကို စာရင်းသွင်းနေသည်...", + "wsl.onboarding.distroReady": "{{distro}} အဆင်သင့်ဖြစ်ပါပြီ။", + "wsl.onboarding.distroNotInstalled": "{{distro}} ကို မထည့်သွင်းရသေးပါ။", + "wsl.onboarding.openDistroOnce": "စနစ်ထည့်သွင်းမှုကို အပြီးသတ်ရန် {{distro}} တစ်ကြိမ်ဖွင့်ပါ။", + "wsl.onboarding.finishingDistro": "{{distro}} အတွက် စနစ်ထည့်သွင်းမှု အပြီးသတ်နေပါသည်။", + "wsl.onboarding.pickDistro": "distro တစ်ခုကို ရွေးပါ သို့မဟုတ် အောက်တွင် တစ်ခု ထည့်သွင်းပါ။", + "wsl.onboarding.checkingOpencode": "OpenCode စစ်ဆေးနေသည်...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}} တွင် OpenCode ကိုစစ်ဆေးနေသည်...", + "wsl.onboarding.updatingOpencode": "OpenCode ကို အပ်ဒိတ်လုပ်နေသည်...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}} တွင် OpenCode ကို အပ်ဒိတ်လုပ်နေသည်...", + "wsl.onboarding.updateOpencodeIn": "{{distro}} တွင် OpenCode ကို အပ်ဒိတ်လုပ်ပါ။", + "wsl.onboarding.updateOpencode": "OpenCode ကို အပ်ဒိတ်လုပ်ပါ။", + "wsl.onboarding.opencodeReadyIn": "OpenCode သည် {{distro}} တွင် အဆင်သင့်ဖြစ်နေပါပြီ။", + "wsl.onboarding.opencodeReady": "OpenCode အဆင်သင့်ဖြစ်ပါပြီ။", + "wsl.onboarding.installOpencodeIn": "OpenCode ကို {{distro}} တွင် ထည့်သွင်းပါ။", + "wsl.onboarding.installOpencode": "OpenCode ကို ထည့်သွင်းပါ။", + "wsl.onboarding.chooseDistroFirst": "ပထမဆုံး distro တစ်ခုကို ရွေးပါ။", + "wsl.onboarding.loadFailed": "WSL အခြေအနေအား တင်ရန် မအောင်မြင်ပါ။", + "wsl.onboarding.loading": "ဖွင့်နေသည်...", + "wsl.onboarding.installedDistros": "ကွဲပြားမှုများကို ထည့်သွင်းထားသည်။", + "wsl.onboarding.checkAgain": "ထပ်မံစစ်ဆေးပါ။", + "wsl.onboarding.distroStatus.ready": "အဆင်သင့်ဖြစ်ပါပြီ။", + "wsl.onboarding.distroStatus.checking": "စစ်ဆေးနေသည်...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode ကို ထည့်သွင်းမထားပါ။", + "wsl.onboarding.distroStatus.missingTools": "ပျောက်နေသော ဘေ့စ်၊ ကောက်ကြောင်း", + "wsl.onboarding.distroStatus.unsupported": "ပံ့ပိုးမထားပါ · WSL 2 ကို သုံးပါ။", + "wsl.onboarding.needAnotherDistro": "နောက်ထပ် distro တစ်ခုလိုပါသလား။", + "wsl.onboarding.needAnotherDistroHint": "WSL ကတ်တလောက်မှ Linux ဖြန့်ဖြူးမှုကို ထည့်သွင်းပါ။", + "wsl.onboarding.wslNotInstalled.title": "WSL ကို ထည့်သွင်းမထားပါ။", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Linux အတွက် Windows Subsystem) ကို OpenCode မှ WSL ဆာဗာ မထည့်မီတွင် လိုအပ်ပါသည်။", + "wsl.onboarding.wslUnavailable.title": "WSL မရနိုင်ပါ။", + "wsl.onboarding.wslUnavailable.description": "OpenCode သည် ဤစက်ပေါ်ရှိ WSL ကို အတည်မပြုနိုင်ပါ။", + "wsl.onboarding.installWsl": "WSL ကို ထည့်သွင်းပါ။", + "wsl.onboarding.windowsRestartRequired": + "WSL ထည့်သွင်းခြင်း အပြီးသတ်ရန် Windows ကို ပြန်လည်စတင်ပါ၊ ထို့နောက် OpenCode ကို ပြန်လည်ဖွင့်ပါ။", + "wsl.onboarding.next": "နောက်တစ်ခု", + "wsl.onboarding.refresh": "ပြန်လည်စတင်ပါ။", + "wsl.onboarding.allDistrosAdded": "ထည့်သွင်းထားသည့် distros အားလုံးကို ထည့်သွင်းပြီးဖြစ်သည်။", + "wsl.onboarding.noDistros": "မည်သည့် distros မှ ရှာမတွေ့သေးပါ။", + "wsl.onboarding.install": "ထည့်သွင်းပါ။", + "wsl.onboarding.installing": "ထည့်သွင်းနေသည်...", + "wsl.onboarding.installDistro": "distro ကို ထည့်သွင်းပါ။", + "wsl.onboarding.searchDistros": "ကွဲပြားမှုများကို ရှာဖွေပါ။", + "wsl.onboarding.wsl2Required": "WSL 2 လိုအပ်သည်။", + "wsl.onboarding.toolsRequired": "ဤ distro သည် bash နှင့် curl လိုအပ်သည်။", + "wsl.onboarding.openTerminal": "Terminal ကိုဖွင့်ပါ။", + "wsl.onboarding.path": "လမ်းကြောင်း- {{path}}", + "wsl.onboarding.notFound": "မတွေ့ပါ။", + "wsl.onboarding.version": "ဗားရှင်း- {{version}}", + "wsl.onboarding.unknown": "အမည်မသိ", + "wsl.onboarding.desktopVersion": "ဒက်စ်တော့ {{version}}", + "wsl.onboarding.versionMismatch": "ထည့်သွင်းထားသောဗားရှင်းသည် ဒက်စ်တော့အက်ပ်ဗားရှင်းနှင့် မကိုက်ညီပါ။", + "wsl.onboarding.adding": "ထည့်နေသည်...", + "help.tabs.toast.ariaLabel": + "တဘ်များကို မိတ်ဆက်ခြင်း။ တက်ဘ်များဖြင့် သင်၏အလုပ်နှင့် တက်ကြွသော ဆက်ရှင်များကို စုစည်းပါ။", + "help.tabs.toast.dismiss": "Tabs အချက်အလက်ကို ဖယ်ပါ။", + "help.tabs.title": "တဘ်များကို မိတ်ဆက်ခြင်း။", + "help.tabs.description": "တက်ဘ်များဖြင့် သင်၏အလုပ်နှင့် တက်ကြွသော ဆက်ရှင်များကို စုစည်းပါ။", + "help.tabs.date": "ဇူလိုင် ၁၄", + "help.tabs.introduction": "OpenCode Desktop သည် ယခုအခါ တက်ဘ်များအနီးတွင် တည်ဆောက်ထားသည်။", + "help.tabs.sessions": + "တက်ဘ်တစ်ခုတွင် စက်ရှင်အသစ်တစ်ခု စတင်ပါ သို့မဟုတ် သင့်ပရောဂျက်တစ်ခုခုမှ လက်ရှိစက်ရှင်တစ်ခုကို ဖွင့်ပါ။ အသစ်တစ်ခုခုကို စတင်သည့်အခါ တက်ဘ်အသစ်တစ်ခုဖွင့်ပြီး ပြီးသွားသောအခါတွင် ၎င်းကိုပိတ်ပါ။", + "help.tabs.organize": + "တက်ဘ်အနည်းငယ်ကို ဖွင့်ထားခြင်းဖြင့် သင်၏တက်ကြွသော ဆက်ရှင်များကို စုစည်းရန် ပိုမိုလွယ်ကူစေသည်။ ၎င်းတို့ကို ဝိုင်းထားရန် စီစဉ်ပါက တဘ်များကို အမှတ်ရဖွယ်အဖြစ်သို့ အမည်ပြောင်းပါ။", + "help.tabs.home": + "ပင်မစခရင်အသစ်တွင် သင့်စက်ရှင်များနှင့် ပရောဂျက်များအားလုံးကို သင်တွေ့ရပါမည်။ စက်ရှင်တစ်ခုကို ရွေးချယ်ခြင်းဖြင့် ၎င်းကို တက်ဘ်တစ်ခုတွင် ဖွင့်သည်။", + "help.tabs.persistence": "အက်ပ်ကို ပြန်ဖွင့်သောအခါ၊ သင့်တက်ဘ်များကို ဖွင့်ထားဆဲဖြစ်သည်။", + "help.tabs.worktrees": + "ဒီဇိုင်းအသစ်သည် Git Worktrees ကို မပံ့ပိုးရသေးပါ၊ ၎င်းသည် မကြာမီ ရောက်ရှိလာတော့မည်ဖြစ်သည်။ ထို့ကြောင့် သင်သည် ယခင်အပြင်အဆင်ကို ဆက်လက်အသုံးပြုလိုပါက၊ ဆက်တင်များတွင် အပြင်အဆင်များအကြား ပြောင်းနိုင်သည်။ အပြင်အဆင်အသစ်သည် ရက်သတ္တပတ်အနည်းငယ်အတွင်း အမြဲတမ်းဖြစ်လာမည်ကို သတိပြုပါ။", + "server.row.noUsername": "အသုံးပြုသူအမည်မရှိပါ။", + "dialog.project.edit.title": "ပရောဂျက်ကို တည်းဖြတ်ပါ။", + "dialog.project.edit.name": "အမည်", + "dialog.project.edit.icon": "သင်္ကေတ", + "dialog.project.edit.icon.alt": "ပရောဂျက် သင်္ကေတ", + "dialog.project.edit.icon.hint": "ပုံတစ်ပုံကို နှိပ်ပါ သို့မဟုတ် ဆွဲယူပါ။", + "dialog.project.edit.icon.recommended": "အကြံပြုထားသည်- 128x128px", + "dialog.project.edit.color": "အရောင်", + "dialog.project.edit.color.select": "{{color}} အရောင်ကို ရွေးပါ။", + "dialog.project.edit.worktree.startup": "Workspace စတင်ခြင်း script", + "dialog.project.edit.worktree.startup.description": "အလုပ်နေရာအသစ် (Git worktree) ဖန်တီးပြီးနောက် လုပ်ဆောင်သည်။", + "dialog.project.edit.worktree.startup.placeholder": "e.g. bun install လုပ်ပါ။", + "dialog.releaseNotes.action.getStarted": "စတင်လိုက်ပါ။", + "dialog.releaseNotes.action.next": "နောက်တစ်ခု", + "dialog.releaseNotes.action.hideFuture": "ဒါတွေကို အနာဂတ်မှာ မပြပါနဲ့။", + "dialog.releaseNotes.media.alt": "ဖြန့်ချိမှု အကြိုကြည့်ရှုခြင်း။", + "context.breakdown.title": "ဆက်စပ်အကြောင်းအရာ ခွဲခြမ်းချက်", + "context.breakdown.note": + 'ထည့်သွင်းမှုတိုကင်များ၏ အနီးစပ်ဆုံး ခွဲခြမ်းစိတ်ဖြာခြင်း။ "အခြား" တွင် ကိရိယာ၏ အဓိပ္ပါယ်ဖွင့်ဆိုချက်များနှင့် အကျုံးဝင်သည်။', + "context.breakdown.system": "စနစ်", + "context.breakdown.user": "အသုံးပြုသူ", + "context.breakdown.assistant": "လက်ထောက်", + "context.breakdown.tool": "တူးလ်ခေါ်ဆိုမှုများ", + "context.breakdown.other": "အခြား", + "context.systemPrompt.title": "System prompt", + "context.rawMessages.title": "အကြမ်း မက်ဆေ့ချ်များ", + "context.export.session": "စက်ရှင်ကို ထုတ်ယူရန်", + "context.stats.session": "အပိုင်း", + "context.stats.messages": "မက်ဆေ့ဂျ်များ", + "context.stats.provider": "ပံ့ပိုးပေးသူ", + "context.stats.model": "မော်ဒယ်", + "context.stats.limit": "ဆက်စပ်အကြောင်းအရာ ကန့်သတ်ချက်", + "context.stats.totalTokens": "စုစုပေါင်း တိုကင်များ", + "context.stats.usage": "အသုံးပြုမှု", + "context.stats.inputTokens": "ထည့်သွင်း တိုကင်များ", + "context.stats.outputTokens": "အထွက်တိုကင်များ", + "context.stats.reasoningTokens": "ကျိုးကြောင်းဆင်ခြင်ခြင်း တိုကင်များ", + "context.stats.cacheTokens": "ကက်ရှ်တိုကင်များ (ဖတ်ရန်/ရေး)", + "context.stats.userMessages": "အသုံးပြုသူ မက်ဆေ့ချ်များ", + "context.stats.assistantMessages": "Assistant မက်ဆေ့ဂျ်များ", + "context.stats.totalCost": "စုစုပေါင်းကုန်ကျစရိတ်", + "context.stats.sessionCreated": "ဆက်ရှင်ကို ဖန်တီးထားသည်။", + "context.stats.lastActivity": "နောက်ဆုံးလုပ်ဆောင်ချက်", + "context.usage.tokens": "တိုကင်များ", + "context.usage.usage": "အသုံးပြုမှု", + "context.usage.cost": "ကုန်ကျစရိတ်", + "context.usage.clickToView": "ဆက်စပ်အကြောင်းအရာကို ကြည့်ရန် နှိပ်ပါ။", + "context.usage.view": "ဆက်စပ်အကြောင်းအရာ အသုံးပြုမှုကို ကြည့်ပါ။", + "language.en": "အင်္ဂလိပ်", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "ပေါ်တူဂီ (ဘရာဇီး)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "ဘာသာစကား", + "toast.language.description": "{{language}} သို့ ပြောင်းထားသည်။", + "toast.theme.title": "အပြင်အဆင်ကို ပြောင်းထားသည်။", + "toast.scheme.title": "အရောင်အစီအစဉ်", + "toast.workspace.enabled.title": "အလုပ်နေရာများကို ဖွင့်ထားသည်။", + "toast.workspace.enabled.description": "ယခု Git worktree များကို ဘေးဘားတွင် ပြသထားသည်။", + "toast.workspace.disabled.title": "အလုပ်နေရာများကို ပိတ်ထားသည်။", + "toast.workspace.disabled.description": "ပင်မ Git worktree ကိုသာ ဘေးဘားတွင် ပြသထားသည်။", + "toast.permissions.autoaccept.on.title": "ခွင့်ပြုချက်များကို အလိုအလျောက် လက်ခံခြင်း။", + "toast.permissions.autoaccept.on.description": "ခွင့်ပြုချက်တောင်းဆိုမှုများကို အလိုအလျောက် အတည်ပြုပေးမည်ဖြစ်သည်။", + "toast.permissions.autoaccept.off.title": "ခွင့်ပြုချက်များကို အလိုအလျောက်လက်ခံခြင်းကို ရပ်ထားသည်။", + "toast.permissions.autoaccept.off.description": "ခွင့်ပြုချက်တောင်းခံမှုများသည် အတည်ပြုချက် လိုအပ်မည်ဖြစ်သည်။", + "toast.model.none.title": "မော်ဒယ်ကို ရွေးထားခြင်းမရှိပါ။", + "toast.model.none.description": "ဤစက်ရှင်ကို အကျဉ်းချုပ်ရန် ဝန်ဆောင်မှုပေးသူကို ချိတ်ဆက်ပါ။", + "toast.file.loadFailed.title": "ဖိုင်ကို တင်ရန် မအောင်မြင်ပါ။", + "toast.file.listFailed.title": "ဖိုင်များကို စာရင်းပြုစုရန် မအောင်မြင်ပါ။", + "toast.context.noLineSelection.title": "လိုင်းရွေးချယ်မှု မရှိပါ။", + "toast.context.noLineSelection.description": "ဖိုင်တဘ်ရှိ မျဉ်းအပိုင်းအခြားကို ဦးစွာရွေးချယ်ပါ။", + "toast.session.share.copyFailed.title": "URL ကို ကလစ်ဘုတ်သို့ ကူးယူရန် မအောင်မြင်ပါ။", + "toast.session.share.success.title": "ဆက်ရှင်ကို မျှဝေထားသည်။", + "toast.session.share.success.description": "ကလစ်ဘုတ်သို့ ကူးယူထားသော URL ကို မျှဝေပါ။", + "toast.session.share.failed.title": "စက်ရှင်ကို မျှဝေရန် မအောင်မြင်ပါ။", + "toast.session.share.failed.description": "စက်ရှင်အား မျှဝေနေစဉ် အမှားအယွင်းတစ်ခု ဖြစ်ပွားခဲ့သည်။", + "toast.session.unshare.success.title": "ဆက်ရှင်ကို မျှဝေမထားပါ။", + "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": "အပ်ဒိတ် ရနိုင်ပါသည်။", + "toast.update.description": "OpenCode ({{version}}) ၏ ဗားရှင်းအသစ်ကို ယခု ထည့်သွင်းနိုင်ပါပြီ။", + "toast.update.action.installRestart": "ထည့်သွင်းပြီး ပြန်လည်စတင်ပါ။", + "toast.update.action.notYet": "မဟုတ်သေးပါ။", + "error.page.title": "တစ်ခုခု မှားသွားသည်။", + "error.page.description": "အပလီကေးရှင်းကို တင်နေစဉ် အမှားအယွင်းတစ်ခု ဖြစ်ပေါ်ခဲ့သည်။", + "error.page.description.localServerStartup": "ဒေသတွင်းဆာဗာကို စတင်စဉ်တွင် အမှားအယွင်းတစ်ခု ဖြစ်ပွားခဲ့သည်။", + "error.page.details.label": "အမှားအသေးစိတ်", + "error.page.action.restart": "ပြန်လည်စတင်ပါ။", + "error.page.action.report": "အမှားအယွင်းကို တိုင်ကြားပါ။", + "error.page.action.reported": "Error အစီရင်ခံခဲ့သည်။", + "error.page.action.exportLogs": "မှတ်တမ်းများ ထုတ်ယူရန်", + "error.page.action.checking": "စစ်ဆေးနေသည်...", + "error.page.action.checkUpdates": "အပ်ဒိတ်များအတွက် စစ်ဆေးပါ။", + "error.page.action.updateTo": "{{version}} သို့ အပ်ဒိတ်လုပ်ပါ။", + "error.page.circular": "[မြို့ပတ်]", + "error.page.report.prefix": "ဤအမှားအယွင်းကို OpenCode အဖွဲ့ထံ တိုင်ကြားပါ။\nDiscord တွင်", + "error.page.report.discord": "Discord တွင်", + "error.page.version": "ဗားရှင်း- {{version}}", + "error.dev.rootNotFound": + "အမြစ်ဒြပ်စင်ကို ရှာမတွေ့ပါ။ ၎င်းကို သင်၏ index.html တွင် ထည့်ရန် မေ့သွားပါသလား။ ဒါမှမဟုတ် id attribute က စာလုံးပေါင်းမှားနေသလား။", + "error.serverSync.connectFailed": "ဆာဗာသို့ ချိတ်ဆက်၍မရပါ။ `{{url}}` တွင်အလုပ်လုပ်နေသောဆာဗာရှိပါသလား။", + "error.serverSDK.noServerAvailable": "ဆာဗာ မရရှိနိုင်ပါ။", + "error.serverSDK.serverNotAvailable": "ဆာဗာ မရရှိနိုင်ပါ။", + "error.childStore.persistedCacheCreateFailed": "ဆက်ရှိနေသော ကက်ရှ်ကို ဖန်တီး၍မရပါ။", + "error.childStore.persistedProjectMetadataCreateFailed": "ဆက်ရှိနေသော ပရောဂျက် မက်တာဒေတာကို ဖန်တီး၍မရပါ။", + "error.childStore.persistedProjectIconCreateFailed": "ဆက်လက်တည်ရှိနေသော ပရောဂျက်သင်္ကေတကို ဖန်တီး၍မရပါ။", + "error.childStore.storeCreateFailed": "စတိုးကို ဖန်တီး၍မရပါ။", + "directory.error.invalidUrl": "URL တွင် မမှန်ကန်သော လမ်းညွှန်ချက်", + "error.chain.unknown": "အမည်မသိ အမှား", + "error.server.invalidConfiguration": "မမှန်ကန်သော ဖွဲ့စည်းမှု", + "error.chain.causedBy": "အကြောင်းရင်း-", + "error.chain.apiError": "API အမှား", + "error.chain.status": "အခြေအနေ- {{status}}", + "error.chain.retryable": "ပြန်ကြိုးစားနိုင်သည်- {{retryable}}", + "error.chain.responseBody": "တုံ့ပြန်မှုကိုယ်ထည်-\n{{body}}", + "error.chain.didYouMean": "ဆိုလိုတာက {{suggestions}}", + "error.chain.modelNotFound": "မော်ဒယ်ကို ရှာမတွေ့ပါ- {{provider}}/{{model}}", + "error.chain.checkConfig": "သင်၏ config (opencode.json) ဝန်ဆောင်မှုပေးသူ/မော်ဒယ်အမည်များကို စစ်ဆေးပါ။", + "error.chain.mcpFailed": + 'MCP ဆာဗာ "{{name}}" မအောင်မြင်ပါ။ မှတ်ချက်၊ OpenCode သည် MCP စစ်မှန်ကြောင်းအထောက်အထားမခိုင်လုံသေးပါ။', + "error.chain.providerAuthFailed": "ပံ့ပိုးသူ စစ်မှန်ကြောင်း အထောက်အထား မအောင်မြင်ပါ ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'ဝန်ဆောင်မှုပေးသူ "{{provider}}" ကို စတင်ရန် မအောင်မြင်ပါ။ အထောက်အထားများနှင့် ဖွဲ့စည်းမှုပုံစံကို စစ်ဆေးပါ။', + "error.chain.configJsonInvalid": "{{path}} ရှိ Config ဖိုင်သည် JSON(C) မမှန်ကန်ပါ", + "error.chain.configJsonInvalidWithMessage": "{{path}} ရှိ Config file သည် JSON(C): {{message}} မမှန်ပါ။", + "error.chain.configDirectoryTypo": + '{{path}} ရှိ လမ်းညွှန် "{{dir}}" သည် မမှန်ကန်ပါ။ လမ်းညွှန်ကို "{{suggestion}}" သို့ အမည်ပြောင်းပါ သို့မဟုတ် ၎င်းကို ဖယ်ရှားပါ။ ၎င်းသည် သာမာန် typo တစ်ခုဖြစ်သည်။', + "error.chain.configFrontmatterError": + "{{path}} တွင် ရှေ့အကြောင်းအရာကို ခွဲခြမ်းစိတ်ဖြာရန် မအောင်မြင်ပါ-\n{{message}}", + "error.chain.configInvalid": "{{path}} ရှိ Config ဖိုင်သည် မမှန်ကန်ပါ။", + "error.chain.configInvalidWithMessage": "{{path}} ရှိ Config ဖိုင်သည် မမှန်ကန်ပါ- {{message}}", + "notification.permission.title": "ခွင့်ပြုချက် လိုအပ်သည်။", + "notification.permission.description": "{{sessionTitle}} တွင် {{projectName}} ခွင့်ပြုချက် လိုအပ်သည်။", + "notification.question.title": "မေးခွန်း", + "notification.question.description": "{{sessionTitle}} {{projectName}} တွင် မေးခွန်းတစ်ခုရှိသည်။", + "notification.action.goToSession": "စက်ရှင်သို့ သွားပါ။", + "notification.session.responseReady.title": "တုံ့ပြန်မှု အဆင်သင့်ဖြစ်ပါပြီ။", + "notification.session.error.title": "စက်ရှင် အမှား", + "notification.session.error.fallbackDescription": "အမှားအယွင်းတစ်ခု ဖြစ်ပွားခဲ့သည်။", + "home.recentProjects": "လတ်တလော ပရောဂျက်များ", + "home.empty.title": "လတ်တလော ပရောဂျက်များ မရှိပါ။", + "home.empty.description": "ဒေသခံပရောဂျက်တစ်ခုကို ဖွင့်လှစ်ခြင်းဖြင့် စတင်လိုက်ပါ။", + "home.title": "ပင်မ", + "home.projects": "ပရောဂျက်များ", + "home.project.add": "ပရောဂျက်ကို ထည့်ပါ။", + "home.recentlyClosed": "မကြာသေးမီက ပိတ်ထားသည်။", + "home.server.collapse": "ဆာဗာပရောဂျက်များကို ခေါက်သိမ်းပါ။", + "home.server.expand": "ဆာဗာ ပရောဂျက်များကို ချဲ့ထွင်ပါ။", + "home.sessions.search.placeholder": "ရှာဖွေမှုကဏ္ဍများ", + "home.sessions.search.placeholder.scoped": "{{scope}} ရှိ ဆက်ရှင်များကို ရှာဖွေပါ။", + "home.sessions.search.sessions": "ဆက်ရှင်များ", + "home.sessions.search.noResults": "{{query}} အတွက် ဆက်ရှင်များ မတွေ့ပါ။", + "home.sessions.empty": "ဤနေရာတွင် ဘာမှမရှိသေးပါ။", + "home.sessions.empty.description": "စတင်ရန် စက်ရှင်တစ်ခုကို ဖန်တီးပါ။", + "home.sessions.group.today": "ယနေ့", + "home.sessions.group.yesterday": "မနေ့က", + "home.sessions.group.older": "အသက်ကြီးသည်။", + "home.providerTip": + "Claude၊ GPT၊ Gemini စသည်တို့အပါအဝင် အခြားမော်ဒယ်များကို အသုံးပြုရန် 75+ ဝန်ဆောင်မှုပေးသူများထံ ချိတ်ဆက်ပါ။", + "session.tab.session": "အပိုင်း", + "session.tab.review": "သုံးသပ်ချက်", + "session.tab.context": "ဆက်စပ်အကြောင်းအရာ", + "session.tab.unknown": "အမည်မသိ ဆက်ရှင်", + "session.panel.reviewAndFiles": "ပြန်လည်သုံးသပ်ခြင်းနှင့် ဖိုင်များ", + "session.error.notFound": "ဤစက်ရှင်ကို ရှာမတွေ့ပါ။", + "session.error.notFound.description": "ဤတဘ်သည် ဤဆာဗာတွင်မရှိတော့သော စက်ရှင်တစ်ခုကို ညွှန်ပြသည်။", + "session.error.notFound.closeTab": "တဘ်ကို ပိတ်ပါ။", + "session.error.serverConnection": "ဤဆာဗာသို့ ချိတ်ဆက်၍မရပါ။", + "session.review.filesChanged": "ဖိုင်များကို {{count}} ပြောင်းထားသည်။", + "session.review.change.one": "ပြောင်းလဲခြင်း။", + "session.review.change.other": "ပြောင်းလဲမှုများ", + "session.review.loadingChanges": "အပြောင်းအလဲများကို ဖွင့်နေသည်...", + "session.review.empty": "ဤစက်ရှင်တွင် ပြောင်းလဲမှုမရှိသေးပါ။", + "session.review.noVcs": "မည်သည့် Git ဗားရှင်းထိန်းချုပ်မှုစနစ်မှ ရှာမတွေ့ပါ၊ အပြောင်းအလဲများကို မပြသပါ။", + "session.review.noVcs.createGit.title": "Git repository တစ်ခုဖန်တီးပါ။", + "session.review.noVcs.createGit.description": "ဤပရောဂျက်ရှိ အပြောင်းအလဲများကို ခြေရာခံ၊ သုံးသပ်ပြီး ပြန်ဖျက်ပါ။", + "session.review.noVcs.createGit.actionLoading": "Git repository ကို ဖန်တီးနေသည်...", + "session.review.noVcs.createGit.action": "Git repository ဖန်တီးပါ။", + "session.review.noSnapshot": + "Snapshot ခြေရာခံခြင်းကို config တွင်ပိတ်ထားသည်၊ ထို့ကြောင့် စက်ရှင်ပြောင်းလဲမှုများကို မရရှိနိုင်ပါ", + "session.review.noChanges": "အပြောင်းအလဲမရှိပါ။", + "session.review.noUncommittedChanges": "ကတိကဝတ်မပြုထားသော အပြောင်းအလဲများ မရှိသေးပါ။", + "session.review.noBranchChanges": "ဌာနခွဲပြောင်းလဲမှုမရှိသေးပါ။", + "session.files.selectToOpen": "ဖွင့်ရန် ဖိုင်တစ်ခုကို ရွေးပါ။", + "session.files.all": "ဖိုင်အားလုံး", + "session.files.empty": "ဖိုင်မရှိပါ။", + "session.files.binaryContent": "Binary ဖိုင် (အကြောင်းအရာကို ပြသ၍မရပါ)", + "session.messages.renderEarlier": "အစောပိုင်းစာတိုများကို တင်ဆက်ပါ။", + "session.messages.loadingEarlier": "အစောပိုင်းမက်ဆေ့ချ်များကို တင်နေသည်...", + "session.messages.loadEarlier": "အစောပိုင်းမက်ဆေ့ချ်များကို တင်ပါ။", + "session.messages.loading": "မက်ဆေ့ချ်များကို ဖွင့်နေသည်...", + "session.messages.jumpToLatest": "နောက်ဆုံးသို့ ခုန်ပါ။", + "session.context.addToContext": "{{selection}} ကို ဆက်စပ်အကြောင်းအရာထဲ ထည့်ပါ။", + "session.todo.title": "လုပ်စရာများ", + "session.todo.collapse": "ခေါက်သိမ်းပါ။", + "session.todo.expand": "ချဲ့ပါ။", + "session.todo.progress": "{{total}} todos များ၏ {{done}} ပြီးပါပြီ။", + "session.question.progress": "{{total}} မေးခွန်းများ၏ {{current}}", + "session.question.minimize": "မေးခွန်းကို လျှော့ပါ။", + "session.question.restore": "မေးခွန်းကို ပြန်ယူပါ။", + "session.question.pending.one": "{{count}} ဆိုင်းငံ့မေးခွန်း", + "session.question.pending.other": "{{count}} ဆိုင်းငံ့မေးခွန်းများ", + "session.followupDock.summary.one": "{{count}} တန်းစီနေသည့် မက်ဆေ့ဂျ်", + "session.followupDock.summary.other": "{{count}} တန်းစီထားသော စာတိုများ", + "session.followupDock.sendNow": "ယခု ပို့ပါ။", + "session.followupDock.edit": "တည်းဖြတ်ပါ။", + "session.followupDock.collapse": "တန်းစီထားသော စာများကို ခေါက်သိမ်းပါ။", + "session.followupDock.expand": "တန်းစီထားသောစာများကို ချဲ့ပါ။", + "session.revertDock.summary.one": "{{count}} သည် မက်ဆေ့ဂျ်ကို ပြန်ပြောင်းထားသည်။", + "session.revertDock.summary.other": "{{count}} သည် မက်ဆေ့ဂျ်များကို ပြန်ဖွင့်ခဲ့သည်။", + "session.revertDock.collapse": "ပြန်လှန်ထားသော စာတိုများကို ခေါက်သိမ်းပါ။", + "session.revertDock.expand": "ပြန်လှန်ထားသော စာတိုများကို ချဲ့ပါ။", + "session.revertDock.restore": "မက်ဆေ့ဂျ်ကို ပြန်ယူပါ။", + "session.new.title": "မည်သည့်အရာကိုမဆို တည်ဆောက်ပါ။", + "session.new.project.new": "ပရောဂျက်အသစ်", + "session.new.project.search": "ရှာဖွေရေး ပရောဂျက်များ", + "session.new.project.add": "ပရောဂျက်ကို ထည့်ပါ။", + "session.new.worktree.main": "ပင်မဌာနခွဲ", + "session.new.worktree.mainWithBranch": "ပင်မဌာနခွဲ ({{branch}})", + "session.new.worktree.create": "Git worktree အသစ် ဖန်တီးပါ။", + "session.new.workspace.runIn": "စက်ရှင်ကို ဖွင့်ပါ။", + "session.new.workspace.triggerLocal": "ပြည်တွင်း", + "session.new.workspace.local": "စက်တွင်းသိုလှောင်မှု", + "session.new.workspace.existing": "အလုပ်နေရာ…", + "session.new.git.none": "Git မရှိပါ။", + "session.new.lastModified": "နောက်ဆုံးမွမ်းမံထားသည်။", + "session.header.search.placeholder": "{{project}} ကိုရှာပါ။", + "session.header.searchFiles": "ဖိုင်များကို ရှာဖွေပါ။", + "session.header.openIn": "ဖွင့်ပါ။", + "session.header.open.action": "{{app}} ကိုဖွင့်ပါ။", + "session.header.open.ariaLabel": "{{app}} တွင် ဖွင့်ပါ။", + "session.header.open.menu": "ရွေးစရာများကို ဖွင့်ပါ။", + "session.header.open.copyPath": "လမ်းကြောင်းကို ကူးယူပါ။", + "session.header.open.finder": "ရှာဖွေသူ", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "ဖိုင်မန်နေဂျာ", + "session.header.reveal.finder": "Finder တွင် ထုတ်ပြပါ။", + "session.header.reveal.fileExplorer": "File Explorer တွင် ထုတ်ပြပါ။", + "session.header.reveal.containingFolder": "ပါဝင်သောဖိုင်တွဲကိုဖွင့်ပါ။", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "အခြေအနေ", + "status.popover.ariaLabel": "ဆာဗာဖွဲ့စည်းပုံများ", + "status.popover.tab.servers": "ဆာဗာများ", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "ပလပ်အင်များ", + "status.popover.action.manageServers": "ဆာဗာများကို စီမံပါ။", + "session.share.popover.title": "ဝဘ်ပေါ်တွင် လွှင့်တင်ပါ။", + "session.share.popover.description.shared": + "ဤစက်ရှင်သည် ဝဘ်ပေါ်တွင် အများသူငှာဖြစ်သည်။ ၎င်းကို လင့်ခ်ရှိသူတိုင်း အသုံးပြုနိုင်သည်။", + "session.share.popover.description.unshared": + "စက်ရှင်ကို ဝဘ်ပေါ်တွင် လူသိရှင်ကြား မျှဝေပါ။ ၎င်းကို လင့်ခ်ရှိသူတိုင်း ဝင်ရောက်ကြည့်ရှုနိုင်မည်ဖြစ်သည်။", + "session.share.action.share": "မျှဝေပါ။", + "session.share.action.publish": "ထုတ်ဝေသည်။", + "session.share.action.publishing": "ထုတ်ဝေနေသည်...", + "session.share.action.unpublish": "ထုတ်ဝေမှုမှ ပြန်ဖြုတ်ပါ။", + "session.share.action.unpublishing": "ထုတ်ဝေမှုမှ ပြန်ဖြုတ်နေသည်...", + "session.share.action.view": "ကြည့်ရန်", + "session.share.copy.copied": "ကူးယူထားသည်။", + "session.share.copy.copyLink": "လင့်ခ်ကို ကူးယူပါ။", + "lsp.tooltip.none": "LSP ဆာဗာများ မရှိပါ။", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "အချက်ပြမှုကို ဖွင့်နေသည်...", + "terminal.loading": "Terminal ကို ဖွင့်နေသည်...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Terminal ကို ပိတ်ပါ", + "terminal.connectionLost.title": "ချိတ်ဆက်မှု ပြတ်တောက်သွားသည်။", + "terminal.connectionLost.abnormalClose": "WebSocket ကို ပုံမှန်မဟုတ်စွာ ပိတ်ထားသည်- {{code}}", + "terminal.connectionLost.description": + "Terminal ချိတ်ဆက်မှု ပြတ်တောက်သွားသည်။ ဆာဗာ ပြန်လည်စတင်သည့်အခါ ၎င်းသည် ဖြစ်ပေါ်လာနိုင်သည်။", + "terminal.connectTicket.csrfError": + "PTY ချိတ်ဆက်မှုလက်မှတ်ကို မူရင်း သို့မဟုတ် CSRF စစ်ဆေးမှုများဖြင့် ပယ်ချထားသည်။ ဆာဗာ CORS config ကိုစစ်ဆေးပါ။", + "terminal.connectTicket.statusError": "PTY ချိတ်ဆက်မှုလက်မှတ် {{status}} ဖြင့် မအောင်မြင်ပါ။", + "titlebar.update": "အပ်ဒိတ်", + "titlebar.updateVersion": "{{version}} ကို အပ်ဒိတ်လုပ်ပါ။", + "common.closeTab": "တဘ်ကို ပိတ်ပါ။", + "common.dismiss": "ပယ်ရန်", + "common.moreCountSuffix": "(+{{count}} နောက်ထပ်)", + "common.requestFailed": "တောင်းဆိုမှု မအောင်မြင်ပါ။", + "common.moreOptions": "နောက်ထပ် ရွေးချယ်စရာများ", + "common.learnMore": "ပိုမိုလေ့လာရန်", + "common.rename": "အမည်ပြောင်း", + "common.export": "တင်ပို့ခြင်း။", + "common.reset": "ပြန်လည်သတ်မှတ်ပါ။", + "common.archive": "မှတ်တမ်း", + "common.delete": "ဖျက်ပါ", + "common.close": "ပိတ်ပါ။", + "common.edit": "တည်းဖြတ်ရန်", + "common.loadMore": "နောက်ထပ် တင်ပါ။", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "အမည်မသိ", + "common.time.justNow": "အခုပဲ", + "common.time.minutesAgo.short": "{{count}}m လွန်ခဲ့သော", + "common.time.hoursAgo.short": "{{count}}h လွန်ခဲ့သော", + "common.time.daysAgo.short": "{{count}}d လွန်ခဲ့သော", + "sidebar.menu.toggle": "မီနူးကို ပြောင်းရန်", + "sidebar.nav.projectsAndSessions": "ပရောဂျက်များနှင့် ဆက်ရှင်များ", + "sidebar.settings": "ဆက်တင်များ", + "sidebar.help": "အကူအညီ", + "sidebar.workspaces.enable": "အလုပ်နေရာများကို ဖွင့်ပါ။", + "sidebar.workspaces.disable": "အလုပ်နေရာများကို ပိတ်ပါ။", + "sidebar.gettingStarted.title": "စတင်ခြင်း။", + "sidebar.gettingStarted.line1": "OpenCode တွင် အခမဲ့မော်ဒယ်များ ပါဝင်သောကြောင့် သင်ချက်ချင်းစတင်နိုင်သည်။", + "sidebar.gettingStarted.line2": + "မော်ဒယ်များ၊ inc ကိုအသုံးပြုရန် မည်သည့်ပံ့ပိုးသူကိုမဆို ချိတ်ဆက်ပါ။ Claude၊ GPT၊ Gemini စသဖြင့်", + "sidebar.project.recentSessions": "လတ်တလော စက်ရှင်များ", + "sidebar.project.viewAllSessions": "ဆက်ရှင်များအားလုံးကို ကြည့်ရှုပါ။", + "sidebar.project.clearNotifications": "အကြောင်းကြားချက်များကို ရှင်းလင်းပါ။", + "sidebar.empty.title": "မည်သည့်ပရောဂျက်မှ ဖွင့်ထားခြင်းမရှိပါ။", + "sidebar.empty.description": "စတင်ရန် ပရောဂျက်တစ်ခုကို ဖွင့်ပါ။", + "debugBar.ariaLabel": "ဖွံ့ဖြိုးတိုးတက်မှု စွမ်းဆောင်ရည် အဖြေရှာခြင်း", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "နောက်ဆုံးပြီးစီးခဲ့သော လမ်းကြောင်းအပြောင်းအရွှေ့သည် အခြေချပြီးနောက် ပထမဆေးခြယ်ခြင်းအထိ တိုင်းတာသည့် စက်ရှင်စာမျက်နှာကိုထိခြင်း၊", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "လွန်ခဲ့သည့် 5 စက္ကန့်အတွင်း တစ်စက္ကန့်လျှင် ရွေ့လျားနေသောဘောင်များ။", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "ပြီးခဲ့သည့် 5 စက္ကန့်အတွင်း အဆိုးဆုံးဘောင်အချိန်။", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "နောက်ဆုံး 5 စက္ကန့်အတွင်း 32ms ကျော်ဘောင်များ။", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "ပြီးခဲ့သော 5 စက္ကန့်အတွင်း ပိတ်ဆို့ထားသော အချိန်နှင့် ကြာရှည်မှုအရေအတွက်။ အများဆုံးလုပ်ဆောင်စရာ- {{max}}။", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "ပြီးခဲ့သော 5 စက္ကန့်အတွင်း အဆိုးရွားဆုံး ထည့်သွင်းမှုနှောင့်နှေးမှုကို တွေ့ရှိခဲ့သည်။", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "ပြီးခဲ့သော 5 စက္ကန့်အတွင်း အနီးစပ်ဆုံး အပြန်အလှန်တုံ့ပြန်မှုကြာချိန်။ ၎င်းသည် INP နှင့်တူသည်၊ တရားဝင် Web Vitals INP မဟုတ်ပါ။", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "လက်ရှိ အက်ပ်သက်တမ်းအတွက် စုစည်းထားသော အပြင်အဆင် အပြောင်းအလဲ။", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "JS heap နှင့် heap ကန့်သတ်ချက်ကို အသုံးပြုထားသည်။ Chromium သာ", + "debugBar.mem.tip": "JS heap နှင့် heap ကန့်သတ်ချက်ကို အသုံးပြုထားသည်။ {{limit}} ၏ {{used}}", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": + "အပြန်အလှန်တုံ့ပြန်မှုရှိသော ဒြပ်စင်များအားလုံးတွင် အာရုံစူးစိုက်မှုပုံစံများကို တွန်းအားပေးပါ။", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": + "ရွေးချယ်ထားသောဘာသာစကားကို မပြောင်းလဲဘဲ အက်ပ်အပြင်အဆင် လမ်းညွှန်ချက်အပြည့်အစုံကို တွန်းအားပေးပါ။", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "ဒက်စ်တော့", + "settings.section.server": "ဆာဗာ", + "settings.tab.general": "အထွေထွေ", + "settings.tab.shortcuts": "ဖြတ်လမ်းများ", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL ပေါင်းစည်းမှု", + "settings.desktop.wsl.description": "Windows တွင် WSL အတွင်းရှိ OpenCode ဆာဗာကို ဖွင့်ပါ။", + "settings.general.section.appearance": "အသွင်အပြင်", + "settings.general.section.advanced": "အဆင့်မြင့်", + "settings.general.section.notifications": "စနစ်အကြောင်းကြားချက်များ", + "settings.general.section.updates": "အပ်ဒိတ်များ", + "settings.general.section.sounds": "အသံသက်ရောက်မှု", + "settings.general.section.feed": "ကျွေးမွေးခြင်း။", + "settings.general.section.display": "မျက်နှာပြင်", + "settings.general.row.language.title": "ဘာသာစကား", + "settings.general.row.language.description": "OpenCode အတွက် ဖော်ပြသည့် ဘာသာစကားကို ပြောင်းပါ။", + "settings.general.row.shell.title": "Terminal Shell", + "settings.general.row.shell.description": "terminal နှင့် agent tools မှအသုံးပြုသော Shell", + "settings.general.row.shell.autoDefault": "အလိုအလျောက် (ပုံသေ)", + "settings.general.row.shell.terminalOnly": "terminal သာ", + "settings.general.row.appearance.title": "အသွင်အပြင်", + "settings.general.row.appearance.description": "သင့်စက်ပေါ်တွင် OpenCode ကို မည်သို့မြင်သည်ကို စိတ်ကြိုက်ပြင်ဆင်ပါ။", + "settings.general.row.colorScheme.title": "အရောင်အစီအစဉ်", + "settings.general.row.colorScheme.description": + "OpenCode သည် စနစ်၊ အလင်း သို့မဟုတ် အမှောင် အပြင်အဆင်ကို လိုက်နာခြင်း ရှိ၊ မရှိကို ရွေးပါ။", + "settings.general.row.theme.title": "အပြင်အဆင်", + "settings.general.row.theme.description": "OpenCode ကို ဘယ်လိုပုံစံနဲ့ စိတ်ကြိုက်လုပ်မလဲ။", + "settings.general.row.font.title": "ကုဒ်ဖောင့်", + "settings.general.row.font.description": "ကုဒ်တုံးများတွင် အသုံးပြုသည့် ဖောင့်ကို စိတ်ကြိုက်ပြင်ဆင်ပါ။", + "settings.general.row.terminalFont.title": "Terminal ဖောင့်", + "settings.general.row.terminalFont.description": "terminal တွင်အသုံးပြုသောဖောင့်ကို စိတ်ကြိုက်ပြင်ဆင်ပါ။", + "settings.general.row.uiFont.title": "UI ဖောင့်", + "settings.general.row.uiFont.description": "အင်တာဖေ့စ်တစ်လျှောက်သုံးဖောင့်ကို စိတ်ကြိုက်ပြင်ဆင်ပါ။", + "settings.general.row.followup.title": "နောက်ဆက်တွဲအပြုအမူ", + "settings.general.row.followup.description": + "နောက်ဆက်တွဲအချက်မှာ ချက်ချင်းပဲ့ကိုင်သလား သို့မဟုတ် တန်းစီစောင့်ဆိုင်းခြင်းရှိမရှိကို ရွေးပါ။", + "settings.general.row.followup.option.queue": "တန်းစီ", + "settings.general.row.followup.option.steer": "ခုတ်မောင်းပါ။", + "settings.general.row.showFileTree.title": "ဖိုင်သစ်", + "settings.general.row.showFileTree.description": "စက်ရှင်များတွင် ဖိုင်သစ်အကန့်ကို ပြပါ။", + "settings.general.row.showNavigation.title": "လမ်းညွှန်မှု ထိန်းချုပ်မှုများ", + "settings.general.row.showNavigation.description": + "ဒက်စ်တော့ ခေါင်းစဉ်ဘားတွင် နောက်နှင့် ရှေ့သို့ ခလုတ်များကို ပြပါ။", + "settings.general.row.showSearch.title": "အမိန့်များစာရင်း", + "settings.general.row.showSearch.description": "ခေါင်းစဉ်ဘားရှိ ရှာဖွေမှုနှင့် အမိန့်ပေးချက် ခလုတ်ကို ပြပါ။", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "ဒက်စ်တော့ခေါင်းစဉ်ဘားရှိ terminal ခလုတ်ကိုပြသပါ။", + "settings.general.row.showStatus.title": "ဆာဗာအခြေအနေ", + "settings.general.row.showStatus.description": "ခေါင်းစဉ်ဘားရှိ ဆာဗာအခြေအနေခလုတ်ကို ပြပါ။", + "settings.general.row.mobileTitlebarBottom.title": "အောက်ခြေလမ်းညွှန်", + "settings.general.row.mobileTitlebarBottom.description": + "မိုဘိုင်းတွင် စခရင်၏အောက်ခြေတွင် ခေါင်းစဉ်ဘားနှင့် ဆက်ရှင်တက်ဘ်များကို ထားရှိပါ။", + "settings.general.row.showCustomAgents.title": "အေးဂျင့်ကို ပြပါ။", + "settings.general.row.showCustomAgents.description": + "တေးရေးဆရာရှိ အေးဂျင့်များအကြား ပြောင်းပါ။ ဝှက်ထားသည့်အခါ၊ Build အေးဂျင့်အဖြစ် သတ်မှတ်သည်။", + "settings.general.row.reasoningSummaries.title": "ကျိုးကြောင်းဆင်ခြင်ခြင်း အနှစ်ချုပ်များကို ပြပါ။", + "settings.general.row.reasoningSummaries.description": + "အချိန်ဇယားတွင် မော်ဒယ် ကျိုးကြောင်းဆင်ခြင်ခြင်း အနှစ်ချုပ်များကို ပြသပါ။", + "settings.general.row.shellToolPartsExpanded.title": "shell tool အစိတ်အပိုင်းများကို ချဲ့ပါ။", + "settings.general.row.shellToolPartsExpanded.description": + "အချိန်ဇယားတွင် ပုံသေဖြင့် ချဲ့ထားသော shell tool အစိတ်အပိုင်းများကို ပြပါ။", + "settings.general.row.editToolPartsExpanded.title": "တည်းဖြတ်ကိရိယာအစိတ်အပိုင်းများကို ချဲ့ထွင်ပါ။", + "settings.general.row.editToolPartsExpanded.description": + "အချိန်ဇယားတွင် ပုံသေဖြင့် ချဲ့ထွင်ထားသော တည်းဖြတ်ခြင်း၊ ရေးသားခြင်းနှင့် ဖာထေးခြင်း ကိရိယာအစိတ်အပိုင်းများကို ပြသပါ။", + "settings.general.row.newInterface.title": "အပြင်အဆင်အသစ်", + "settings.general.row.newInterface.badge": "အသစ်", + "settings.general.row.newInterface.description": + "တက်ဘ်အသစ်များနှင့် အိမ်အပြင်အဆင်ကို အသုံးပြုပါ။ အပြင်အဆင်များကြားတွင် အချိန်အကန့်အသတ်ဖြင့် ပြောင်းပါ။", + "settings.general.row.newInterfaceNotice.title": "သင်သည် ယခုအခါ အပြင်အဆင်အသစ်ကို အသုံးပြုနေပါသည်။", + "settings.general.row.newInterfaceNotice.description": "ယခင်အပြင်အဆင်ကို မရနိုင်တော့ပါ။", + "settings.general.row.newInterfaceNotice.dismiss": "ပယ်ရန်", + "settings.general.row.pinchZoom.title": "ဇူးမ်ချဲ့ရန် ဖိပါ။", + "settings.general.row.pinchZoom.description": + "ဇူးမ်လုပ်ရန် trackpad pinch နှင့် Ctrl-scroll gestures ကို ခွင့်ပြုပါ။", + "settings.general.row.wayland.title": "ဇာတိ Wayland ကို သုံးပါ။", + "settings.general.row.wayland.description": "Wayland ရှိ X11 လှည့်ကွက်ကို ပိတ်ပါ။ ပြန်လည်စတင်ရန် လိုအပ်သည်။", + "settings.general.row.wayland.tooltip": + "Linux တွင် ရောစပ်ထားသော refresh-rate မော်နီတာများနှင့်အတူ၊ မူရင်း Wayland သည် ပိုမိုတည်ငြိမ်နိုင်သည်။", + "settings.general.row.releaseNotes.title": "ထုတ်ဝေမှုမှတ်စုများ", + "settings.general.row.releaseNotes.description": "အပ်ဒိတ်များပြီးနောက် ပေါ့ပ်အပ်အသစ်များကို ပြပါ။", + "settings.updates.row.startup.title": "စတင်ခြင်းတွင် အပ်ဒိတ်များကို စစ်ဆေးပါ။", + "settings.updates.row.startup.description": "OpenCode စတင်သည့်အခါ အပ်ဒိတ်များအတွက် အလိုအလျောက်စစ်ဆေးပါ။", + "settings.updates.row.check.title": "အပ်ဒိတ်များအတွက် စစ်ဆေးပါ။", + "settings.updates.row.check.description": "အပ်ဒိတ်များအတွက် ကိုယ်တိုင်စစ်ဆေးပြီး ရနိုင်လျှင် ထည့်သွင်းပါ။", + "settings.updates.action.checkNow": "ယခုစစ်ဆေးပါ။", + "settings.updates.action.checking": "စစ်ဆေးနေသည်...", + "settings.updates.action.downloading": "ဒေါင်းလုဒ်လုပ်နေသည်...", + "settings.updates.action.installing": "ထည့်သွင်းနေသည်...", + "settings.updates.toast.latest.title": "သင်သည် ခေတ်မီနေပါသည်။", + "settings.updates.toast.latest.description": "သင်သည် OpenCode ၏ နောက်ဆုံးဗားရှင်းကို အသုံးပြုနေပါသည်။", + "sound.option.none": "မရှိပါ။", + "sound.option.alert01": "သတိပေးချက် ၀၁", + "sound.option.alert02": "သတိပေးချက် ၀၂", + "sound.option.alert03": "သတိပေးချက် ၀၃", + "sound.option.alert04": "သတိပေးချက် ၀၄", + "sound.option.alert05": "သတိပေးချက် ၀၅", + "sound.option.alert06": "သတိပေးချက် ၀၆", + "sound.option.alert07": "သတိပေးချက် ၀၇", + "sound.option.alert08": "သတိပေးချက် ၀၈", + "sound.option.alert09": "သတိပေးချက် ၀၉", + "sound.option.alert10": "သတိပေးချက် ၁၀", + "sound.option.bipbop01": "Bip-bop ၀၁", + "sound.option.bipbop02": "Bip-bop ၀၂", + "sound.option.bipbop03": "Bip-bop ၀၃", + "sound.option.bipbop04": "Bip-bop ၀၄", + "sound.option.bipbop05": "Bip-bop ၀၅", + "sound.option.bipbop06": "Bip-bop ၀၆", + "sound.option.bipbop07": "Bip-bop ၀၇", + "sound.option.bipbop08": "Bip-bop ၀၈", + "sound.option.bipbop09": "Bip-bop ၀၉", + "sound.option.bipbop10": "Bip-bop ၁၀", + "sound.option.staplebops01": "Staplebops ၀၁", + "sound.option.staplebops02": "Staplebops ၀၂", + "sound.option.staplebops03": "Staplebops ၀၃", + "sound.option.staplebops04": "Staplebops ၀၄", + "sound.option.staplebops05": "Staplebops ၀၅", + "sound.option.staplebops06": "Staplebops ၀၆", + "sound.option.staplebops07": "Staplebops ၀၇", + "sound.option.nope01": "မဟုတ်ဘူး ၀၁", + "sound.option.nope02": "မဟုတ်ဘူး ၀၂", + "sound.option.nope03": "မဟုတ်ဘူး ၀၃", + "sound.option.nope04": "မဟုတ်ဘူး ၀၄", + "sound.option.nope05": "မဟုတ်ဘူး ၀၅", + "sound.option.nope06": "မဟုတ်ဘူး ၀၆", + "sound.option.nope07": "မဟုတ်ဘူး ၀၇", + "sound.option.nope08": "မဟုတ်ဘူး ၀၈", + "sound.option.nope09": "မဟုတ်ဘူး ၀၉", + "sound.option.nope10": "မဟုတ်ဘူး ၁၀", + "sound.option.nope11": "မဟုတ်ဘူး ၁၁", + "sound.option.nope12": "မဟုတ်ဘူး ၁၂", + "sound.option.yup01": "ဟုတ် ၀၁", + "sound.option.yup02": "ဟုတ် ၀၂", + "sound.option.yup03": "ဟုတ် ၀၃", + "sound.option.yup04": "ဟုတ် ၀၄", + "sound.option.yup05": "အင်း ၀၅", + "sound.option.yup06": "အင်း ၀၆", + "settings.general.notifications.agent.title": "အေးဂျင့်", + "settings.general.notifications.agent.description": + "အေးဂျင့် ပြီးသွားသောအခါ သို့မဟုတ် သတိထားရန် လိုအပ်သည့်အခါ စနစ်သတိပေးချက်ကို ပြပါ။", + "settings.general.notifications.permissions.title": "ခွင့်ပြုချက်များ", + "settings.general.notifications.permissions.description": "ခွင့်ပြုချက်လိုအပ်သည့်အခါ စနစ်သတိပေးချက်ကို ပြပါ။", + "settings.general.notifications.errors.title": "အမှားများ", + "settings.general.notifications.errors.description": "အမှားအယွင်းတစ်ခုဖြစ်ပေါ်သည့်အခါ စနစ်သတိပေးချက်ကို ပြသပါ။", + "settings.general.sounds.agent.title": "အေးဂျင့်", + "settings.general.sounds.agent.description": + "အေးဂျင့်ပြီးသွားသောအခါ သို့မဟုတ် အာရုံစူးစိုက်မှုလိုအပ်သည့်အခါ အသံဖွင့်ပါ။", + "settings.general.sounds.permissions.title": "ခွင့်ပြုချက်များ", + "settings.general.sounds.permissions.description": "ခွင့်ပြုချက်လိုအပ်သည့်အခါ အသံဖွင့်ပါ။", + "settings.general.sounds.errors.title": "အမှားများ", + "settings.general.sounds.errors.description": "အမှားအယွင်းတစ်ခုဖြစ်ပေါ်သောအခါ အသံဖွင့်ပါ။", + "settings.shortcuts.title": "ကီးဘုတ်ဖြတ်လမ်းများ", + "settings.shortcuts.reset.button": "မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်ပါ။", + "settings.shortcuts.reset.toast.title": "ဖြတ်လမ်းလင့်ခ်များကို ပြန်လည်သတ်မှတ်သည်။", + "settings.shortcuts.reset.toast.description": "ကီးဘုတ်ဖြတ်လမ်းများကို မူရင်းအတိုင်း ပြန်လည်သတ်မှတ်ထားသည်။", + "settings.shortcuts.conflict.title": "ဖြတ်လမ်းလင့်ခ်ကို အသုံးပြုပြီးဖြစ်သည်။", + "settings.shortcuts.conflict.description": "{{keybind}} ကို {{titles}} တွင် တာဝန်ပေးထားပြီးဖြစ်သည်။", + "settings.shortcuts.unassigned": "တာဝန်မယူထား", + "settings.shortcuts.pressKeys": "ခလုတ်များကို နှိပ်ပါ။", + "settings.shortcuts.search.placeholder": "ရှာရန် ဖြတ်လမ်းများ", + "settings.shortcuts.search.empty": "ဖြတ်လမ်းလင့်ခ်များ မတွေ့ပါ။", + "settings.shortcuts.group.general": "အထွေထွေ", + "settings.shortcuts.group.session": "အပိုင်း", + "settings.shortcuts.group.navigation": "အညွှန်း", + "settings.shortcuts.group.modelAndAgent": "မော်ဒယ်နှင့် အေးဂျင့်", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "အချက်ပြပါ။", + "settings.providers.title": "ဝန်ဆောင်မှုပေးသူများ", + "settings.providers.description": "ဝန်ဆောင်မှုပေးသူ ဆက်တင်များကို ဤနေရာတွင် ပြင်ဆင်သတ်မှတ်နိုင်ပါမည်။", + "settings.providers.section.connected": "ချိတ်ဆက်ထားသော ဝန်ဆောင်မှုပေးသူများ", + "settings.providers.connected.empty": "ချိတ်ဆက်ပေးသူ မရှိပါ။", + "settings.providers.connected.environmentDescription": "သင့်ပတ်ဝန်းကျင် ကိန်းရှင်များမှ ချိတ်ဆက်ထားသည်။", + "settings.providers.section.popular": "လူကြိုက်များသော ဝန်ဆောင်မှုပေးသူများ", + "settings.providers.custom.description": "အခြေခံ URL ဖြင့် OpenAI-သဟဇာတ ပံ့ပိုးပေးသူကို ထည့်ပါ။", + "settings.providers.tag.environment": "ပတ်ဝန်းကျင်", + "settings.providers.tag.config": "ပြင်ဆင်မှု", + "settings.providers.tag.custom": "စိတ်ကြိုက်", + "settings.providers.tag.other": "အခြား", + "settings.models.title": "မော်ဒယ်များ", + "settings.models.description": "မော်ဒယ်လ်ဆက်တင်များကို ဤနေရာတွင် စီစဉ်သတ်မှတ်နိုင်မည်ဖြစ်သည်။", + "settings.agents.title": "အေးဂျင့်များ", + "settings.agents.description": "အေးဂျင့်ဆက်တင်များကို ဤနေရာတွင် ပြင်ဆင်သတ်မှတ်နိုင်ပါမည်။", + "settings.commands.title": "အမိန့်များ", + "settings.commands.description": "ကွန်မန်းဆက်တင်များကို ဤနေရာတွင် စီစဉ်သတ်မှတ်နိုင်မည်ဖြစ်သည်။", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP ဆက်တင်များကို ဤနေရာတွင် ပြင်ဆင်သတ်မှတ်နိုင်ပါသည်။", + "settings.permissions.title": "ခွင့်ပြုချက်များ", + "settings.permissions.description": "မူရင်းအတိုင်း ဆာဗာအသုံးပြုနိုင်သည့် ကိရိယာများကို ထိန်းချုပ်ပါ။", + "settings.permissions.section.tools": "ကိရိယာများ", + "settings.permissions.toast.updateFailed.title": "ခွင့်ပြုချက်များကို အပ်ဒိတ်လုပ်ရန် မအောင်မြင်ပါ။", + "settings.permissions.action.allow": "ခွင့်ပြုပါ။", + "settings.permissions.action.ask": "မေး", + "settings.permissions.action.deny": "ငြင်းဆို", + "settings.permissions.tool.read.title": "ဖတ်ပါ။", + "settings.permissions.tool.read.description": "ဖိုင်တစ်ခုဖတ်ခြင်း (ဖိုင်လမ်းကြောင်းနှင့် ကိုက်ညီသည်)", + "settings.permissions.tool.edit.title": "တည်းဖြတ်ပါ။", + "settings.permissions.tool.edit.description": + "တည်းဖြတ်မှုများ၊ ရေးသားမှုများနှင့် ဖာထေးမှုများအပါအဝင် ဖိုင်များကို မွမ်းမံပါ။", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "glob ပုံစံများကို အသုံးပြု၍ ဖိုင်များကို ယှဉ်တွဲပါ။", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "ပုံမှန်အသုံးအနှုန်းများဖြင့် ဖိုင်အကြောင်းအရာများကို ရှာဖွေပါ။", + "settings.permissions.tool.list.title": "စာရင်း", + "settings.permissions.tool.list.description": "လမ်းညွှန်တစ်ခုအတွင်း ဖိုင်များကို စာရင်းပြုစုပါ။", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "shell command များကို လုပ်ဆောင်ပါ။", + "settings.permissions.tool.task.title": "လုပ်ဆောင်စရာ", + "settings.permissions.tool.task.description": "အေးဂျင့်ခွဲများကို စတင်ပါ။", + "settings.permissions.tool.skill.title": "ကျွမ်းကျင်မှု", + "settings.permissions.tool.skill.description": "အမည်ဖြင့် ကျွမ်းကျင်မှုတစ်ခုကို တင်ပါ။", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "ဘာသာစကားဆာဗာ မေးမြန်းချက်များကို လုပ်ဆောင်ပါ။", + "settings.permissions.tool.todowrite.title": "Todo ရေးပါ။", + "settings.permissions.tool.todowrite.description": "လုပ်စရာစာရင်းကို အပ်ဒိတ်လုပ်ပါ။", + "settings.permissions.tool.webfetch.title": "ဝဘ်ထုတ်ယူမှု", + "settings.permissions.tool.webfetch.description": "URL တစ်ခုမှ အကြောင်းအရာကို ရယူပါ။", + "settings.permissions.tool.websearch.title": "ဝဘ်ရှာဖွေမှု", + "settings.permissions.tool.websearch.description": "ဝဘ်ကို ရှာပါ။", + "settings.permissions.tool.external_directory.title": "ပြင်ပလမ်းညွှန်", + "settings.permissions.tool.external_directory.description": "ပရောဂျက်လမ်းညွှန်မှ ဖိုင်များကို ဝင်ရောက်ကြည့်ရှုပါ။", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": + "တူညီသောထည့်သွင်းမှုဖြင့် ထပ်ခါတလဲလဲ ကိရိယာခေါ်ဆိုမှုများကို ရှာဖွေပါ။", + "session.delete.failed.title": "စက်ရှင်ကို ဖျက်၍မရပါ။", + "session.delete.title": "စက်ရှင်ကို ဖျက်ပါ။", + "session.delete.confirm": 'စက်ရှင် "{{name}}" ကို ဖျက်မလား။', + "session.delete.button": "စက်ရှင်ကို ဖျက်ပါ။", + "workspace.new": "အလုပ်နေရာအသစ်", + "workspace.type.local": "ပြည်တွင်း", + "workspace.type.sandbox": "သဲပုံး", + "workspace.create.failed.title": "အလုပ်ခွင်ကို ဖန်တီး၍မရပါ။", + "workspace.delete.failed.title": "အလုပ်ခွင်ကို ဖျက်၍မရပါ။", + "workspace.resetting.title": "အလုပ်ခွင်ကို ပြန်လည်သတ်မှတ်ခြင်း။", + "workspace.resetting.description": "၎င်းသည် တစ်မိနစ်ခန့် ကြာနိုင်သည်။", + "workspace.reset.failed.title": "လုပ်ငန်းခွင်နေရာကို ပြန်လည်သတ်မှတ်ရန် မအောင်မြင်ပါ။", + "workspace.reset.success.title": "အလုပ်နေရာ ပြန်လည်သတ်မှတ်ခြင်း။", + "workspace.reset.success.description": "ယခု Workspace သည် မူရင်းဌာနခွဲနှင့် ကိုက်ညီပါသည်။", + "workspace.error.stillPreparing": "အလုပ်နေရာသည် ပြင်ဆင်ဆဲဖြစ်သည်။", + "workspace.status.checking": "ပေါင်းစပ်မထားသော အပြောင်းအလဲများကို စစ်ဆေးနေသည်...", + "workspace.status.error": "git အခြေအနေကို အတည်မပြုနိုင်ပါ။", + "workspace.status.clean": "ပေါင်းစပ်မထားသော အပြောင်းအလဲများကို ရှာမတွေ့ပါ။", + "workspace.status.dirty": "ဤလုပ်ငန်းခွင်တွင် ပေါင်းစပ်မထားသော အပြောင်းအလဲများကို တွေ့ရှိခဲ့သည်။", + "workspace.delete.title": "အလုပ်ခွင်ကို ဖျက်ပါ။", + "workspace.delete.confirm": 'အလုပ်ခွင် "{{name}}" ကို ဖျက်မလား။', + "workspace.delete.button": "အလုပ်ခွင်ကို ဖျက်ပါ။", + "workspace.reset.title": "အလုပ်ခွင်ကို ပြန်လည်သတ်မှတ်ပါ။", + "workspace.reset.confirm": 'အလုပ်ခွင် "{{name}}" ကို ပြန်လည်သတ်မှတ်မလား။', + "workspace.reset.button": "အလုပ်ခွင်ကို ပြန်လည်သတ်မှတ်ပါ။", + "workspace.reset.archived.none": "လက်ရှိအသုံးပြုနေသည့် ဆက်ရှင်များကို သိမ်းဆည်းထားမည်မဟုတ်ပါ။", + "workspace.reset.archived.one": "1 ဆက်ရှင်ကို သိမ်းဆည်းပါမည်။", + "workspace.reset.archived.many": "{{count}} ဆက်ရှင်များကို သိမ်းဆည်းထားပါမည်။", + "workspace.reset.note": "၎င်းသည် မူရင်းဌာနခွဲနှင့် ကိုက်ညီစေရန် အလုပ်ခွင်ကို ပြန်လည်သတ်မှတ်ပါမည်။", + "dialog.usageExceeded.dontShowAgain": "ထပ်မပြပါနှင့်", +} diff --git a/packages/app/src/i18n/ne.ts b/packages/app/src/i18n/ne.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc60bff9f40086ef78e874a87e66f4a457f3a519 --- /dev/null +++ b/packages/app/src/i18n/ne.ts @@ -0,0 +1,1157 @@ +export const dict: Record = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "फाइल", + "desktop.menu.edit": "सम्पादन गर्नुहोस्", + "desktop.menu.view": "हेर्नुहोस्", + "desktop.menu.go": "जानुहोस्", + "desktop.menu.window": "झ्याल", + "desktop.menu.help": "मद्दत गर्नुहोस्", + "desktop.menu.checkForUpdates": "अपडेटहरूको लागि जाँच गर्नुहोस्...", + "desktop.menu.settings": "सेटिङहरू", + "desktop.menu.reloadWebview": "Webview पुन: लोड गर्नुहोस्", + "desktop.menu.restart": "पुन: सुरु गर्नुहोस्", + "desktop.menu.exportLogs": "लगहरू निर्यात गर्नुहोस्...", + "desktop.menu.newSession": "नयाँ सत्र", + "desktop.menu.openProject": "परियोजना खोल्नुहोस्...", + "desktop.menu.newWindow": "नयाँ विन्डो", + "desktop.menu.closeWindow": "सञ्झ्याल बन्द गर्नुहोस्", + "desktop.menu.undo": "पूर्ववत गर्नुहोस्", + "desktop.menu.redo": "पुन: गर्नुहोस्", + "desktop.menu.cut": "काट्नुहोस्", + "desktop.menu.copy": "प्रतिलिपि गर्नुहोस्", + "desktop.menu.paste": "टाँस्नुहोस्", + "desktop.menu.delete": "मेट्नुहोस्", + "desktop.menu.selectAll": "सबै चयन गर्नुहोस्", + "desktop.menu.toggleSidebar": "साइडबार टगल गर्नुहोस्", + "desktop.menu.toggleTerminal": "टर्मिनल टगल गर्नुहोस्", + "desktop.menu.toggleFileTree": "फाइल ट्री टगल गर्नुहोस्", + "desktop.menu.reload": "पुन: लोड गर्नुहोस्", + "desktop.menu.toggleDeveloperTools": "विकासकर्ता उपकरणहरू टगल गर्नुहोस्", + "desktop.menu.actualSize": "वास्तविक आकार", + "desktop.menu.zoomIn": "जुम इन गर्नुहोस्", + "desktop.menu.zoomOut": "जुम आउट गर्नुहोस्", + "desktop.menu.toggleFullScreen": "पूर्ण स्क्रिन टगल गर्नुहोस्", + "desktop.menu.back": "पछाडि", + "desktop.menu.forward": "अगाडि", + "desktop.menu.previousSession": "अघिल्लो सत्र", + "desktop.menu.nextSession": "अर्को सत्र", + "desktop.menu.previousProject": "अघिल्लो परियोजना", + "desktop.menu.nextProject": "अर्को परियोजना", + "desktop.menu.minimize": "न्यूनतम गर्नुहोस्", + "desktop.menu.maximize": "अधिकतम गर्नुहोस्", + "desktop.menu.documentation": "OpenCode कागजात", + "desktop.menu.supportForum": "समर्थन फोरम", + "desktop.menu.shareFeedback": "प्रतिक्रिया साझा गर्नुहोस्", + "desktop.menu.reportBug": "बग रिपोर्ट गर्नुहोस्", + "desktop.menu.ariaLabel": "OpenCode मेनु", + "desktop.updater.dialog.checkFailed.message": "अद्यावधिक जाँच असफल भयो।", + "desktop.updater.dialog.checkFailed.title": "अपडेट त्रुटि", + "desktop.updater.dialog.upToDate.message": "तपाईं अद्यावधिक हुनुहुन्छ।", + "desktop.updater.dialog.upToDate.title": "कुनै अद्यावधिकहरू छैनन्", + "desktop.updater.dialog.ready.message": "अपडेट {{version}} डाउनलोड गरियो। अहिले पुन: सुरु गर्ने हो?", + "desktop.updater.dialog.ready.title": "अपडेट तयार", + "desktop.updater.dialog.restart": "पुन: सुरु गर्नुहोस्", + "desktop.updater.dialog.later": "पछि", + "desktop.recovery.action.relaunch": "पुन: सुरु गर्नुहोस्", + "desktop.recovery.action.exportLogs": "लग निर्यात गर्नुहोस्", + "desktop.recovery.action.keepWaiting": "प्रतीक्षा गरिरहनुहोस्", + "desktop.recovery.action.quit": "छोड्नुहोस्", + "desktop.recovery.loadFailed": "OpenCode लोड हुन सकेन", + "desktop.recovery.terminated": "OpenCode विन्डो अप्रत्याशित रूपमा समाप्त भयो", + "desktop.recovery.unresponsive": "OpenCode ले प्रतिक्रिया दिइरहेको छैन", + "desktop.recovery.unresponsive.detail": + "तपाईं एप पुन: लन्च गर्न सक्नुहुन्छ, लगहरू खोल्न सक्नुहुन्छ वा पर्खन सक्नुहुन्छ।", + "desktop.recovery.loadFailed.detail": "विन्डो: {{window}}\nURL: {{url}}\nत्रुटि: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "विन्डो: {{window}}\nकारण: {{reason}}\nकोड: {{code}}", + "desktop.recovery.unknown": "<अज्ञात>", + "desktop.dialog.chooseFolder": "एउटा फोल्डर छान्नुहोस्", + "desktop.dialog.chooseFile": "एउटा फाइल छान्नुहोस्", + "desktop.dialog.saveFile": "फाइल बचत गर्नुहोस्", + "desktop.dialog.files": "फाइलहरू", + "desktop.server.local": "स्थानीय सर्भर", + "desktop.wsl.error.windowsOnly": "WSL Windows मा मात्र उपलब्ध छ", + "desktop.wsl.error.unavailable": "WSL उपलब्ध छैन", + "desktop.wsl.error.listInstalled": "स्थापित WSL distros सूची गर्न असफल", + "desktop.wsl.error.listOnline": "अनलाइन WSL distros सूचीबद्ध गर्न असफल", + "desktop.wsl.error.executeDistro": "डिस्ट्रोमा आदेशहरू कार्यान्वयन गर्न सकिँदैन", + "desktop.wsl.error.installWsl": "WSL स्थापना असफल भयो", + "desktop.wsl.error.installDistro": "डिस्ट्रो स्थापना गर्न असफल: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode स्थापना असफल भयो", + "desktop.wsl.error.alreadyAdded": "{{distro}} पहिले नै थपिएको छ", + "desktop.wsl.error.opencodeMissing": "Opencode यस distro मा स्थापित छैन", + "desktop.wsl.error.opencodeCannotRun": "opencode स्थापित छ तर चलाउन सकिएन", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode {{distro}} मा स्थापित छैन", + "desktop.wsl.error.updateVersion": + "OpenCode अद्यावधिक समाप्त भयो तर {{distro}} ले अझै पनि रिपोर्ट गर्छ {{installed}}; अपेक्षित {{expected}}", + "desktop.wsl.error.noVersion": "कुनै संस्करण छैन", + "desktop.wsl.error.serverExited": "WSL सर्भर स्टार्टअप पछि बाहिर निस्कियो (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL सर्भर स्वस्थ हुनु अघि बाहिर निस्कियो (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} स्वास्थ्य जाँचको लागि साइडकार {{timeout}}ms पछि समय सकियो", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} {{timeout}}ms पछि टाइमआउट भयो", + "desktop.wsl.error.failedPort": "पोर्ट प्राप्त गर्न असफल भयो", + "desktop.picker.error.notSelected": "फाइल चयनकर्ता द्वारा चयन गरिएको थिएन", + "desktop.picker.error.sizeLimit": "चयन गरिएका संलग्नहरू {{limit}} MB सीमा नाघ्छन्", + "command.category.suggested": "सिफारिस गर्नुभयो", + "command.category.view": "हेर्नुहोस्", + "command.category.project": "परियोजना", + "command.category.provider": "प्रदायक", + "command.category.server": "सर्भर", + "command.category.session": "सत्र", + "command.category.theme": "विषयवस्तु", + "command.category.language": "भाषा", + "command.category.file": "फाइल", + "command.category.context": "सन्दर्भ", + "command.category.terminal": "टर्मिनल", + "command.category.model": "मोडेल", + "command.category.mcp": "MCP", + "command.category.agent": "एजेन्ट", + "command.category.permissions": "अनुमतिहरू", + "command.category.workspace": "कार्यस्थान", + "command.category.settings": "सेटिङहरू", + "command.logs.export": "लगहरू निर्यात गर्नुहोस्", + "theme.scheme.system": "प्रणाली", + "theme.scheme.light": "उज्यालो", + "theme.scheme.dark": "अँध्यारो", + "command.sidebar.toggle": "साइडबार टगल गर्नुहोस्", + "command.project.open": "परियोजना खोल्नुहोस्", + "command.project.previous": "अघिल्लो परियोजना", + "command.project.next": "अर्को परियोजना", + "command.project.index": "परियोजना {{index}} मा स्विच गर्नुहोस्", + "command.provider.connect": "प्रदायक जडान गर्नुहोस्", + "command.server.switch": "सर्भर स्विच गर्नुहोस्", + "command.settings.open": "सेटिङ्हरू खोल्नुहोस्", + "command.session.previous": "अघिल्लो सत्र", + "command.session.next": "अर्को सत्र", + "command.session.previous.unseen": "अघिल्लो नपढिएको सत्र", + "command.session.next.unseen": "अर्को अपठित सत्र", + "command.session.archive": "अभिलेख सत्र", + "command.palette": "आदेश प्यालेट", + "command.theme.cycle": "साइकल विषयवस्तु", + "command.theme.set": "विषयवस्तु प्रयोग गर्नुहोस्: {{theme}}", + "command.theme.scheme.cycle": "साइकल रङ योजना", + "command.theme.scheme.set": "रङ योजना प्रयोग गर्नुहोस्: {{scheme}}", + "command.language.cycle": "साइकल भाषा", + "command.language.set": "भाषा प्रयोग गर्नुहोस्: {{language}}", + "command.session.new": "नयाँ सत्र", + "command.file.open": "फाइल खोल्नुहोस्", + "command.tab.close": "ट्याब बन्द गर्नुहोस्", + "command.tab.reopenClosed": "बन्द ट्याब पुन: खोल्नुहोस्", + "command.context.addSelection": "सन्दर्भमा चयन थप्नुहोस्", + "command.context.addSelection.description": "हालको फाइलबाट चयन गरिएका रेखाहरू थप्नुहोस्", + "command.input.focus": "फोकस इनपुट", + "command.terminal.toggle": "टर्मिनल टगल गर्नुहोस्", + "command.fileTree.toggle": "फाइल रूख टगल गर्नुहोस्", + "command.review.toggle": "समीक्षा टगल गर्नुहोस्", + "command.terminal.new": "नयाँ टर्मिनल", + "command.terminal.new.description": "नयाँ टर्मिनल ट्याब सिर्जना गर्नुहोस्", + "command.steps.toggle": "चरणहरू टगल गर्नुहोस्", + "command.steps.toggle.description": "हालको सन्देशका लागि चरणहरू देखाउनुहोस् वा लुकाउनुहोस्", + "command.message.previous": "अघिल्लो सन्देश", + "command.message.previous.description": "अघिल्लो प्रयोगकर्ता सन्देशमा जानुहोस्", + "command.message.next": "अर्को सन्देश", + "command.message.next.description": "अर्को प्रयोगकर्ता सन्देशमा जानुहोस्", + "command.model.choose": "मोडेल छान्नुहोस्", + "command.model.choose.description": "फरक मोडेल चयन गर्नुहोस्", + "command.mcp.toggle": "MCPs टगल गर्नुहोस्", + "command.mcp.toggle.description": "MCPs टगल गर्नुहोस्", + "command.agent.cycle": "साइकल एजेन्ट", + "command.agent.cycle.description": "अर्को एजेन्टमा स्विच गर्नुहोस्", + "command.agent.cycle.reverse": "साइकल एजेन्ट पछाडि", + "command.agent.cycle.reverse.description": "अघिल्लो एजेन्टमा स्विच गर्नुहोस्", + "command.model.variant.cycle": "साइकल सोच प्रयास", + "command.model.variant.cycle.description": "अर्को प्रयास स्तरमा स्विच गर्नुहोस्", + "command.prompt.mode.shell": "शेल", + "command.prompt.mode.normal": "प्रम्प्ट", + "command.permissions.autoaccept.enable": "अनुमतिहरू स्वत: स्वीकार गर्नुहोस्", + "command.permissions.autoaccept.disable": "अनुमतिहरू स्वत: स्वीकार गर्न रोक्नुहोस्", + "command.workspace.toggle": "कार्यस्थान टगल गर्नुहोस्", + "command.workspace.toggle.description": "साइडबारमा बहु कार्यस्थानहरू सक्षम वा असक्षम गर्नुहोस्", + "command.session.undo": "पूर्ववत गर्नुहोस्", + "command.session.undo.description": "अन्तिम सन्देश अनडू गर्नुहोस्", + "command.session.redo": "पुन: गर्नुहोस्", + "command.session.redo.description": "अन्तिम अनडू सन्देश पुन: गर्नुहोस्", + "command.session.compact": "कम्प्याक्ट सत्र", + "command.session.compact.description": "प्रसंगको आकार घटाउन सत्रलाई संक्षेप गर्नुहोस्", + "command.session.fork": "सन्देशबाट फोर्क", + "command.session.fork.description": "अघिल्लो सन्देशबाट नयाँ सत्र सिर्जना गर्नुहोस्", + "command.session.share": "सत्र साझेदारी गर्नुहोस्", + "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": "कुनै परिणाम फेला परेन", + "palette.group.commands": "आदेशहरू", + "palette.group.files": "फाइलहरू", + "dialog.provider.search.placeholder": "खोज प्रदायकहरू", + "dialog.provider.empty": "कुनै प्रदायक भेटिएन", + "dialog.provider.group.popular": "लोकप्रिय", + "dialog.provider.group.other": "अन्य", + "dialog.provider.custom.label": "आफू अनुकूल OpenAI-कम्प्याटिबल प्रदायक", + "dialog.provider.tag.recommended": "सिफारिस गर्नुभयो", + "dialog.provider.opencode.note": "Claude, GPT, Gemini र थप सहित क्युरेट गरिएका मोडेलहरू", + "dialog.provider.opencode.tagline": "भरपर्दो अनुकूलित मोडेलहरू", + "dialog.provider.opencodeGo.tagline": "सबैको लागि कम लागत सदस्यता", + "dialog.provider.anthropic.note": "प्रो र म्याक्स सहित Claude मोडेलहरूमा प्रत्यक्ष पहुँच", + "dialog.provider.copilot.note": "GitHub Copilot मार्फत कोडिङ सहायताका लागि AI मोडेलहरू", + "dialog.provider.openai.note": "छिटो, सक्षम सामान्य AI कार्यहरूको लागि GPT मोडेलहरू", + "dialog.provider.google.note": "छिटो, संरचित प्रतिक्रियाहरूको लागि Gemini मोडेलहरू", + "dialog.provider.openrouter.note": "एक प्रदायकबाट सबै समर्थित मोडेलहरू पहुँच गर्नुहोस्", + "dialog.provider.vercel.note": "स्मार्ट राउटिङको साथ एआई मोडेलहरूमा एकीकृत पहुँच", + "dialog.model.select.title": "मोडेल चयन गर्नुहोस्", + "dialog.model.search.placeholder": "मोडेलहरू खोज्नुहोस्", + "dialog.model.empty": "कुनै मोडेल परिणामहरू छैनन्", + "dialog.model.manage": "मोडेलहरू प्रबन्ध गर्नुहोस्", + "dialog.model.manage.description": "मोडेल चयनकर्तामा कुन मोडेलहरू देखा पर्दछ अनुकूलित गर्नुहोस्।", + "dialog.model.manage.provider.toggle": "सबै {{provider}} मोडेलहरू टगल गर्नुहोस्", + "dialog.model.unpaid.freeModels.title": "OpenCode द्वारा प्रदान गरिएको नि:शुल्क मोडेलहरू", + "dialog.model.unpaid.addMore.title": "लोकप्रिय प्रदायकहरूबाट थप मोडेलहरू थप्नुहोस्", + "dialog.model.unpaid.viewMoreProviders": "70+ थप प्रदायकहरू हेर्नुहोस्", + "dialog.provider.viewAll": "थप प्रदायकहरू देखाउनुहोस्", + "provider.connect.title": "जडान गर्नुहोस् {{provider}}", + "provider.connect.title.anthropicProMax": "Claude Pro/Max सँग लगइन गर्नुहोस्", + "provider.connect.selectMethod": "{{provider}} का लागि लगइन विधि चयन गर्नुहोस्।", + "provider.connect.method.apiKey": "API कुञ्जी", + "provider.connect.method.browser": "ब्राउजर", + "provider.connect.method.headless": "टाउकोविहीन", + "provider.connect.status.inProgress": "प्राधिकरण जारी छ...", + "provider.connect.status.waiting": "प्राधिकरणको लागि पर्खँदै...", + "provider.connect.status.failed": "प्राधिकरण असफल भयो: {{error}}", + "provider.connect.apiKey.description": + "आफ्नो खाता जडान गर्न आफ्नो {{provider}} API कुञ्जी प्रविष्ट गर्नुहोस् र OpenCode मा {{provider}} मोडेलहरू प्रयोग गर्नुहोस्।", + "provider.connect.apiKey.label": "{{provider}} API कुञ्जी", + "provider.connect.apiKey.placeholder": "API कुञ्जी", + "provider.connect.apiKey.required": "API कुञ्जी आवश्यक छ", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ले तपाईंलाई कोडिङ एजेन्टहरूका लागि भरपर्दो अनुकूलित मोडेलहरूको क्युरेट गरिएको सेटमा पहुँच दिन्छ।", + "provider.connect.opencodeZen.line2": + "एकल API कुञ्जीको साथ तपाईंले Claude, GPT, Gemini, GLM र थप जस्ता मोडेलहरूमा पहुँच प्राप्त गर्नुहुनेछ।", + "provider.connect.opencodeZen.visit.prefix": "भ्रमण गर्नुहोस्", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "तपाईँको API कुञ्जी सङ्कलन गर्न।", + "provider.connect.oauth.code.visit.prefix": "भ्रमण गर्नुहोस्", + "provider.connect.oauth.code.visit.link": "यो लिङ्क", + "provider.connect.oauth.code.visit.suffix": + "आफ्नो खाता जडान गर्न र OpenCode मा {{provider}} मोडेलहरू प्रयोग गर्न आफ्नो प्राधिकरण कोड सङ्कलन गर्न।", + "provider.connect.oauth.code.label": "{{method}} प्राधिकरण कोड", + "provider.connect.oauth.code.placeholder": "प्राधिकरण कोड", + "provider.connect.oauth.code.required": "प्राधिकरण कोड आवश्यक छ", + "provider.connect.oauth.code.invalid": "अवैध प्राधिकरण कोड", + "provider.connect.oauth.auto.visit.prefix": "भ्रमण गर्नुहोस्", + "provider.connect.oauth.auto.visit.link": "यो लिङ्क", + "provider.connect.oauth.auto.visit.suffix": + "र आफ्नो खाता जडान गर्न तलको कोड प्रविष्ट गर्नुहोस् र {{provider}} मोडेलहरू OpenCode मा प्रयोग गर्नुहोस्।", + "provider.connect.oauth.auto.confirmationCode": "पुष्टि कोड", + "provider.connect.toast.connected.title": "{{provider}} जडान भयो", + "provider.connect.toast.connected.description": "{{provider}} मोडेलहरू अब प्रयोग गर्न उपलब्ध छन्।", + "provider.custom.title": "अनुकूलन प्रदायक", + "provider.custom.unavailable": "यस सर्भरमा अनुकूलन प्रदायकहरू उपलब्ध छैनन्", + "provider.custom.description.prefix": "एउटा OpenAI-कम्प्याटिबल प्रदायक कन्फिगर गर्नुहोस्। हेर्नुहोस्", + "provider.custom.description.link": "प्रदायक कन्फिगरेसन कागजातहरू", + "provider.custom.description.suffix": "।", + "provider.custom.field.providerID.label": "प्रदायक आईडी", + "provider.custom.field.providerID.placeholder": "मेरो प्रदायक", + "provider.custom.field.providerID.description": "लोअरकेस अक्षरहरू, संख्याहरू, हाइफनहरू, वा अन्डरस्कोरहरू", + "provider.custom.field.name.label": "नाम प्रदर्शन गर्नुहोस्", + "provider.custom.field.name.placeholder": "मेरो एआई प्रदायक", + "provider.custom.field.baseURL.label": "आधार URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API कुञ्जी", + "provider.custom.field.apiKey.placeholder": "API कुञ्जी", + "provider.custom.field.apiKey.description": + "ऐच्छिक। यदि तपाइँ हेडरहरू मार्फत प्रमाणीकरण व्यवस्थापन गर्नुहुन्छ भने खाली छोड्नुहोस्।", + "provider.custom.models.label": "मोडेलहरू", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "मोडेल-आईडी", + "provider.custom.models.name.label": "नाम", + "provider.custom.models.name.placeholder": "प्रदर्शन नाम", + "provider.custom.models.remove": "मोडेल हटाउनुहोस्", + "provider.custom.models.add": "मोडेल थप्नुहोस्", + "provider.custom.headers.label": "हेडर (वैकल्पिक)", + "provider.custom.headers.key.label": "हेडर", + "provider.custom.headers.key.placeholder": "हेडर-नाम", + "provider.custom.headers.value.label": "मूल्य", + "provider.custom.headers.value.placeholder": "मूल्य", + "provider.custom.headers.remove": "हेडर हटाउनुहोस्", + "provider.custom.headers.add": "हेडर थप्नुहोस्", + "provider.custom.error.providerID.required": "प्रदायक ID आवश्यक छ", + "provider.custom.error.providerID.format": "सानो अक्षर, संख्या, हाइफन वा अन्डरस्कोरहरू प्रयोग गर्नुहोस्", + "provider.custom.error.providerID.exists": "त्यो प्रदायक ID पहिले नै अवस्थित छ", + "provider.custom.error.name.required": "प्रदर्शन नाम आवश्यक छ", + "provider.custom.error.baseURL.required": "आधार URL आवश्यक छ", + "provider.custom.error.baseURL.format": "http:// वा https:// बाट सुरु गर्नुपर्छ", + "provider.custom.error.required": "आवश्यक छ", + "provider.custom.error.duplicate": "नक्कल", + "provider.disconnect.toast.disconnected.title": "{{provider}} जडान विच्छेद भयो", + "provider.disconnect.toast.disconnected.description": "{{provider}} मोडेलहरू अब उपलब्ध छैनन्।", + "model.tag.free": "नि:शुल्क", + "model.tag.latest": "पछिल्लो", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "पाठ", + "model.input.image": "छवि", + "model.input.audio": "अडियो", + "model.input.video": "भिडियो", + "model.input.pdf": "pdf", + "model.tooltip.allows": "अनुमति दिन्छ: {{inputs}}", + "model.tooltip.context.label": "सन्दर्भ", + "model.tooltip.inputs": "इनपुटहरू", + "model.tooltip.model": "मोडेल", + "model.tooltip.provider": "प्रदायक", + "model.tooltip.reasoning": "तर्क", + "model.tooltip.reasoning.allowed": "तर्क गर्न अनुमति दिन्छ", + "model.tooltip.reasoning.none": "कुनै तर्क छैन", + "model.tooltip.context": "सन्दर्भ सीमा {{limit}}", + "common.search.placeholder": "खोज्नुहोस्", + "common.goBack": "पछाडि नेभिगेट गर्नुहोस्", + "common.goForward": "अगाडि नेभिगेट गर्नुहोस्", + "common.loading": "लोड गर्दै", + "common.loading.ellipsis": "...", + "common.cancel": "रद्द गर्नुहोस्", + "common.clear": "खाली गर्नुहोस्", + "common.open": "खोल्नुहोस्", + "common.connect": "जडान गर्नुहोस्", + "common.disconnect": "जडान विच्छेद गर्नुहोस्", + "common.continue": "जारी राख्नुहोस्", + "common.submit": "पेश गर्नुहोस्", + "common.save": "बचत गर्नुहोस्", + "common.saving": "बचत गर्दै...", + "common.default": "पूर्वनिर्धारित", + "common.attachment": "संलग्नक", + "prompt.placeholder.shell": "शेल आदेश प्रविष्ट गर्नुहोस्... {{example}}", + "prompt.placeholder.normal": 'केहि सोध्नुहोस्... "{{example}}"', + "prompt.placeholder.simple": "जे पनि सोध...", + "prompt.placeholder.summarizeComments": "टिप्पणीहरू संक्षेप गर्नुहोस् ...", + "prompt.placeholder.summarizeComment": "टिप्पणी संक्षेप...", + "prompt.mode.shell": "शेल", + "prompt.mode.normal": "प्रम्प्ट", + "prompt.mode.shell.exit": "esc बाहिर निस्कन", + "session.child.promptDisabled": "Subagent सत्रहरू प्रोम्प्ट गर्न सकिँदैन।", + "session.child.backToParent": "मुख्य सत्रमा फर्कनुहोस्।", + "prompt.example.1": "कोडबेसमा TODO फिक्स गर्नुहोस्", + "prompt.example.2": "यस परियोजनाको प्राविधिक स्ट्याक के हो?", + "prompt.example.3": "बिग्रिएको परीक्षणहरू ठीक गर्नुहोस्", + "prompt.example.4": "प्रमाणीकरण कसरी काम गर्दछ व्याख्या गर्नुहोस्", + "prompt.example.5": "सुरक्षा कमजोरीहरू फेला पार्नुहोस् र ठीक गर्नुहोस्", + "prompt.example.6": "प्रयोगकर्ता सेवाको लागि एकाइ परीक्षणहरू थप्नुहोस्", + "prompt.example.7": "यस प्रकार्यलाई थप पढ्न योग्य हुन रिफ्याक्टर गर्नुहोस्", + "prompt.example.8": "यो त्रुटिको अर्थ के हो?", + "prompt.example.9": "मलाई यो समस्या डिबग गर्न मद्दत गर्नुहोस्", + "prompt.example.10": "API कागजातहरू उत्पन्न गर्नुहोस्", + "prompt.example.11": "डाटाबेस प्रश्नहरू अनुकूलन गर्नुहोस्", + "prompt.example.12": "इनपुट प्रमाणीकरण थप्नुहोस्", + "prompt.example.13": "यसका लागि नयाँ कम्पोनेन्ट सिर्जना गर्नुहोस्...", + "prompt.example.14": "म यो परियोजना कसरी लागू गर्न सक्छु?", + "prompt.example.15": "उत्तम अभ्यासहरूको लागि मेरो कोड समीक्षा गर्नुहोस्", + "prompt.example.16": "यस प्रकार्यमा त्रुटि ह्यान्डलिङ थप्नुहोस्", + "prompt.example.17": "यो regex ढाँचा व्याख्या गर्नुहोस्", + "prompt.example.18": "यसलाई TypeScript मा रूपान्तरण गर्नुहोस्", + "prompt.example.19": "कोडबेसमा लगिङ थप्नुहोस्", + "prompt.example.20": "कुन निर्भरताहरू पुरानो छन्?", + "prompt.example.21": "मलाई माइग्रेसन लिपि लेख्न मद्दत गर्नुहोस्", + "prompt.example.22": "यो अन्तिम बिन्दुको लागि क्यासिङ लागू गर्नुहोस्", + "prompt.example.23": "यो सूचीमा पृष्ठांकन थप्नुहोस्", + "prompt.example.24": "यसका लागि CLI आदेश सिर्जना गर्नुहोस्...", + "prompt.example.25": "वातावरणीय चरहरूले यहाँ कसरी काम गर्छन्?", + "prompt.popover.emptyResults": "कुनै मिल्दो परिणामहरू छैनन्", + "prompt.popover.emptyCommands": "कुनै मिल्दो आदेशहरू छैनन्", + "prompt.dropzone.label": "छविहरू, PDF हरू, वा पाठ फाइलहरू यहाँ छोड्नुहोस्", + "prompt.dropzone.file.label": "@mention फाइलमा ड्रप गर्नुहोस्", + "prompt.slash.badge.custom": "अनुकूलन", + "prompt.slash.badge.skill": "कौशल", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "सक्रिय", + "prompt.context.includeActiveFile": "सक्रिय फाइल समावेश गर्नुहोस्", + "prompt.context.removeActiveFile": "सन्दर्भबाट सक्रिय फाइल हटाउनुहोस्", + "prompt.context.removeFile": "सन्दर्भबाट फाइल हटाउनुहोस्", + "prompt.action.attachFile": "फाइलहरू थप्नुहोस्", + "prompt.menu.addImagesAndFiles": "फाइलहरू र थप थप्नुहोस्", + "prompt.menu.imagesAndFiles": "छविहरू र फाइलहरू", + "prompt.menu.commands": "आदेशहरू", + "prompt.menu.context": "सन्दर्भ", + "prompt.menu.shellCommand": "शेल आदेश", + "prompt.attachment.remove": "संलग्नक हटाउनुहोस्", + "prompt.action.send": "पठाउनुहोस्", + "prompt.action.stop": "रोक्नुहोस्", + "prompt.toast.pasteUnsupported.title": "असमर्थित संलग्नक", + "prompt.toast.pasteUnsupported.description": "केवल छविहरू, PDF हरू, वा पाठ फाइलहरू यहाँ संलग्न गर्न सकिन्छ।", + "prompt.toast.attachmentDuplicate.title": "यो फाइल पहिले नै अपलोड गरिएको छ", + "prompt.toast.modelAgentRequired.title": "एक एजेन्ट र मोडेल चयन गर्नुहोस्", + "prompt.toast.modelAgentRequired.description": "प्रम्प्ट पठाउनु अघि एजेन्ट र मोडेल छान्नुहोस्।", + "prompt.toast.worktreeCreateFailed.title": "Git वर्कट्री सिर्जना गर्न असफल भयो", + "prompt.toast.sessionCreateFailed.title": "सत्र सिर्जना गर्न असफल भयो", + "prompt.toast.shellSendFailed.title": "शेल आदेश पठाउन असफल भयो", + "prompt.toast.commandSendFailed.title": "आदेश पठाउन असफल भयो", + "prompt.toast.promptSendFailed.title": "प्रम्प्ट पठाउन असफल भयो", + "prompt.toast.promptSendFailed.description": "सत्र पुन: प्राप्त गर्न असमर्थ", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} को {{total}} सक्षम गरियो", + "dialog.mcp.empty": "कुनै पनि MCPs कन्फिगर गरिएको छैन", + "dialog.lsp.empty": "LSPs फाइल प्रकारहरूबाट स्वतः पत्ता लगाइयो", + "dialog.plugins.empty": "opencode.json मा कन्फिगर गरिएका प्लगइनहरू", + "mcp.status.connected": "जोडिएको", + "mcp.status.failed": "असफल", + "mcp.status.needs_auth": "प्रमाणीकरण चाहिन्छ", + "mcp.status.disabled": "असक्षम", + "mcp.auth.clickToAuthenticate": "प्रमाणीकरण गर्न क्लिक गर्नुहोस्", + "dialog.fork.empty": "बाट फोर्क गर्न कुनै सन्देश छैन", + "dialog.directory.search.placeholder": "फोल्डरहरू खोज्नुहोस्", + "dialog.directory.empty": "कुनै फोल्डरहरू फेला परेनन्", + "dialog.directory.action.selectFile": "फाइल चयन गर्नुहोस्", + "dialog.directory.action.selectFolder": "फोल्डर चयन गर्नुहोस्", + "dialog.directory.root": "रूट", + "dialog.directory.parent": "अभिभावक", + "dialog.directory.readError": "यो फोल्डर पढ्न असमर्थ", + "app.server.unreachable": "{{server}} मा पुग्न सकिएन", + "app.server.retrying": "स्वतः पुन: प्रयास गर्दै...", + "app.server.otherServers": "अन्य सर्भरहरू", + "dialog.server.title": "सर्भरहरू", + "dialog.server.description": "यो एप जडान भएको OpenCode सर्भरलाई स्विच गर्नुहोस्।", + "dialog.server.search.placeholder": "सर्भरहरू खोज्नुहोस्", + "dialog.server.empty": "अहिलेसम्म कुनै सर्भर छैन", + "dialog.server.add.title": "सर्भर थप्नुहोस्", + "dialog.server.add.url": "सर्भर ठेगाना", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "सर्भरमा जडान गर्न सकिएन", + "dialog.server.add.checking": "जाँच गर्दै...", + "dialog.server.add.button": "सर्भर थप्नुहोस्", + "dialog.server.add.name": "सर्भर नाम (वैकल्पिक)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "प्रयोगकर्ता नाम (वैकल्पिक)", + "dialog.server.add.usernamePlaceholder": "प्रयोगकर्ता नाम", + "dialog.server.add.password": "पासवर्ड (वैकल्पिक)", + "dialog.server.add.passwordPlaceholder": "पासवर्ड", + "dialog.server.edit.title": "सर्भर सम्पादन गर्नुहोस्", + "dialog.server.default.title": "पूर्वनिर्धारित सर्भर", + "dialog.server.default.description": + "स्थानीय सर्भर सुरु गर्नुको सट्टा एप लन्चमा यो सर्भरमा जडान गर्नुहोस्। पुन: सुरु गर्न आवश्यक छ।", + "dialog.server.default.none": "कुनै सर्भर चयन गरिएको छैन", + "dialog.server.default.set": "पूर्वनिर्धारित रूपमा हालको सर्भर सेट गर्नुहोस्", + "dialog.server.default.clear": "खाली गर्नुहोस्", + "dialog.server.action.remove": "सर्भर हटाउनुहोस्", + "dialog.server.menu.edit": "सम्पादन गर्नुहोस्", + "dialog.server.menu.default": "पूर्वनिर्धारित रूपमा सेट गर्नुहोस्", + "dialog.server.menu.defaultRemove": "पूर्वनिर्धारित हटाउनुहोस्", + "dialog.server.menu.delete": "मेट्नुहोस्", + "dialog.server.current": "हालको सर्भर", + "dialog.server.status.default": "पूर्वनिर्धारित", + "wsl.server.add": "WSL सर्भर थप्नुहोस्", + "wsl.server.addShort": "WSL थप्नुहोस्", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL सर्भर", + "wsl.server.retryStart": "पुन: प्रयास सुरु गर्नुहोस्", + "wsl.server.updating": "अद्यावधिक गर्दै...", + "wsl.onboarding.step.distro": "डिस्ट्रो छान्नुहोस्", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL जाँच गर्दै...", + "wsl.onboarding.restartRequired": "WSL स्थापना पूरा गर्न Windows लाई पुन: सुरु गर्न आवश्यक छ।", + "wsl.onboarding.ready": "WSL तयार छ।", + "wsl.onboarding.required": "WSL जारी राख्न आवश्यक छ।", + "wsl.onboarding.checkingDistros": "distros जाँच गर्दै...", + "wsl.onboarding.installingDistro": "स्थापना गर्दै {{distro}}...", + "wsl.onboarding.checkingDistro": "जाँच गर्दै {{distro}}...", + "wsl.onboarding.listingDistros": "सूचीबद्ध distros...", + "wsl.onboarding.distroReady": "{{distro}} तयार छ।", + "wsl.onboarding.distroNotInstalled": "{{distro}} अझै स्थापना भएको छैन।", + "wsl.onboarding.openDistroOnce": "सेटअप पूरा गर्न एक पटक {{distro}} खोल्नुहोस्।", + "wsl.onboarding.finishingDistro": "{{distro}} को लागि सेटअप समाप्त गर्दै।", + "wsl.onboarding.pickDistro": "एउटा डिस्ट्रो छान्नुहोस् वा तल स्थापना गर्नुहोस्।", + "wsl.onboarding.checkingOpencode": "OpenCode जाँच गर्दै...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}} मा OpenCode जाँच गर्दै...", + "wsl.onboarding.updatingOpencode": "अद्यावधिक गर्दै OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}} मा OpenCode अद्यावधिक गर्दै...", + "wsl.onboarding.updateOpencodeIn": "अपडेट गर्नुहोस् OpenCode {{distro}} मा।", + "wsl.onboarding.updateOpencode": "अपडेट गर्नुहोस् OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode {{distro}} मा तयार छ।", + "wsl.onboarding.opencodeReady": "OpenCode तयार छ।", + "wsl.onboarding.installOpencodeIn": "{{distro}} मा OpenCode स्थापना गर्नुहोस्।", + "wsl.onboarding.installOpencode": "OpenCode स्थापना गर्नुहोस्", + "wsl.onboarding.chooseDistroFirst": "पहिले डिस्ट्रो छान्नुहोस्।", + "wsl.onboarding.loadFailed": "WSL स्थिति लोड गर्न असफल।", + "wsl.onboarding.loading": "लोड गर्दै...", + "wsl.onboarding.installedDistros": "स्थापित distros", + "wsl.onboarding.checkAgain": "फेरि जाँच गर्नुहोस्", + "wsl.onboarding.distroStatus.ready": "तयार", + "wsl.onboarding.distroStatus.checking": "जाँच गर्दै...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode स्थापित छैन", + "wsl.onboarding.distroStatus.missingTools": "हराइरहेको ब्याश, कर्ल", + "wsl.onboarding.distroStatus.unsupported": "असमर्थित · WSL २ प्रयोग गर्नुहोस्", + "wsl.onboarding.needAnotherDistro": "अर्को डिस्ट्रो चाहिन्छ?", + "wsl.onboarding.needAnotherDistroHint": "WSL क्याटलगबाट Linux वितरण स्थापना गर्नुहोस्", + "wsl.onboarding.wslNotInstalled.title": "WSL स्थापित छैन", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Linux को लागि सबसिस्टम) OpenCode ले WSL सर्भर थप्न अघि आवश्यक छ।", + "wsl.onboarding.wslUnavailable.title": "WSL उपलब्ध छैन", + "wsl.onboarding.wslUnavailable.description": "OpenCode ले यो मेसिनमा WSL प्रमाणित गर्न सकेन।", + "wsl.onboarding.installWsl": "WSL स्थापना गर्नुहोस्", + "wsl.onboarding.windowsRestartRequired": + "WSL स्थापना पूरा गर्न Windows पुन: सुरु गर्नुहोस्, त्यसपछि OpenCode पुन: खोल्नुहोस्।", + "wsl.onboarding.next": "अर्को", + "wsl.onboarding.refresh": "रिफ्रेस गर्नुहोस्", + "wsl.onboarding.allDistrosAdded": "सबै स्थापित distros पहिले नै थपिएको छ।", + "wsl.onboarding.noDistros": "अहिलेसम्म कुनै डिस्ट्रोस पत्ता लागेन।", + "wsl.onboarding.install": "स्थापना गर्नुहोस्", + "wsl.onboarding.installing": "स्थापना गर्दै...", + "wsl.onboarding.installDistro": "डिस्ट्रो स्थापना गर्नुहोस्", + "wsl.onboarding.searchDistros": "डिस्ट्रो खोज्नुहोस्", + "wsl.onboarding.wsl2Required": "WSL २ आवश्यक छ।", + "wsl.onboarding.toolsRequired": "यो डिस्ट्रोलाई ब्याश र कर्ल चाहिन्छ।", + "wsl.onboarding.openTerminal": "टर्मिनल खोल्नुहोस्", + "wsl.onboarding.path": "पथ: {{path}}", + "wsl.onboarding.notFound": "फेला परेन", + "wsl.onboarding.version": "संस्करण: {{version}}", + "wsl.onboarding.unknown": "अज्ञात", + "wsl.onboarding.desktopVersion": "डेस्कटप {{version}}", + "wsl.onboarding.versionMismatch": "स्थापना गरिएको संस्करण डेस्कटप एप संस्करणसँग मेल खाँदैन।", + "wsl.onboarding.adding": "थप्दै...", + "help.tabs.toast.ariaLabel": "ट्याबहरू प्रस्तुत गर्दै। ट्याबहरूसँग आफ्नो काम र सक्रिय सत्रहरू व्यवस्थित गर्नुहोस्", + "help.tabs.toast.dismiss": "ट्याब जानकारी खारेज गर्नुहोस्", + "help.tabs.title": "ट्याबहरू प्रस्तुत गर्दै", + "help.tabs.description": "ट्याबहरूसँग आफ्नो काम र सक्रिय सत्रहरू व्यवस्थित गर्नुहोस्", + "help.tabs.date": "जुलाई १४", + "help.tabs.introduction": "OpenCode डेस्कटप अब ट्याब वरिपरि बनाइएको छ।", + "help.tabs.sessions": + "ट्याबमा नयाँ सत्र सुरु गर्नुहोस्, वा तपाईंको कुनै पनि परियोजनाहरूबाट अवस्थित सत्र खोल्नुहोस्। तपाईंले केहि नयाँ सुरु गर्दा एउटा नयाँ ट्याब खोल्नुहोस्, र तपाईंले सकिसकेपछि यसलाई बन्द गर्नुहोस्।", + "help.tabs.organize": + "केही ट्याबहरू खुला राख्नाले तपाईंको सक्रिय सत्रहरू व्यवस्थित गर्न सजिलो बनाउँछ। यदि तपाइँ तिनीहरूलाई वरिपरि राख्ने योजना बनाउनुहुन्छ भने ट्याबहरूलाई स्मरणीय कुरामा पुन: नामाकरण गर्नुहोस्।", + "help.tabs.home": + "तपाईंले नयाँ गृह स्क्रिनमा आफ्ना सबै सत्रहरू र परियोजनाहरू फेला पार्नुहुनेछ। सत्र चयन गर्नाले यसलाई ट्याबमा खोल्छ।", + "help.tabs.persistence": "जब तपाइँ एप पुन: खोल्नुहुन्छ, तपाइँका ट्याबहरू अझै खुला छन्।", + "help.tabs.worktrees": + "नयाँ डिजाइनले अझै पनि Git Worktrees लाई समर्थन गर्दैन, यो चाँडै आउँदैछ। त्यसैले यदि तपाइँ अघिल्लो लेआउट प्रयोग गर्न जारी राख्न रुचाउनुहुन्छ भने, तपाइँ सेटिङहरूमा लेआउटहरू बीच स्विच गर्न सक्नुहुन्छ। केवल दिमागमा राख्नुहोस् कि नयाँ लेआउट केही हप्तामा स्थायी हुनेछ।", + "server.row.noUsername": "प्रयोगकर्ता नाम छैन", + "dialog.project.edit.title": "परियोजना सम्पादन गर्नुहोस्", + "dialog.project.edit.name": "नाम", + "dialog.project.edit.icon": "आइकन", + "dialog.project.edit.icon.alt": "परियोजना आइकन", + "dialog.project.edit.icon.hint": "छवि क्लिक गर्नुहोस् वा तान्नुहोस्", + "dialog.project.edit.icon.recommended": "सिफारिस गरिएको: 128x128px", + "dialog.project.edit.color": "रङ", + "dialog.project.edit.color.select": "{{color}} रंग चयन गर्नुहोस्", + "dialog.project.edit.worktree.startup": "कार्यस्थान स्टार्टअप स्क्रिप्ट", + "dialog.project.edit.worktree.startup.description": "नयाँ कार्यस्थान (वर्कट्री) सिर्जना गरेपछि चल्छ।", + "dialog.project.edit.worktree.startup.placeholder": "जस्तै बन स्थापना", + "dialog.releaseNotes.action.getStarted": "सुरु गर्नुहोस्", + "dialog.releaseNotes.action.next": "अर्को", + "dialog.releaseNotes.action.hideFuture": "भविष्यमा यी नदेखाउनुहोस्", + "dialog.releaseNotes.media.alt": "रिलिज पूर्वावलोकन", + "context.breakdown.title": "सन्दर्भ ब्रेकडाउन", + "context.breakdown.note": 'इनपुट टोकनहरूको अनुमानित ब्रेकडाउन। "अन्य" मा उपकरण परिभाषा र ओभरहेड समावेश छ।', + "context.breakdown.system": "प्रणाली", + "context.breakdown.user": "प्रयोगकर्ता", + "context.breakdown.assistant": "सहायक", + "context.breakdown.tool": "उपकरण कलहरू", + "context.breakdown.other": "अन्य", + "context.systemPrompt.title": "प्रणाली प्रम्प्ट", + "context.rawMessages.title": "कच्चा सन्देशहरू", + "context.export.session": "निर्यात सत्र", + "context.stats.session": "सत्र", + "context.stats.messages": "सन्देशहरू", + "context.stats.provider": "प्रदायक", + "context.stats.model": "मोडेल", + "context.stats.limit": "सन्दर्भ सीमा", + "context.stats.totalTokens": "कुल टोकनहरू", + "context.stats.usage": "प्रयोग", + "context.stats.inputTokens": "इनपुट टोकनहरू", + "context.stats.outputTokens": "आउटपुट टोकनहरू", + "context.stats.reasoningTokens": "तर्क टोकनहरू", + "context.stats.cacheTokens": "क्यास टोकनहरू (पढ्नुहोस्/लेख्नुहोस्)", + "context.stats.userMessages": "प्रयोगकर्ता सन्देशहरू", + "context.stats.assistantMessages": "सहायक सन्देशहरू", + "context.stats.totalCost": "कुल लागत", + "context.stats.sessionCreated": "सत्र सिर्जना गरियो", + "context.stats.lastActivity": "पछिल्लो गतिविधि", + "context.usage.tokens": "टोकनहरू", + "context.usage.usage": "प्रयोग", + "context.usage.cost": "लागत", + "context.usage.clickToView": "सन्दर्भ हेर्न क्लिक गर्नुहोस्", + "context.usage.view": "सन्दर्भ प्रयोग हेर्नुहोस्", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "भाषा", + "toast.language.description": "{{language}} मा स्विच गरियो", + "toast.theme.title": "विषयवस्तु बदलियो", + "toast.scheme.title": "रंग योजना", + "toast.workspace.enabled.title": "कार्यस्थान सक्षम पारियो", + "toast.workspace.enabled.description": "बहु कार्य रूखहरू अब साइडबारमा देखाइएका छन्", + "toast.workspace.disabled.title": "कार्यस्थान असक्षम पारियो", + "toast.workspace.disabled.description": "साइडबारमा मुख्य कार्य रूख मात्र देखाइएको छ", + "toast.permissions.autoaccept.on.title": "स्वत: स्वीकार गर्ने अनुमतिहरू", + "toast.permissions.autoaccept.on.description": "अनुमति अनुरोधहरू स्वचालित रूपमा स्वीकृत हुनेछन्", + "toast.permissions.autoaccept.off.title": "स्वत: स्वीकार गर्ने अनुमतिहरू रोकियो", + "toast.permissions.autoaccept.off.description": "अनुमति अनुरोधहरू अनुमोदन आवश्यक हुनेछ", + "toast.model.none.title": "कुनै मोडेल चयन गरिएको छैन", + "toast.model.none.description": "यो सत्र संक्षेप गर्न प्रदायक जडान गर्नुहोस्", + "toast.file.loadFailed.title": "फाइल लोड गर्न असफल भयो", + "toast.file.listFailed.title": "फाइलहरू सूचीबद्ध गर्न असफल भयो", + "toast.context.noLineSelection.title": "कुनै लाइन चयन छैन", + "toast.context.noLineSelection.description": "पहिले फाइल ट्याबमा रेखा दायरा चयन गर्नुहोस्।", + "toast.session.share.copyFailed.title": "URL लाई क्लिपबोर्डमा प्रतिलिपि गर्न असफल भयो", + "toast.session.share.success.title": "सत्र साझा गर्नुभयो", + "toast.session.share.success.description": "साझा गर्नुहोस् URL क्लिपबोर्डमा प्रतिलिपि गरियो!", + "toast.session.share.failed.title": "सत्र साझेदारी गर्न असफल भयो", + "toast.session.share.failed.description": "सत्र साझा गर्दा त्रुटि भयो", + "toast.session.unshare.success.title": "सत्र अनसेयर गरियो", + "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": "अपडेट उपलब्ध छ", + "toast.update.description": "OpenCode ({{version}}) को नयाँ संस्करण अब स्थापना गर्न उपलब्ध छ।", + "toast.update.action.installRestart": "स्थापना र पुन: सुरु गर्नुहोस्", + "toast.update.action.notYet": "अझै छैन", + "error.page.title": "केही गडबड भयो", + "error.page.description": "एप्लिकेसन लोड गर्दा त्रुटि भयो।", + "error.page.description.localServerStartup": "स्थानीय सर्भर सुरु गर्दा त्रुटि भयो।", + "error.page.details.label": "त्रुटि विवरण", + "error.page.action.restart": "पुन: सुरु गर्नुहोस्", + "error.page.action.report": "त्रुटि रिपोर्ट गर्नुहोस्", + "error.page.action.reported": "त्रुटि रिपोर्ट गरियो", + "error.page.action.exportLogs": "लग निर्यात गर्नुहोस्", + "error.page.action.checking": "जाँच गर्दै...", + "error.page.action.checkUpdates": "अपडेटहरूको लागि जाँच गर्नुहोस्", + "error.page.action.updateTo": "{{version}} मा अद्यावधिक गर्नुहोस्", + "error.page.circular": "[परिपत्र]", + "error.page.report.prefix": "कृपया यो त्रुटि OpenCode टोलीलाई रिपोर्ट गर्नुहोस्", + "error.page.report.discord": "Discord मा", + "error.page.version": "संस्करण: {{version}}", + "error.dev.rootNotFound": + "मूल तत्व फेला परेन। के तपाईंले यसलाई आफ्नो index.html मा थप्न बिर्सनुभयो? वा हुनसक्छ आईडी विशेषता गलत हिज्जे भयो?", + "error.serverSync.connectFailed": "सर्भरमा जडान गर्न सकिएन। `{{url}}` मा कुनै सर्भर चलिरहेको छ?", + "error.serverSDK.noServerAvailable": "कुनै सर्भर उपलब्ध छैन", + "error.serverSDK.serverNotAvailable": "सर्भर उपलब्ध छैन", + "error.childStore.persistedCacheCreateFailed": "निरन्तर क्यास सिर्जना गर्न असफल भयो", + "error.childStore.persistedProjectMetadataCreateFailed": "निरन्तर परियोजना मेटाडेटा सिर्जना गर्न असफल भयो", + "error.childStore.persistedProjectIconCreateFailed": "निरन्तर परियोजना प्रतिमा सिर्जना गर्न असफल भयो", + "error.childStore.storeCreateFailed": "स्टोर सिर्जना गर्न असफल भयो", + "directory.error.invalidUrl": "URL मा अवैध निर्देशिका।", + "error.chain.unknown": "अज्ञात त्रुटि", + "error.server.invalidConfiguration": "अवैध कन्फिगरेसन", + "error.chain.causedBy": "कारण:", + "error.chain.apiError": "API त्रुटि", + "error.chain.status": "स्थिति: {{status}}", + "error.chain.retryable": "पुन: प्रयास गर्न सकिने: {{retryable}}", + "error.chain.responseBody": "प्रतिक्रिया मुख्य भाग:\n{{body}}", + "error.chain.didYouMean": "के तपाईको मतलब: {{suggestions}}", + "error.chain.modelNotFound": "मोडेल फेला परेन: {{provider}}/{{model}}", + "error.chain.checkConfig": "आफ्नो कन्फिगरेसन (opencode.json) प्रदायक/मोडेल नामहरू जाँच गर्नुहोस्", + "error.chain.mcpFailed": + 'MCP सर्भर "{{name}}" असफल भयो। नोट गर्नुहोस्, OpenCode ले अझै पनि MCP प्रमाणीकरणलाई समर्थन गर्दैन।', + "error.chain.providerAuthFailed": "प्रदायक प्रमाणीकरण असफल भयो ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'प्रदायक "{{provider}}" प्रारम्भ गर्न असफल भयो। प्रमाण र कन्फिगरेसन जाँच गर्नुहोस्।', + "error.chain.configJsonInvalid": "{{path}} मा कन्फिग फाइल मान्य छैन JSON(C)", + "error.chain.configJsonInvalidWithMessage": "{{path}} मा कन्फिग फाइल मान्य छैन JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'डाइरेक्टरी "{{dir}}" {{path}} मा मान्य छैन। डाइरेक्टरीलाई "{{suggestion}}" मा पुन: नामाकरण गर्नुहोस् वा यसलाई हटाउनुहोस्। यो सामान्य टाइपो हो।', + "error.chain.configFrontmatterError": "{{path}} मा फ्रन्टमेटर पार्स गर्न असफल भयो:\n{{message}}", + "error.chain.configInvalid": "{{path}} मा कन्फिग फाइल अमान्य छ", + "error.chain.configInvalidWithMessage": "{{path}} मा कन्फिग फाइल अमान्य छ: {{message}}", + "notification.permission.title": "अनुमति आवश्यक छ", + "notification.permission.description": "{{sessionTitle}} मा {{projectName}} लाई अनुमति चाहिन्छ", + "notification.question.title": "प्रश्न", + "notification.question.description": "{{sessionTitle}} {{projectName}} मा एउटा प्रश्न छ", + "notification.action.goToSession": "सत्रमा जानुहोस्", + "notification.session.responseReady.title": "प्रतिक्रिया तयार छ", + "notification.session.error.title": "सत्र त्रुटि", + "notification.session.error.fallbackDescription": "एउटा त्रुटि भयो", + "home.recentProjects": "भर्खरका परियोजनाहरू", + "home.empty.title": "हालको कुनै परियोजनाहरू छैनन्", + "home.empty.description": "स्थानीय परियोजना खोलेर सुरु गर्नुहोस्", + "home.title": "घर", + "home.projects": "परियोजनाहरू", + "home.project.add": "परियोजना थप्नुहोस्", + "home.recentlyClosed": "हालै बन्द", + "home.server.collapse": "सर्भर परियोजनाहरू संक्षिप्त गर्नुहोस्", + "home.server.expand": "सर्भर परियोजनाहरू विस्तार गर्नुहोस्", + "home.sessions.search.placeholder": "खोज सत्रहरू", + "home.sessions.search.placeholder.scoped": "{{scope}} मा सत्रहरू खोज्नुहोस्", + "home.sessions.search.sessions": "सत्रहरू", + "home.sessions.search.noResults": "{{query}} को लागि कुनै सत्र फेला परेन", + "home.sessions.empty": "यहाँ अझै केही छैन", + "home.sessions.empty.description": "सुरु गर्न एक सत्र सिर्जना गर्नुहोस्", + "home.sessions.group.today": "आज", + "home.sessions.group.yesterday": "हिजो", + "home.sessions.group.older": "पुरानो", + "home.providerTip": "Claude, GPT, Gemini, आदि सहित अन्य मोडेलहरू प्रयोग गर्न ७५+ प्रदायकहरूसँग जडान गर्नुहोस्", + "session.tab.session": "सत्र", + "session.tab.review": "समीक्षा गर्नुहोस्", + "session.tab.context": "सन्दर्भ", + "session.tab.unknown": "अज्ञात सत्र", + "session.panel.reviewAndFiles": "समीक्षा र फाइलहरू", + "session.error.notFound": "यो सत्र फेला पार्न सकिँदैन", + "session.error.notFound.description": "यो ट्याबले यो सर्भरमा अब अवस्थित नभएको सत्रलाई संकेत गर्छ।", + "session.error.notFound.closeTab": "ट्याब बन्द गर्नुहोस्", + "session.error.serverConnection": "यो सर्भरमा जडान गर्न सकिँदैन", + "session.review.filesChanged": "फाइलहरू परिवर्तन गरियो {{count}}", + "session.review.change.one": "परिवर्तन गर्नुहोस्", + "session.review.change.other": "परिवर्तनहरू", + "session.review.loadingChanges": "परिवर्तनहरू लोड गर्दै...", + "session.review.empty": "यस सत्रमा अझै कुनै परिवर्तन छैन", + "session.review.noVcs": "कुनै Git संस्करण नियन्त्रण प्रणाली पत्ता लागेन, परिवर्तनहरू प्रदर्शित भएन", + "session.review.noVcs.createGit.title": "एउटा Git भण्डार सिर्जना गर्नुहोस्", + "session.review.noVcs.createGit.description": + "यस परियोजनामा ​​भएका परिवर्तनहरू ट्र्याक गर्नुहोस्, समीक्षा गर्नुहोस् र पूर्ववत गर्नुहोस्", + "session.review.noVcs.createGit.actionLoading": "Git भण्डार सिर्जना गर्दै...", + "session.review.noVcs.createGit.action": "Git भण्डार सिर्जना गर्नुहोस्", + "session.review.noSnapshot": + "स्न्यापसट ट्र्याकिङ कन्फिगरेसनमा असक्षम गरिएको छ, त्यसैले सत्र परिवर्तनहरू उपलब्ध छैनन्", + "session.review.noChanges": "कुनै परिवर्तन छैन", + "session.review.noUncommittedChanges": "अहिलेसम्म कुनै पनि असीमित परिवर्तनहरू छैनन्", + "session.review.noBranchChanges": "अझै शाखा परिवर्तन भएको छैन", + "session.files.selectToOpen": "खोल्नको लागि फाइल चयन गर्नुहोस्", + "session.files.all": "सबै फाइलहरू", + "session.files.empty": "कुनै फाइलहरू छैनन्", + "session.files.binaryContent": "बाइनरी फाइल (सामग्री प्रदर्शन गर्न सकिँदैन)", + "session.messages.renderEarlier": "पहिलेका सन्देशहरू प्रस्तुत गर्नुहोस्", + "session.messages.loadingEarlier": "पहिलेका सन्देशहरू लोड गर्दै...", + "session.messages.loadEarlier": "पहिलेका सन्देशहरू लोड गर्नुहोस्", + "session.messages.loading": "सन्देशहरू लोड गर्दै...", + "session.messages.jumpToLatest": "नवीनतममा जानुहोस्", + "session.context.addToContext": "सन्दर्भमा {{selection}} थप्नुहोस्", + "session.todo.title": "गर्नुपर्ने कामहरू", + "session.todo.collapse": "संक्षिप्त गर्नुहोस्", + "session.todo.expand": "विस्तार गर्नुहोस्", + "session.todo.progress": "{{done}} को {{total}} todos पूरा भयो", + "session.question.progress": "{{total}} प्रश्नहरूको {{current}}", + "session.question.minimize": "प्रश्न कम गर्नुहोस्", + "session.question.restore": "प्रश्न पुनर्स्थापित गर्नुहोस्", + "session.question.pending.one": "{{count}} विचाराधीन प्रश्न", + "session.question.pending.other": "{{count}} विचाराधीन प्रश्नहरू", + "session.followupDock.summary.one": "{{count}} लामबद्ध सन्देश", + "session.followupDock.summary.other": "{{count}} लामबद्ध सन्देशहरू", + "session.followupDock.sendNow": "अहिले पठाउनुहोस्", + "session.followupDock.edit": "सम्पादन गर्नुहोस्", + "session.followupDock.collapse": "लामबद्ध सन्देशहरू संक्षिप्त गर्नुहोस्", + "session.followupDock.expand": "लामबद्ध सन्देशहरू विस्तार गर्नुहोस्", + "session.revertDock.summary.one": "{{count}} रोल्ड ब्याक सन्देश", + "session.revertDock.summary.other": "{{count}} रोल्ड ब्याक सन्देशहरू", + "session.revertDock.collapse": "रोल्ड ब्याक सन्देशहरू संक्षिप्त गर्नुहोस्", + "session.revertDock.expand": "घुमाइएको सन्देशहरू विस्तार गर्नुहोस्", + "session.revertDock.restore": "सन्देश पुनर्स्थापित गर्नुहोस्", + "session.new.title": "जे पनि बनाउनुहोस्", + "session.new.project.new": "नयाँ परियोजना", + "session.new.project.search": "परियोजनाहरू खोज्नुहोस्", + "session.new.project.add": "परियोजना थप्नुहोस्", + "session.new.worktree.main": "मुख्य शाखा", + "session.new.worktree.mainWithBranch": "मुख्य शाखा ({{branch}})", + "session.new.worktree.create": "नयाँ Git वर्कट्री सिर्जना गर्नुहोस्", + "session.new.workspace.runIn": "मा सत्र चलाउनुहोस्", + "session.new.workspace.triggerLocal": "स्थानीय", + "session.new.workspace.local": "स्थानीय भण्डार", + "session.new.workspace.existing": "कार्यस्थल…", + "session.new.git.none": "छैन Git", + "session.new.lastModified": "पछिल्लो पटक परिमार्जन गरिएको", + "session.header.search.placeholder": "{{project}} खोज्नुहोस्", + "session.header.searchFiles": "फाइलहरू खोज्नुहोस्", + "session.header.openIn": "मा खोल्नुहोस्", + "session.header.open.action": "{{app}} खोल्नुहोस्", + "session.header.open.ariaLabel": "{{app}} मा खोल्नुहोस्", + "session.header.open.menu": "विकल्प खोल्नुहोस्", + "session.header.open.copyPath": "बाटो प्रतिलिपि गर्नुहोस्", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "Finder मा प्रकट गर्नुहोस्", + "session.header.reveal.fileExplorer": "File Explorer मा प्रकट गर्नुहोस्", + "session.header.reveal.containingFolder": "समावेश भएको फोल्डर खोल्नुहोस्", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "टर्मिनल", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "स्थिति", + "status.popover.ariaLabel": "सर्भर कन्फिगरेसनहरू", + "status.popover.tab.servers": "सर्भरहरू", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "प्लगइनहरू", + "status.popover.action.manageServers": "सर्भरहरू प्रबन्ध गर्नुहोस्", + "session.share.popover.title": "वेबमा प्रकाशित गर्नुहोस्", + "session.share.popover.description.shared": "यो सत्र वेबमा सार्वजनिक छ। यो लिङ्क भएको जो कोहीको लागि पहुँचयोग्य छ।", + "session.share.popover.description.unshared": + "वेबमा सार्वजनिक रूपमा सत्र साझेदारी गर्नुहोस्। यो लिङ्क भएको जो कोहीको लागि पहुँचयोग्य हुनेछ।", + "session.share.action.share": "सेयर गर्नुहोस्", + "session.share.action.publish": "प्रकाशित गर्नुहोस्", + "session.share.action.publishing": "प्रकाशन गर्दै...", + "session.share.action.unpublish": "अप्रकाशित", + "session.share.action.unpublishing": "अप्रकाशित...", + "session.share.action.view": "हेर्नुहोस्", + "session.share.copy.copied": "प्रतिलिपि गरियो", + "session.share.copy.copyLink": "लिङ्क प्रतिलिपि गर्नुहोस्", + "lsp.tooltip.none": "कुनै LSP सर्भरहरू छैनन्", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "प्रम्प्ट लोड गर्दै...", + "terminal.loading": "टर्मिनल लोड गर्दै...", + "terminal.title": "टर्मिनल", + "terminal.title.numbered": "टर्मिनल {{number}}", + "terminal.close": "टर्मिनल बन्द गर्नुहोस्", + "terminal.connectionLost.title": "जडान हरायो", + "terminal.connectionLost.abnormalClose": "WebSocket असामान्य रूपमा बन्द भयो: {{code}}", + "terminal.connectionLost.description": "टर्मिनल जडान अवरुद्ध भयो। सर्भर पुन: सुरु हुँदा यो हुन सक्छ।", + "terminal.connectTicket.csrfError": + "PTY जडान टिकट उत्पत्ति वा CSRF चेक द्वारा अस्वीकार गरियो। सर्भर CORS कन्फिगरेसन जाँच गर्नुहोस्।", + "terminal.connectTicket.statusError": "PTY जडान टिकट {{status}} सँग असफल भयो", + "titlebar.update": "अपडेट गर्नुहोस्", + "titlebar.updateVersion": "अपडेट गर्नुहोस् {{version}}", + "common.closeTab": "ट्याब बन्द गर्नुहोस्", + "common.dismiss": "खारेज गर्नुहोस्", + "common.moreCountSuffix": "(+{{count}} थप)", + "common.requestFailed": "अनुरोध असफल भयो", + "common.moreOptions": "थप विकल्पहरू", + "common.learnMore": "थप जान्नुहोस्", + "common.rename": "पुन: नामाकरण गर्नुहोस्", + "common.export": "निर्यात गर्नुहोस्", + "common.reset": "रिसेट गर्नुहोस्", + "common.archive": "अभिलेख", + "common.delete": "मेट्नुहोस्", + "common.close": "बन्द गर्नुहोस्", + "common.edit": "सम्पादन गर्नुहोस्", + "common.loadMore": "थप लोड गर्नुहोस्", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "अज्ञात", + "common.time.justNow": "भर्खरै", + "common.time.minutesAgo.short": "{{count}}मिनेट अघि", + "common.time.hoursAgo.short": "{{count}}घन्टा अघि", + "common.time.daysAgo.short": "{{count}}d अघि", + "sidebar.menu.toggle": "मेनु टगल गर्नुहोस्", + "sidebar.nav.projectsAndSessions": "परियोजनाहरू र सत्रहरू", + "sidebar.settings": "सेटिङहरू", + "sidebar.help": "मद्दत गर्नुहोस्", + "sidebar.workspaces.enable": "कार्यस्थान सक्षम गर्नुहोस्", + "sidebar.workspaces.disable": "कार्यस्थानहरू असक्षम गर्नुहोस्", + "sidebar.gettingStarted.title": "सुरु गर्दै", + "sidebar.gettingStarted.line1": "OpenCode मा नि:शुल्क मोडेलहरू समावेश छन् ताकि तपाईं तुरुन्तै सुरु गर्न सक्नुहुन्छ।", + "sidebar.gettingStarted.line2": "मोडेलहरू, inc प्रयोग गर्न कुनै पनि प्रदायक जडान गर्नुहोस्। Claude, GPT, Gemini आदि।", + "sidebar.project.recentSessions": "भर्खरका सत्रहरू", + "sidebar.project.viewAllSessions": "सबै सत्रहरू हेर्नुहोस्", + "sidebar.project.clearNotifications": "सूचनाहरू खाली गर्नुहोस्", + "sidebar.empty.title": "कुनै पनि आयोजना खुलेका छैनन्", + "sidebar.empty.description": "सुरु गर्न एउटा परियोजना खोल्नुहोस्", + "debugBar.ariaLabel": "विकास प्रदर्शन निदान", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "अन्तिम सम्पन्न भएको रुट ट्रान्जिसन सत्र पृष्ठलाई छुँदै, राउटर स्टार्टबाट पहिलो पेन्ट सेटल भएपछि मापन गरिन्छ।", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "अन्तिम 5 सेकेन्डमा प्रति सेकेन्ड फ्रेमहरू रोलिङ।", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "पछिल्लो ५ सेकेन्डमा सबैभन्दा खराब फ्रेम समय।", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "पछिल्लो 5 सेकेन्डमा 32ms भन्दा बढी फ्रेमहरू।", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "पछिल्लो 5 सेकेन्डमा अवरुद्ध समय र लामो-कार्य गणना। अधिकतम कार्य: {{max}}।", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "पछिल्लो 5 सेकेन्डमा सबैभन्दा खराब अवलोकन इनपुट ढिलाइ।", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "पछिल्लो ५ सेकेन्डमा अन्तरक्रियाको अनुमानित अवधि। यो INP-जस्तो हो, आधिकारिक Web Vitals INP होइन।", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "हालको एपको जीवनकालको लागि संचयी लेआउट शिफ्ट।", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "JS हीप बनाम हिप सीमा प्रयोग गरियो। Chromium मात्र।", + "debugBar.mem.tip": "JS हीप बनाम हिप सीमा प्रयोग गरियो। {{limit}} को {{used}}।", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "सबै अन्तरक्रियात्मक तत्वहरूमा फोकस शैलीहरू बल गर्नुहोस्", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "बन्द", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "चयन गरिएको भाषा परिवर्तन नगरी पूर्ण एप लेआउट दिशा बल गर्नुहोस्", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode डेस्कटप", + "settings.section.desktop": "डेस्कटप", + "settings.section.server": "सर्भर", + "settings.tab.general": "सामान्य", + "settings.tab.shortcuts": "सर्टकटहरू", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL एकीकरण", + "settings.desktop.wsl.description": "OpenCode सर्भर भित्र WSL Windows मा चलाउनुहोस्।", + "settings.general.section.appearance": "उपस्थिति", + "settings.general.section.advanced": "उन्नत", + "settings.general.section.notifications": "प्रणाली सूचनाहरू", + "settings.general.section.updates": "अपडेटहरू", + "settings.general.section.sounds": "ध्वनि प्रभावहरू", + "settings.general.section.feed": "फिड", + "settings.general.section.display": "प्रदर्शन", + "settings.general.row.language.title": "भाषा", + "settings.general.row.language.description": "OpenCode को लागि प्रदर्शन भाषा परिवर्तन गर्नुहोस्", + "settings.general.row.shell.title": "टर्मिनल शेल", + "settings.general.row.shell.description": "टर्मिनल र एजेन्ट उपकरणहरू द्वारा प्रयोग गरिएको शेल", + "settings.general.row.shell.autoDefault": "स्वतः (पूर्वनिर्धारित)", + "settings.general.row.shell.terminalOnly": "टर्मिनल मात्र", + "settings.general.row.appearance.title": "उपस्थिति", + "settings.general.row.appearance.description": "आफ्नो यन्त्रमा OpenCode कस्तो देखिन्छ अनुकूलित गर्नुहोस्", + "settings.general.row.colorScheme.title": "रंग योजना", + "settings.general.row.colorScheme.description": + "OpenCode ले प्रणाली, उज्यालो वा गाढा विषयवस्तुलाई फलो गर्छ कि गर्दैन भन्ने छनौट गर्नुहोस्", + "settings.general.row.theme.title": "विषयवस्तु", + "settings.general.row.theme.description": "कसरी OpenCode विषयवस्तुलाई अनुकूलित गर्नुहोस्।", + "settings.general.row.font.title": "कोड फन्ट", + "settings.general.row.font.description": "कोड ब्लकहरूमा प्रयोग गरिएको फन्ट अनुकूलित गर्नुहोस्", + "settings.general.row.terminalFont.title": "टर्मिनल फन्ट", + "settings.general.row.terminalFont.description": "टर्मिनलमा प्रयोग गरिएको फन्ट अनुकूलन गर्नुहोस्", + "settings.general.row.uiFont.title": "UI फन्ट", + "settings.general.row.uiFont.description": "इन्टरफेसमा प्रयोग गरिएको फन्ट अनुकूलित गर्नुहोस्", + "settings.general.row.followup.title": "अनुगमन व्यवहार", + "settings.general.row.followup.description": "छनोट गर्नुहोस् कि फलो-अपले तुरुन्तै स्टेयर गर्ने वा लाममा पर्खनुहोस्", + "settings.general.row.followup.option.queue": "लाम", + "settings.general.row.followup.option.steer": "स्टेयर", + "settings.general.row.showFileTree.title": "फाइल रूख", + "settings.general.row.showFileTree.description": "सत्रहरूमा फाइल ट्री प्यानल देखाउनुहोस्", + "settings.general.row.showNavigation.title": "नेभिगेसन नियन्त्रणहरू", + "settings.general.row.showNavigation.description": "डेस्कटप शीर्षक पट्टीमा पछाडि र अगाडि बटनहरू देखाउनुहोस्", + "settings.general.row.showSearch.title": "आदेश प्यालेट", + "settings.general.row.showSearch.description": "शीर्षक पट्टीमा खोज र आदेश प्यालेट बटन देखाउनुहोस्", + "settings.general.row.showTerminal.title": "टर्मिनल", + "settings.general.row.showTerminal.description": "डेस्कटप शीर्षक पट्टीमा टर्मिनल बटन देखाउनुहोस्", + "settings.general.row.showStatus.title": "सर्भर स्थिति", + "settings.general.row.showStatus.description": "शीर्षक पट्टीमा सर्भर स्थिति बटन देखाउनुहोस्", + "settings.general.row.mobileTitlebarBottom.title": "तल्लो नेभिगेसन", + "settings.general.row.mobileTitlebarBottom.description": + "मोबाइलमा स्क्रिनको फेदमा शीर्षक पट्टी र सत्र ट्याबहरू राख्नुहोस्", + "settings.general.row.showCustomAgents.title": "एजेन्ट देखाउनुहोस्", + "settings.general.row.showCustomAgents.description": + "कम्पोजरमा एजेन्टहरू बीच स्विच गर्नुहोस्। लुकेको बेला, बिल्ड एजेन्टमा पूर्वनिर्धारित हुन्छ।", + "settings.general.row.reasoningSummaries.title": "तर्क सारांशहरू देखाउनुहोस्", + "settings.general.row.reasoningSummaries.description": "टाइमलाइनमा मोडेल तर्क सारांशहरू प्रदर्शन गर्नुहोस्", + "settings.general.row.shellToolPartsExpanded.title": "शेल उपकरणका भागहरू विस्तार गर्नुहोस्", + "settings.general.row.shellToolPartsExpanded.description": + "टाइमलाइनमा पूर्वनिर्धारित रूपमा विस्तारित शेल उपकरण भागहरू देखाउनुहोस्", + "settings.general.row.editToolPartsExpanded.title": "सम्पादन उपकरण भागहरू विस्तार गर्नुहोस्", + "settings.general.row.editToolPartsExpanded.description": + "टाइमलाइनमा पूर्वनिर्धारित रूपमा विस्तार गरिएको सम्पादन, लेख्नुहोस् र प्याच उपकरण भागहरू देखाउनुहोस्", + "settings.general.row.newInterface.title": "नयाँ लेआउट", + "settings.general.row.newInterface.badge": "नयाँ", + "settings.general.row.newInterface.description": + "नयाँ ट्याबहरू र गृह लेआउट प्रयोग गर्नुहोस्। सीमित समयको लागि लेआउटहरू बीच स्विच गर्नुहोस्।", + "settings.general.row.newInterfaceNotice.title": "तपाईं अहिले नयाँ लेआउट प्रयोग गर्दै हुनुहुन्छ", + "settings.general.row.newInterfaceNotice.description": "अघिल्लो लेआउट अब उपलब्ध छैन", + "settings.general.row.newInterfaceNotice.dismiss": "खारेज गर्नुहोस्", + "settings.general.row.pinchZoom.title": "जुम गर्न पिन्च गर्नुहोस्", + "settings.general.row.pinchZoom.description": + "ट्र्याकप्याड पिन्च र Ctrl-स्क्रोल इशाराहरूलाई जुम गर्न अनुमति दिनुहोस्", + "settings.general.row.wayland.title": "नेटिभ Wayland प्रयोग गर्नुहोस्", + "settings.general.row.wayland.description": "Wayland मा X11 फलब्याक असक्षम गर्नुहोस्। पुन: सुरु गर्न आवश्यक छ।", + "settings.general.row.wayland.tooltip": "Linux मा मिश्रित रिफ्रेस-रेट मनिटरहरूसँग, नेटिभ Wayland थप स्थिर हुन सक्छ।", + "settings.general.row.releaseNotes.title": "रिलिज नोटहरू", + "settings.general.row.releaseNotes.description": "अपडेट पछि के नयाँ पपअपहरू देखाउनुहोस्", + "settings.updates.row.startup.title": "स्टार्टअपमा अद्यावधिकहरूको लागि जाँच गर्नुहोस्", + "settings.updates.row.startup.description": "OpenCode सुरु हुँदा अद्यावधिकहरूको लागि स्वचालित रूपमा जाँच गर्नुहोस्", + "settings.updates.row.check.title": "अपडेटहरूको लागि जाँच गर्नुहोस्", + "settings.updates.row.check.description": + "म्यानुअल रूपमा अद्यावधिकहरूको लागि जाँच गर्नुहोस् र यदि उपलब्ध छ भने स्थापना गर्नुहोस्", + "settings.updates.action.checkNow": "अहिले जाँच गर्नुहोस्", + "settings.updates.action.checking": "जाँच गर्दै...", + "settings.updates.action.downloading": "डाउनलोड गर्दै...", + "settings.updates.action.installing": "स्थापना गर्दै...", + "settings.updates.toast.latest.title": "तपाईं अद्यावधिक हुनुहुन्छ", + "settings.updates.toast.latest.description": "तपाईं OpenCode को नवीनतम संस्करण चलाइरहनुभएको छ।", + "sound.option.none": "कुनै पनि होइन", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "एजेन्ट", + "settings.general.notifications.agent.description": + "एजेन्ट पूरा हुँदा वा ध्यान आवश्यक हुँदा प्रणाली सूचना देखाउनुहोस्", + "settings.general.notifications.permissions.title": "अनुमतिहरू", + "settings.general.notifications.permissions.description": "अनुमति आवश्यक हुँदा प्रणाली सूचना देखाउनुहोस्", + "settings.general.notifications.errors.title": "त्रुटिहरू", + "settings.general.notifications.errors.description": "त्रुटि हुँदा प्रणाली सूचना देखाउनुहोस्", + "settings.general.sounds.agent.title": "एजेन्ट", + "settings.general.sounds.agent.description": "जब एजेन्ट पूर्ण हुन्छ वा ध्यान चाहिन्छ ध्वनि बजाउनुहोस्", + "settings.general.sounds.permissions.title": "अनुमतिहरू", + "settings.general.sounds.permissions.description": "अनुमति आवश्यक हुँदा ध्वनि बजाउनुहोस्", + "settings.general.sounds.errors.title": "त्रुटिहरू", + "settings.general.sounds.errors.description": "त्रुटि हुँदा ध्वनि बजाउनुहोस्", + "settings.shortcuts.title": "किबोर्ड सर्टकटहरू", + "settings.shortcuts.reset.button": "पूर्वनिर्धारितमा रिसेट गर्नुहोस्", + "settings.shortcuts.reset.toast.title": "सर्टकट रिसेट", + "settings.shortcuts.reset.toast.description": "किबोर्ड सर्टकटहरू पूर्वनिर्धारितमा रिसेट गरिएका छन्।", + "settings.shortcuts.conflict.title": "सर्टकट पहिले नै प्रयोगमा छ", + "settings.shortcuts.conflict.description": "{{keybind}} पहिले नै {{titles}} लाई तोकिएको छ।", + "settings.shortcuts.unassigned": "तोकिएको छैन", + "settings.shortcuts.pressKeys": "कुञ्जीहरू थिच्नुहोस्", + "settings.shortcuts.search.placeholder": "सर्टकट खोज्नुहोस्", + "settings.shortcuts.search.empty": "कुनै सर्टकट फेला परेन", + "settings.shortcuts.group.general": "सामान्य", + "settings.shortcuts.group.session": "सत्र", + "settings.shortcuts.group.navigation": "नेभिगेसन", + "settings.shortcuts.group.modelAndAgent": "मोडेल र एजेन्ट", + "settings.shortcuts.group.terminal": "टर्मिनल", + "settings.shortcuts.group.prompt": "प्रम्प्ट", + "settings.providers.title": "प्रदायकहरू", + "settings.providers.description": "प्रदायक सेटिङहरू यहाँ कन्फिगर योग्य हुनेछ।", + "settings.providers.section.connected": "जडान गरिएका प्रदायकहरू", + "settings.providers.connected.empty": "जडान गरिएका प्रदायकहरू छैनन्", + "settings.providers.connected.environmentDescription": "तपाईंको वातावरण चरबाट जडान गरिएको", + "settings.providers.section.popular": "लोकप्रिय प्रदायकहरू", + "settings.providers.custom.description": "आधार URL द्वारा एक OpenAI-कम्प्याटिबल प्रदायक थप्नुहोस्।", + "settings.providers.tag.environment": "वातावरण", + "settings.providers.tag.config": "कन्फिग", + "settings.providers.tag.custom": "अनुकूलन", + "settings.providers.tag.other": "अन्य", + "settings.models.title": "मोडेलहरू", + "settings.models.description": "मोडेल सेटिङहरू यहाँ कन्फिगर योग्य हुनेछ।", + "settings.agents.title": "एजेन्टहरू", + "settings.agents.description": "एजेन्ट सेटिङहरू यहाँ कन्फिगर योग्य हुनेछ।", + "settings.commands.title": "आदेशहरू", + "settings.commands.description": "आदेश सेटिङहरू यहाँ कन्फिगर योग्य हुनेछ।", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP सेटिङहरू यहाँ कन्फिगर योग्य हुनेछन्।", + "settings.permissions.title": "अनुमतिहरू", + "settings.permissions.description": "सर्भरले पूर्वनिर्धारित रूपमा प्रयोग गर्न सक्ने उपकरणहरू नियन्त्रण गर्नुहोस्।", + "settings.permissions.section.tools": "उपकरणहरू", + "settings.permissions.toast.updateFailed.title": "अनुमतिहरू अद्यावधिक गर्न असफल भयो", + "settings.permissions.action.allow": "अनुमति दिनुहोस्", + "settings.permissions.action.ask": "सोध्नुहोस्", + "settings.permissions.action.deny": "अस्वीकार गर्नुहोस्", + "settings.permissions.tool.read.title": "पढ्नुहोस्", + "settings.permissions.tool.read.description": "फाइल पढ्दै (फाइल पथसँग मेल खान्छ)", + "settings.permissions.tool.edit.title": "सम्पादन गर्नुहोस्", + "settings.permissions.tool.edit.description": "सम्पादनहरू, लेखहरू, र प्याचहरू सहित फाइलहरू परिमार्जन गर्नुहोस्", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "ग्लोब ढाँचाहरू प्रयोग गरेर फाइलहरू मिलाउनुहोस्", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "नियमित अभिव्यक्ति प्रयोग गरी फाइल सामग्री खोज्नुहोस्", + "settings.permissions.tool.list.title": "सूची", + "settings.permissions.tool.list.description": "निर्देशिका भित्र फाइलहरू सूचीबद्ध गर्नुहोस्", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "शेल आदेशहरू चलाउनुहोस्", + "settings.permissions.tool.task.title": "कार्य", + "settings.permissions.tool.task.description": "उप-एजेन्टहरू सुरु गर्नुहोस्", + "settings.permissions.tool.skill.title": "सीप", + "settings.permissions.tool.skill.description": "नाम द्वारा एक कौशल लोड गर्नुहोस्", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "भाषा सर्भर प्रश्नहरू चलाउनुहोस्", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "कार्य सूची अद्यावधिक गर्नुहोस्", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "URL बाट सामग्री प्राप्त गर्नुहोस्", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "वेब खोज्नुहोस्", + "settings.permissions.tool.external_directory.title": "बाह्य निर्देशिका", + "settings.permissions.tool.external_directory.description": "परियोजना निर्देशिका बाहिर फाइलहरू पहुँच गर्नुहोस्", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "उस्तै इनपुटको साथ बारम्बार उपकरण कलहरू पत्ता लगाउनुहोस्", + "session.delete.failed.title": "सत्र मेटाउन असफल भयो", + "session.delete.title": "सत्र मेटाउनुहोस्", + "session.delete.confirm": 'सत्र "{{name}}" मेटाउने हो?', + "session.delete.button": "सत्र मेटाउनुहोस्", + "workspace.new": "नयाँ कार्यस्थान", + "workspace.type.local": "स्थानीय", + "workspace.type.sandbox": "स्यान्डबक्स", + "workspace.create.failed.title": "कार्यस्थान सिर्जना गर्न असफल भयो", + "workspace.delete.failed.title": "कार्यस्थान मेटाउन सकिएन", + "workspace.resetting.title": "कार्यस्थान रिसेट गर्दै", + "workspace.resetting.description": "यसले एक मिनेट लिन सक्छ।", + "workspace.reset.failed.title": "कार्यस्थान रिसेट गर्न असफल भयो", + "workspace.reset.success.title": "कार्यस्थान रिसेट", + "workspace.reset.success.description": "कार्यस्थान अब पूर्वनिर्धारित शाखासँग मेल खान्छ।", + "workspace.error.stillPreparing": "कार्यस्थान अझै तयार हुँदै छ", + "workspace.status.checking": "मर्ज नगरिएका परिवर्तनहरूको लागि जाँच गर्दै...", + "workspace.status.error": "Git स्थिति प्रमाणित गर्न असमर्थ।", + "workspace.status.clean": "मर्ज नगरिएका परिवर्तनहरू फेला परेनन्।", + "workspace.status.dirty": "यस कार्यस्थानमा अनमर्ज गरिएका परिवर्तनहरू पत्ता लगाइयो।", + "workspace.delete.title": "कार्यस्थान मेटाउनुहोस्", + "workspace.delete.confirm": 'कार्यस्थान "{{name}}" मेट्ने हो?', + "workspace.delete.button": "कार्यस्थान मेटाउनुहोस्", + "workspace.reset.title": "कार्यस्थान रिसेट गर्नुहोस्", + "workspace.reset.confirm": 'कार्यस्थान "{{name}}" रिसेट गर्ने हो?', + "workspace.reset.button": "कार्यस्थान रिसेट गर्नुहोस्", + "workspace.reset.archived.none": "कुनै पनि सक्रिय सत्रहरू अभिलेख गरिने छैन।", + "workspace.reset.archived.one": "1 सत्र अभिलेख गरिनेछ।", + "workspace.reset.archived.many": "{{count}} सत्रहरू अभिलेख गरिनेछ।", + "workspace.reset.note": "यसले पूर्वनिर्धारित शाखासँग मिलाउन कार्यस्थान रिसेट गर्नेछ।", + "dialog.usageExceeded.dontShowAgain": "फेरि नदेखाउनुहोस्", +} diff --git a/packages/app/src/i18n/nl.ts b/packages/app/src/i18n/nl.ts new file mode 100644 index 0000000000000000000000000000000000000000..6fb1dc09f24b2c01a3f401e71bf8051ce3c0b86e --- /dev/null +++ b/packages/app/src/i18n/nl.ts @@ -0,0 +1,1171 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Bestand", + "desktop.menu.edit": "Bewerken", + "desktop.menu.view": "Weergave", + "desktop.menu.go": "Ga", + "desktop.menu.window": "Venster", + "desktop.menu.help": "Help", + "desktop.menu.checkForUpdates": "Controleren op updates...", + "desktop.menu.settings": "Instellingen", + "desktop.menu.reloadWebview": "Webweergave opnieuw laden", + "desktop.menu.restart": "Opnieuw opstarten", + "desktop.menu.exportLogs": "Logboeken exporteren...", + "desktop.menu.newSession": "Nieuwe sessie", + "desktop.menu.openProject": "Project openen...", + "desktop.menu.newWindow": "Nieuw venster", + "desktop.menu.closeWindow": "Venster sluiten", + "desktop.menu.undo": "Ongedaan maken", + "desktop.menu.redo": "Opnieuw uitvoeren", + "desktop.menu.cut": "Knippen", + "desktop.menu.copy": "Kopiëren", + "desktop.menu.paste": "Plakken", + "desktop.menu.delete": "Verwijderen", + "desktop.menu.selectAll": "Alles selecteren", + "desktop.menu.toggleSidebar": "Zijbalk tonen of verbergen", + "desktop.menu.toggleTerminal": "Terminal tonen of verbergen", + "desktop.menu.toggleFileTree": "Bestandsstructuur tonen of verbergen", + "desktop.menu.reload": "Opnieuw laden", + "desktop.menu.toggleDeveloperTools": "Ontwikkelaarstools tonen of verbergen", + "desktop.menu.actualSize": "Werkelijke grootte", + "desktop.menu.zoomIn": "Inzoomen", + "desktop.menu.zoomOut": "Uitzoomen", + "desktop.menu.toggleFullScreen": "Schermvullende weergave in- of uitschakelen", + "desktop.menu.back": "Terug", + "desktop.menu.forward": "Vooruit", + "desktop.menu.previousSession": "Vorige sessie", + "desktop.menu.nextSession": "Volgende sessie", + "desktop.menu.previousProject": "Vorig project", + "desktop.menu.nextProject": "Volgend project", + "desktop.menu.minimize": "Minimaliseren", + "desktop.menu.maximize": "Maximaliseren", + "desktop.menu.documentation": "OpenCode-documentatie", + "desktop.menu.supportForum": "Supportforum", + "desktop.menu.shareFeedback": "Feedback delen", + "desktop.menu.reportBug": "Een bug melden", + "desktop.menu.ariaLabel": "OpenCode-menu", + "desktop.updater.dialog.checkFailed.message": "Controleren op updates is mislukt.", + "desktop.updater.dialog.checkFailed.title": "Updatefout", + "desktop.updater.dialog.upToDate.message": "Je bent helemaal bijgewerkt.", + "desktop.updater.dialog.upToDate.title": "Geen updates", + "desktop.updater.dialog.ready.message": "Update {{version}} is gedownload. Nu opnieuw opstarten?", + "desktop.updater.dialog.ready.title": "Update gereed", + "desktop.updater.dialog.restart": "Opnieuw opstarten", + "desktop.updater.dialog.later": "Later", + "desktop.recovery.action.relaunch": "Opnieuw openen", + "desktop.recovery.action.exportLogs": "Logboeken exporteren", + "desktop.recovery.action.keepWaiting": "Blijven wachten", + "desktop.recovery.action.quit": "Stoppen", + "desktop.recovery.loadFailed": "OpenCode kan niet worden geladen", + "desktop.recovery.terminated": "Het OpenCode-venster is onverwacht beëindigd", + "desktop.recovery.unresponsive": "OpenCode reageert niet", + "desktop.recovery.unresponsive.detail": "Je kunt de app opnieuw openen, de logboeken openen of blijven wachten.", + "desktop.recovery.loadFailed.detail": "Venster: {{window}}\nURL: {{url}}\nFout: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Venster: {{window}}\nReden: {{reason}}\nCode: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Kies een map", + "desktop.dialog.chooseFile": "Kies een bestand", + "desktop.dialog.saveFile": "Bestand opslaan", + "desktop.dialog.files": "Bestanden", + "desktop.server.local": "Lokale server", + "desktop.wsl.error.windowsOnly": "WSL is alleen beschikbaar op Windows", + "desktop.wsl.error.unavailable": "WSL is niet beschikbaar", + "desktop.wsl.error.listInstalled": "Kan geïnstalleerde WSL-distributies niet weergeven", + "desktop.wsl.error.listOnline": "Kan online WSL-distributies niet weergeven", + "desktop.wsl.error.executeDistro": "Kan geen opdrachten uitvoeren in de distributie", + "desktop.wsl.error.installWsl": "Installatie van WSL is mislukt", + "desktop.wsl.error.installDistro": "Kan distributie niet installeren: {{distro}}", + "desktop.wsl.error.installOpencode": "Installatie van OpenCode is mislukt", + "desktop.wsl.error.alreadyAdded": "{{distro}} is al toegevoegd", + "desktop.wsl.error.opencodeMissing": "opencode is niet geïnstalleerd in deze distributie", + "desktop.wsl.error.opencodeCannotRun": "opencode is geïnstalleerd, maar kan niet worden uitgevoerd", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode is niet geïnstalleerd in {{distro}}", + "desktop.wsl.error.updateVersion": + "De OpenCode-update is voltooid, maar {{distro}} meldt nog steeds {{installed}}; verwacht: {{expected}}", + "desktop.wsl.error.noVersion": "geen versie", + "desktop.wsl.error.serverExited": "WSL-server is na het opstarten afgesloten (code={{code}} signaal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL-server is afgesloten voordat deze gereed was (code={{code}} signaal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Time-out bij statuscontrole van sidecar voor {{distro}} na {{timeout}} ms", + "desktop.wsl.error.commandTimeout": "Time-out voor {{command}} {{args}} na {{timeout}} ms", + "desktop.wsl.error.failedPort": "Kan poort niet ophalen", + "desktop.picker.error.notSelected": "Er is geen bestand geselecteerd in de bestandskiezer", + "desktop.picker.error.sizeLimit": "De geselecteerde bijlagen overschrijden de limiet van {{limit}} MB", + "command.category.suggested": "Voorgesteld", + "command.category.view": "Weergave", + "command.category.project": "Project", + "command.category.provider": "Aanbieder", + "command.category.server": "Server", + "command.category.session": "Sessie", + "command.category.theme": "Thema", + "command.category.language": "Taal", + "command.category.file": "Bestand", + "command.category.context": "Context", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Machtigingen", + "command.category.workspace": "Werkruimte", + "command.category.settings": "Instellingen", + "command.logs.export": "Logboeken exporteren", + "theme.scheme.system": "Systeem", + "theme.scheme.light": "Licht", + "theme.scheme.dark": "Donker", + "command.sidebar.toggle": "Zijbalk tonen of verbergen", + "command.project.open": "Project openen", + "command.project.previous": "Vorig project", + "command.project.next": "Volgende project", + "command.project.index": "Ga naar project {{index}}", + "command.provider.connect": "Verbinding maken met aanbieder", + "command.server.switch": "Van server wisselen", + "command.settings.open": "Instellingen openen", + "command.session.previous": "Vorige sessie", + "command.session.next": "Volgende sessie", + "command.session.previous.unseen": "Vorige ongelezen sessie", + "command.session.next.unseen": "Volgende ongelezen sessie", + "command.session.archive": "Sessie archiveren", + "command.palette": "Commandopalet", + "command.theme.cycle": "Volgend thema", + "command.theme.set": "Gebruik thema: {{theme}}", + "command.theme.scheme.cycle": "Volgend kleurenschema", + "command.theme.scheme.set": "Gebruik kleurenschema: {{scheme}}", + "command.language.cycle": "Volgende taal", + "command.language.set": "Gebruik taal: {{language}}", + "command.session.new": "Nieuwe sessie", + "command.file.open": "Bestand openen", + "command.tab.close": "Tabblad sluiten", + "command.tab.reopenClosed": "Gesloten tabblad opnieuw openen", + "command.context.addSelection": "Voeg selectie toe aan context", + "command.context.addSelection.description": "Voeg geselecteerde regels uit het huidige bestand toe", + "command.input.focus": "Invoerveld activeren", + "command.terminal.toggle": "Terminal tonen of verbergen", + "command.fileTree.toggle": "Bestandsstructuur tonen of verbergen", + "command.review.toggle": "Beoordeling tonen of verbergen", + "command.terminal.new": "Nieuwe terminal", + "command.terminal.new.description": "Maak een nieuw terminaltabblad", + "command.steps.toggle": "Stappen tonen of verbergen", + "command.steps.toggle.description": "Toon of verberg stappen voor het huidige bericht", + "command.message.previous": "Vorig bericht", + "command.message.previous.description": "Ga naar het vorige gebruikersbericht", + "command.message.next": "Volgend bericht", + "command.message.next.description": "Ga naar het volgende gebruikersbericht", + "command.model.choose": "Kies model", + "command.model.choose.description": "Selecteer een ander model", + "command.mcp.toggle": "MCP's in- of uitschakelen", + "command.mcp.toggle.description": "MCP's in- of uitschakelen", + "command.agent.cycle": "Volgende agent", + "command.agent.cycle.description": "Schakel over naar de volgende agent", + "command.agent.cycle.reverse": "Vorige agent", + "command.agent.cycle.reverse.description": "Schakel over naar de vorige agent", + "command.model.variant.cycle": "Volgend denkniveau", + "command.model.variant.cycle.description": "Ga naar het volgende inspanningsniveau", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Machtigingen automatisch accepteren", + "command.permissions.autoaccept.disable": "Stop met het automatisch accepteren van machtigingen", + "command.workspace.toggle": "Werkruimten in- of uitschakelen", + "command.workspace.toggle.description": "Schakel meerdere werkruimten in de zijbalk in of uit", + "command.session.undo": "Ongedaan maken", + "command.session.undo.description": "Maak het laatste bericht ongedaan", + "command.session.redo": "Opnieuw uitvoeren", + "command.session.redo.description": "Voer het laatste ongedaan gemaakte bericht opnieuw uit", + "command.session.compact": "Sessie comprimeren", + "command.session.compact.description": "Vat de sessie samen om de contextgrootte te verkleinen", + "command.session.fork": "Afsplitsen vanaf bericht", + "command.session.fork.description": "Maak een nieuwe sessie op basis van een eerder bericht", + "command.session.share": "Sessie delen", + "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", + "palette.group.commands": "Commando's", + "palette.group.files": "Bestanden", + "dialog.provider.search.placeholder": "Aanbieders zoeken", + "dialog.provider.empty": "Geen aanbieders gevonden", + "dialog.provider.group.popular": "Populair", + "dialog.provider.group.other": "Anders", + "dialog.provider.custom.label": "Aangepaste OpenAI-compatibele aanbieder", + "dialog.provider.tag.recommended": "Aanbevolen", + "dialog.provider.opencode.note": "Samengestelde modellen, waaronder Claude, GPT, Gemini en meer", + "dialog.provider.opencode.tagline": "Betrouwbare geoptimaliseerde modellen", + "dialog.provider.opencodeGo.tagline": "Goedkoop abonnement voor iedereen", + "dialog.provider.anthropic.note": "Directe toegang tot Claude-modellen, inclusief Pro en Max", + "dialog.provider.copilot.note": "AI-modellen voor codeerondersteuning via GitHub Copilot", + "dialog.provider.openai.note": "GPT-modellen voor snelle, capabele algemene AI-taken", + "dialog.provider.google.note": "Gemini-modellen voor snelle, gestructureerde reacties", + "dialog.provider.openrouter.note": "Toegang tot alle ondersteunde modellen via één aanbieder", + "dialog.provider.vercel.note": "Uniforme toegang tot AI-modellen met slimme routering", + "dialog.model.select.title": "Selecteer model", + "dialog.model.search.placeholder": "Zoek modellen", + "dialog.model.empty": "Geen modelresultaten", + "dialog.model.manage": "Beheer modellen", + "dialog.model.manage.description": "Pas aan welke modellen in de modelkiezer verschijnen.", + "dialog.model.manage.provider.toggle": "Alle {{provider}}-modellen in- of uitschakelen", + "dialog.model.unpaid.freeModels.title": "Gratis modellen geleverd door OpenCode", + "dialog.model.unpaid.addMore.title": "Voeg meer modellen toe van populaire aanbieders", + "dialog.model.unpaid.viewMoreProviders": "Bekijk nog 70+ aanbieders", + "dialog.provider.viewAll": "Toon meer aanbieders", + "provider.connect.title": "Verbinding maken met {{provider}}", + "provider.connect.title.anthropicProMax": "Inloggen met Claude Pro/Max", + "provider.connect.selectMethod": "Selecteer de aanmeldingsmethode voor {{provider}}.", + "provider.connect.method.apiKey": "API-sleutel", + "provider.connect.method.browser": "Browser", + "provider.connect.method.headless": "Headless", + "provider.connect.status.inProgress": "Autorisatie in behandeling...", + "provider.connect.status.waiting": "Wachten op autorisatie...", + "provider.connect.status.failed": "Autorisatie mislukt: {{error}}", + "provider.connect.apiKey.description": + "Voer je {{provider}}-API-sleutel in om je account te koppelen en {{provider}}-modellen in OpenCode te gebruiken.", + "provider.connect.apiKey.label": "{{provider}}-API-sleutel", + "provider.connect.apiKey.placeholder": "API-sleutel", + "provider.connect.apiKey.required": "API-sleutel is vereist", + "provider.connect.opencodeZen.line1": + "OpenCode Zen geeft je toegang tot een samengestelde set betrouwbare, geoptimaliseerde modellen voor codeeragenten.", + "provider.connect.opencodeZen.line2": + "Met één API-sleutel krijg je toegang tot modellen zoals Claude, GPT, Gemini, GLM en meer.", + "provider.connect.opencodeZen.visit.prefix": "Ga naar ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " om je API-sleutel op te halen.", + "provider.connect.oauth.code.visit.prefix": "Ga naar ", + "provider.connect.oauth.code.visit.link": "deze link", + "provider.connect.oauth.code.visit.suffix": + " om je autorisatiecode op te halen, je account te koppelen en {{provider}}-modellen in OpenCode te gebruiken.", + "provider.connect.oauth.code.label": "{{method}}-autorisatiecode", + "provider.connect.oauth.code.placeholder": "Autorisatiecode", + "provider.connect.oauth.code.required": "Autorisatiecode is vereist", + "provider.connect.oauth.code.invalid": "Ongeldige autorisatiecode", + "provider.connect.oauth.auto.visit.prefix": "Ga naar ", + "provider.connect.oauth.auto.visit.link": "deze link", + "provider.connect.oauth.auto.visit.suffix": + " en voer de onderstaande code in om je account te koppelen en {{provider}}-modellen in OpenCode te gebruiken.", + "provider.connect.oauth.auto.confirmationCode": "Bevestigingscode", + "provider.connect.toast.connected.title": "Verbonden met {{provider}}", + "provider.connect.toast.connected.description": "{{provider}}-modellen zijn nu beschikbaar voor gebruik.", + "provider.custom.title": "Aangepaste aanbieder", + "provider.custom.unavailable": "Aangepaste aanbieders zijn niet beschikbaar op deze server", + "provider.custom.description.prefix": "Configureer een OpenAI-compatibele aanbieder. Bekijk de ", + "provider.custom.description.link": "documentatie over aanbiedersconfiguratie", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Aanbieder-ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Kleine letters, cijfers, koppeltekens of onderstrepingstekens", + "provider.custom.field.name.label": "Weergavenaam", + "provider.custom.field.name.placeholder": "Mijn AI-aanbieder", + "provider.custom.field.baseURL.label": "Basis-URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API-sleutel", + "provider.custom.field.apiKey.placeholder": "API-sleutel", + "provider.custom.field.apiKey.description": "Optioneel. Laat leeg als je de authenticatie via headers beheert.", + "provider.custom.models.label": "Modellen", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Naam", + "provider.custom.models.name.placeholder": "Weergavenaam", + "provider.custom.models.remove": "Model verwijderen", + "provider.custom.models.add": "Voeg model toe", + "provider.custom.headers.label": "Headers (optioneel)", + "provider.custom.headers.key.label": "Header", + "provider.custom.headers.key.placeholder": "Header-Naam", + "provider.custom.headers.value.label": "Waarde", + "provider.custom.headers.value.placeholder": "waarde", + "provider.custom.headers.remove": "Header verwijderen", + "provider.custom.headers.add": "Header toevoegen", + "provider.custom.error.providerID.required": "Aanbieder-ID is vereist", + "provider.custom.error.providerID.format": "Gebruik kleine letters, cijfers, koppeltekens of onderstrepingstekens", + "provider.custom.error.providerID.exists": "Die aanbieder-ID bestaat al", + "provider.custom.error.name.required": "Weergavenaam is vereist", + "provider.custom.error.baseURL.required": "Basis-URL is vereist", + "provider.custom.error.baseURL.format": "Moet beginnen met http:// of https://", + "provider.custom.error.required": "Vereist", + "provider.custom.error.duplicate": "Duplicaat", + "provider.disconnect.toast.disconnected.title": "Verbinding met {{provider}} verbroken", + "provider.disconnect.toast.disconnected.description": "{{provider}}-modellen zijn niet langer beschikbaar.", + "model.tag.free": "Gratis", + "model.tag.latest": "Nieuwste", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekst", + "model.input.image": "afbeelding", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Staat toe: {{inputs}}", + "model.tooltip.context.label": "Context", + "model.tooltip.inputs": "Invoer", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Aanbieder", + "model.tooltip.reasoning": "Redenering", + "model.tooltip.reasoning.allowed": "Maakt redeneren mogelijk", + "model.tooltip.reasoning.none": "Geen redenering", + "model.tooltip.context": "Contextlimiet {{limit}}", + "common.search.placeholder": "Zoeken", + "common.goBack": "Terug navigeren", + "common.goForward": "Vooruit navigeren", + "common.loading": "Laden", + "common.loading.ellipsis": "...", + "common.cancel": "Annuleer", + "common.clear": "Wissen", + "common.open": "Openen", + "common.connect": "Verbinden", + "common.disconnect": "Verbinding verbreken", + "common.continue": "Doorgaan", + "common.submit": "Verzenden", + "common.save": "Opslaan", + "common.saving": "Opslaan...", + "common.default": "Standaard", + "common.attachment": "bijlage", + "prompt.placeholder.shell": "Voer de shell-opdracht in... {{example}}", + "prompt.placeholder.normal": 'Vraag wat je wilt... "{{example}}"', + "prompt.placeholder.simple": "Vraag wat je wilt...", + "prompt.placeholder.summarizeComments": "Opmerkingen samenvatten…", + "prompt.placeholder.summarizeComment": "Opmerking samenvatten…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "esc om af te sluiten", + "session.child.promptDisabled": "Je kunt geen prompts naar subagentsessies sturen.", + "session.child.backToParent": "Terug naar de hoofdsessie.", + "prompt.example.1": "Repareer een TODO in de codebase", + "prompt.example.2": "Wat is de tech-stack van dit project?", + "prompt.example.3": "Herstel kapotte tests", + "prompt.example.4": "Leg uit hoe authenticatie werkt", + "prompt.example.5": "Vind en repareer beveiligingsproblemen", + "prompt.example.6": "Voeg eenheidstests toe voor de gebruikersservice", + "prompt.example.7": "Refactoreer deze functie om beter leesbaar te zijn", + "prompt.example.8": "Wat betekent deze fout?", + "prompt.example.9": "Help me dit probleem te debuggen", + "prompt.example.10": "Genereer API-documentatie", + "prompt.example.11": "Optimaliseer databasequery's", + "prompt.example.12": "Voeg invoervalidatie toe", + "prompt.example.13": "Maak een nieuwe component voor...", + "prompt.example.14": "Hoe deploy ik dit project?", + "prompt.example.15": "Controleer mijn code op best practices", + "prompt.example.16": "Voeg foutafhandeling toe aan deze functie", + "prompt.example.17": "Leg dit regex-patroon uit", + "prompt.example.18": "Converteer dit naar TypeScript", + "prompt.example.19": "Voeg logboekregistratie toe aan de hele codebase", + "prompt.example.20": "Welke afhankelijkheden zijn verouderd?", + "prompt.example.21": "Help me een migratiescript te schrijven", + "prompt.example.22": "Implementeer caching voor dit eindpunt", + "prompt.example.23": "Voeg paginering toe aan deze lijst", + "prompt.example.24": "Maak een CLI-opdracht voor...", + "prompt.example.25": "Hoe werken omgevingsvariabelen hier?", + "prompt.popover.emptyResults": "Geen overeenkomende resultaten", + "prompt.popover.emptyCommands": "Geen overeenkomende opdrachten", + "prompt.dropzone.label": "Zet hier afbeeldingen, pdf's of tekstbestanden neer", + "prompt.dropzone.file.label": "Zet neer om het bestand met @ te vermelden", + "prompt.slash.badge.custom": "aangepast", + "prompt.slash.badge.skill": "vaardigheid", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "actief", + "prompt.context.includeActiveFile": "Actief bestand opnemen", + "prompt.context.removeActiveFile": "Verwijder actief bestand uit context", + "prompt.context.removeFile": "Bestand uit context verwijderen", + "prompt.action.attachFile": "Bestanden toevoegen", + "prompt.menu.addImagesAndFiles": "Voeg bestanden toe en meer", + "prompt.menu.imagesAndFiles": "Afbeeldingen en bestanden", + "prompt.menu.commands": "Commando's", + "prompt.menu.context": "Context", + "prompt.menu.shellCommand": "Shell-commando", + "prompt.attachment.remove": "Bijlage verwijderen", + "prompt.action.send": "Verzenden", + "prompt.action.stop": "Stop", + "prompt.toast.pasteUnsupported.title": "Niet-ondersteunde bijlage", + "prompt.toast.pasteUnsupported.description": + "Hier kunnen alleen afbeeldingen, pdf's of tekstbestanden worden bijgevoegd.", + "prompt.toast.attachmentDuplicate.title": "Dit bestand is al geüpload", + "prompt.toast.modelAgentRequired.title": "Selecteer een agent en model", + "prompt.toast.modelAgentRequired.description": "Kies een agent en model voordat je een prompt verzendt.", + "prompt.toast.worktreeCreateFailed.title": "Kan worktree niet maken", + "prompt.toast.sessionCreateFailed.title": "Kan sessie niet maken", + "prompt.toast.shellSendFailed.title": "Kan shell-opdracht niet verzenden", + "prompt.toast.commandSendFailed.title": "Kan opdracht niet verzenden", + "prompt.toast.promptSendFailed.title": "Kan de prompt niet verzenden", + "prompt.toast.promptSendFailed.description": "Kan sessie niet ophalen", + "dialog.mcp.title": "MCP's", + "dialog.mcp.description": "{{enabled}} of {{total}} ingeschakeld", + "dialog.mcp.empty": "Geen MCP's geconfigureerd", + "dialog.lsp.empty": "LSP's worden automatisch gedetecteerd uit bestandstypen", + "dialog.plugins.empty": "Plug-ins geconfigureerd in opencode.json", + "mcp.status.connected": "verbonden", + "mcp.status.failed": "mislukt", + "mcp.status.needs_auth": "heeft autorisatie nodig", + "mcp.status.disabled": "uitgeschakeld", + "mcp.auth.clickToAuthenticate": "Klik om te authenticeren", + "dialog.fork.empty": "Geen berichten om van af te splitsen", + "dialog.directory.search.placeholder": "Zoek in mappen", + "dialog.directory.empty": "Geen mappen gevonden", + "dialog.directory.action.selectFile": "Selecteer bestand", + "dialog.directory.action.selectFolder": "Selecteer map", + "dialog.directory.root": "Hoofdmap", + "dialog.directory.parent": "Bovenliggende map", + "dialog.directory.readError": "Kan deze map niet lezen", + "app.server.unreachable": "Kan {{server}} niet bereiken", + "app.server.retrying": "Automatisch opnieuw proberen...", + "app.server.otherServers": "Andere servers", + "dialog.server.title": "Servers", + "dialog.server.description": "Wijzig met welke OpenCode-server deze app verbinding maakt.", + "dialog.server.search.placeholder": "Servers zoeken", + "dialog.server.empty": "Nog geen servers", + "dialog.server.add.title": "Server toevoegen", + "dialog.server.add.url": "Serveradres", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Kan geen verbinding maken met de server", + "dialog.server.add.checking": "Controleren...", + "dialog.server.add.button": "Server toevoegen", + "dialog.server.add.name": "Servernaam (optioneel)", + "dialog.server.add.namePlaceholder": "Lokale host", + "dialog.server.add.username": "Gebruikersnaam (optioneel)", + "dialog.server.add.usernamePlaceholder": "gebruikersnaam", + "dialog.server.add.password": "Wachtwoord (optioneel)", + "dialog.server.add.passwordPlaceholder": "wachtwoord", + "dialog.server.edit.title": "Server bewerken", + "dialog.server.default.title": "Standaardserver", + "dialog.server.default.description": + "Maak verbinding met deze server bij het starten van de app in plaats van een lokale server te starten. Vereist opnieuw opstarten.", + "dialog.server.default.none": "Geen server geselecteerd", + "dialog.server.default.set": "Stel de huidige server in als standaard", + "dialog.server.default.clear": "Wissen", + "dialog.server.action.remove": "Server verwijderen", + "dialog.server.menu.edit": "Bewerken", + "dialog.server.menu.default": "Als standaard instellen", + "dialog.server.menu.defaultRemove": "Standaard verwijderen", + "dialog.server.menu.delete": "Verwijderen", + "dialog.server.current": "Huidige server", + "dialog.server.status.default": "Standaard", + "wsl.server.add": "WSL-server toevoegen", + "wsl.server.addShort": "WSL toevoegen", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL-server", + "wsl.server.retryStart": "Opnieuw proberen te starten", + "wsl.server.updating": "Updaten...", + "wsl.onboarding.step.distro": "Kies distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL controleren...", + "wsl.onboarding.restartRequired": "Windows moet opnieuw worden opgestart om de installatie van WSL te voltooien.", + "wsl.onboarding.ready": "WSL is klaar.", + "wsl.onboarding.required": "WSL is vereist om door te gaan.", + "wsl.onboarding.checkingDistros": "Distributies controleren...", + "wsl.onboarding.installingDistro": "{{distro}} installeren...", + "wsl.onboarding.checkingDistro": "{{distro}} controleren...", + "wsl.onboarding.listingDistros": "Lijst met distributies ophalen...", + "wsl.onboarding.distroReady": "{{distro}} is klaar.", + "wsl.onboarding.distroNotInstalled": "{{distro}} is nog niet geïnstalleerd.", + "wsl.onboarding.openDistroOnce": "Open {{distro}} één keer om de installatie te voltooien.", + "wsl.onboarding.finishingDistro": "Installatie van {{distro}} voltooien.", + "wsl.onboarding.pickDistro": "Kies een distributie of installeer er hieronder een.", + "wsl.onboarding.checkingOpencode": "OpenCode controleren...", + "wsl.onboarding.checkingOpencodeIn": "OpenCode controleren in {{distro}}...", + "wsl.onboarding.updatingOpencode": "OpenCode bijwerken...", + "wsl.onboarding.updatingOpencodeIn": "OpenCode bijwerken in {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Werk OpenCode bij in {{distro}}.", + "wsl.onboarding.updateOpencode": "OpenCode bijwerken", + "wsl.onboarding.opencodeReadyIn": "OpenCode is gereed in {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode is klaar.", + "wsl.onboarding.installOpencodeIn": "Installeer OpenCode in {{distro}}.", + "wsl.onboarding.installOpencode": "Installeer OpenCode", + "wsl.onboarding.chooseDistroFirst": "Kies eerst een distributie.", + "wsl.onboarding.loadFailed": "Kan de WSL-status niet laden.", + "wsl.onboarding.loading": "Laden...", + "wsl.onboarding.installedDistros": "Geïnstalleerde distributies", + "wsl.onboarding.checkAgain": "Controleer opnieuw", + "wsl.onboarding.distroStatus.ready": "Klaar", + "wsl.onboarding.distroStatus.checking": "Controleren...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode niet geïnstalleerd", + "wsl.onboarding.distroStatus.missingTools": "bash en curl ontbreken", + "wsl.onboarding.distroStatus.unsupported": "Niet ondersteund · Gebruik WSL 2", + "wsl.onboarding.needAnotherDistro": "Nog een distro nodig?", + "wsl.onboarding.needAnotherDistroHint": "Installeer een Linux-distributie uit de WSL-catalogus", + "wsl.onboarding.wslNotInstalled.title": "WSL niet geïnstalleerd", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows-subsysteem voor Linux) is vereist voordat OpenCode een WSL-server kan toevoegen", + "wsl.onboarding.wslUnavailable.title": "WSL niet beschikbaar", + "wsl.onboarding.wslUnavailable.description": "OpenCode kan WSL niet verifiëren op deze machine.", + "wsl.onboarding.installWsl": "Installeer WSL", + "wsl.onboarding.windowsRestartRequired": + "Start Windows opnieuw om de installatie van WSL te voltooien en open vervolgens OpenCode opnieuw.", + "wsl.onboarding.next": "Volgende", + "wsl.onboarding.refresh": "Vernieuwen", + "wsl.onboarding.allDistrosAdded": "Alle geïnstalleerde distributies zijn al toegevoegd.", + "wsl.onboarding.noDistros": "Nog geen distributies gedetecteerd.", + "wsl.onboarding.install": "Installeren", + "wsl.onboarding.installing": "Installeren...", + "wsl.onboarding.installDistro": "Distro installeren", + "wsl.onboarding.searchDistros": "Zoek distributies", + "wsl.onboarding.wsl2Required": "WSL 2 is vereist.", + "wsl.onboarding.toolsRequired": "Deze distro heeft bash en curl nodig.", + "wsl.onboarding.openTerminal": "Terminal openen", + "wsl.onboarding.path": "Pad: {{path}}", + "wsl.onboarding.notFound": "niet gevonden", + "wsl.onboarding.version": "Versie: {{version}}", + "wsl.onboarding.unknown": "onbekend", + "wsl.onboarding.desktopVersion": "desktop {{version}}", + "wsl.onboarding.versionMismatch": "De geïnstalleerde versie komt niet overeen met de versie van de desktop-app.", + "wsl.onboarding.adding": "Toevoegen...", + "help.tabs.toast.ariaLabel": "Introductie van tabbladen. Organiseer je werk en actieve sessies met tabbladen", + "help.tabs.toast.dismiss": "Informatie over tabbladen sluiten", + "help.tabs.title": "Introductie van tabbladen", + "help.tabs.description": "Organiseer je werk en actieve sessies met tabbladen", + "help.tabs.date": "14 juli", + "help.tabs.introduction": "OpenCode Desktop is nu opgebouwd rond tabbladen.", + "help.tabs.sessions": + "Start een nieuwe sessie in een tabblad of open een bestaande sessie vanuit een van je projecten. Open een nieuw tabblad wanneer je aan iets nieuws begint en sluit het wanneer je klaar bent.", + "help.tabs.organize": + "Met enkele geopende tabbladen kun je je actieve sessies gemakkelijker organiseren. Geef tabbladen een herkenbare naam als je ze geopend wilt houden.", + "help.tabs.home": + "Je vindt al je sessies en projecten op het nieuwe startscherm. Wanneer je een sessie selecteert, wordt deze in een tabblad geopend.", + "help.tabs.persistence": "Wanneer je de app opnieuw opent, zijn je tabbladen nog steeds geopend.", + "help.tabs.worktrees": + "Het nieuwe ontwerp ondersteunt Git-worktrees nog niet, maar die ondersteuning komt binnenkort. Als je liever de vorige lay-out blijft gebruiken, kun je in Instellingen tussen de lay-outs schakelen. Houd er rekening mee dat de nieuwe lay-out over enkele weken permanent wordt.", + "server.row.noUsername": "geen gebruikersnaam", + "dialog.project.edit.title": "Project bewerken", + "dialog.project.edit.name": "Naam", + "dialog.project.edit.icon": "Pictogram", + "dialog.project.edit.icon.alt": "Projectpictogram", + "dialog.project.edit.icon.hint": "Klik of sleep een afbeelding", + "dialog.project.edit.icon.recommended": "Aanbevolen: 128x128px", + "dialog.project.edit.color": "Kleur", + "dialog.project.edit.color.select": "Kleur {{color}} selecteren", + "dialog.project.edit.worktree.startup": "Opstartscript voor werkruimte", + "dialog.project.edit.worktree.startup.description": + "Wordt uitgevoerd nadat een nieuwe werkruimte (worktree) is aangemaakt.", + "dialog.project.edit.worktree.startup.placeholder": "bijv. bun install", + "dialog.releaseNotes.action.getStarted": "Ga aan de slag", + "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.', + "context.breakdown.system": "Systeem", + "context.breakdown.user": "Gebruiker", + "context.breakdown.assistant": "Assistent", + "context.breakdown.tool": "Toolaanroepen", + "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", + "context.stats.model": "Model", + "context.stats.limit": "Contextlimiet", + "context.stats.totalTokens": "Totaal tokens", + "context.stats.usage": "Gebruik", + "context.stats.inputTokens": "Invoertokens", + "context.stats.outputTokens": "Uitvoertokens", + "context.stats.reasoningTokens": "Redeneringstokens", + "context.stats.cacheTokens": "Cachetokens (lezen/schrijven)", + "context.stats.userMessages": "Gebruikersberichten", + "context.stats.assistantMessages": "Assistentberichten", + "context.stats.totalCost": "Totale kosten", + "context.stats.sessionCreated": "Sessie gemaakt", + "context.stats.lastActivity": "Laatste activiteit", + "context.usage.tokens": "Tokens", + "context.usage.usage": "Gebruik", + "context.usage.cost": "Kosten", + "context.usage.clickToView": "Klik om de context te bekijken", + "context.usage.view": "Bekijk contextgebruik", + "language.en": "Engels", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Duits", + "language.es": "Spaans", + "language.fr": "Frans", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Portugees (Brazilië)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Taal", + "toast.language.description": "Overgeschakeld naar {{language}}", + "toast.theme.title": "Thema gewijzigd", + "toast.scheme.title": "Kleurenschema", + "toast.workspace.enabled.title": "Werkruimten ingeschakeld", + "toast.workspace.enabled.description": "Er worden nu meerdere worktrees weergegeven in de zijbalk", + "toast.workspace.disabled.title": "Werkruimten uitgeschakeld", + "toast.workspace.disabled.description": "Alleen de hoofdworktree wordt weergegeven in de zijbalk", + "toast.permissions.autoaccept.on.title": "Machtigingen automatisch accepteren", + "toast.permissions.autoaccept.on.description": "Toestemmingsverzoeken worden automatisch goedgekeurd", + "toast.permissions.autoaccept.off.title": "Machtigingen niet meer automatisch accepteren", + "toast.permissions.autoaccept.off.description": "Machtigingsverzoeken moeten worden goedgekeurd", + "toast.model.none.title": "Geen model geselecteerd", + "toast.model.none.description": "Maak verbinding met een aanbieder om deze sessie samen te vatten", + "toast.file.loadFailed.title": "Kan bestand niet laden", + "toast.file.listFailed.title": "Kan bestanden niet weergeven", + "toast.context.noLineSelection.title": "Geen regels geselecteerd", + "toast.context.noLineSelection.description": "Selecteer eerst een reeks regels in een bestandstabblad.", + "toast.session.share.copyFailed.title": "Kan de URL niet naar het klembord kopiëren", + "toast.session.share.success.title": "Sessie gedeeld", + "toast.session.share.success.description": "Deel-URL gekopieerd naar klembord!", + "toast.session.share.failed.title": "Kan sessie niet delen", + "toast.session.share.failed.description": "Er is een fout opgetreden tijdens het delen van de sessie", + "toast.session.unshare.success.title": "Sessie niet gedeeld", + "toast.session.unshare.success.description": "Sessie is niet meer gedeeld!", + "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", + "toast.update.description": "Er is nu een nieuwe versie van OpenCode ({{version}}) beschikbaar om te installeren.", + "toast.update.action.installRestart": "Installeren en opnieuw opstarten", + "toast.update.action.notYet": "Nog niet", + "error.page.title": "Er is iets misgegaan", + "error.page.description": "Er is een fout opgetreden tijdens het laden van de applicatie.", + "error.page.description.localServerStartup": "Er is een fout opgetreden bij het starten van de lokale server.", + "error.page.details.label": "Foutdetails", + "error.page.action.restart": "Opnieuw opstarten", + "error.page.action.report": "Rapporteer fout", + "error.page.action.reported": "Fout gerapporteerd", + "error.page.action.exportLogs": "Logboeken exporteren", + "error.page.action.checking": "Controleren...", + "error.page.action.checkUpdates": "Controleer op updates", + "error.page.action.updateTo": "Update naar {{version}}", + "error.page.circular": "[Circulair]", + "error.page.report.prefix": "Rapporteer deze fout aan het OpenCode-team", + "error.page.report.discord": "op Discord", + "error.page.version": "Versie: {{version}}", + "error.dev.rootNotFound": + "Root-element niet gevonden. Ben je vergeten het toe te voegen aan je index.html? Of is het id-attribuut misschien verkeerd gespeld?", + "error.serverSync.connectFailed": "Kan geen verbinding maken met de server. Draait er een server op `{{url}}`?", + "error.serverSDK.noServerAvailable": "Geen server beschikbaar", + "error.serverSDK.serverNotAvailable": "Server niet beschikbaar", + "error.childStore.persistedCacheCreateFailed": "Kan persistente cache niet maken", + "error.childStore.persistedProjectMetadataCreateFailed": "Kan de persistente projectmetagegevens niet maken", + "error.childStore.persistedProjectIconCreateFailed": "Kan het opgeslagen projectpictogram niet maken", + "error.childStore.storeCreateFailed": "Kan opslag niet maken", + "directory.error.invalidUrl": "Ongeldige map in URL.", + "error.chain.unknown": "Onbekende fout", + "error.server.invalidConfiguration": "Ongeldige configuratie", + "error.chain.causedBy": "Veroorzaakt door:", + "error.chain.apiError": "API-fout", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Opnieuw proberen mogelijk: {{retryable}}", + "error.chain.responseBody": "Antwoordtekst:\n{{body}}", + "error.chain.didYouMean": "Bedoelde je: {{suggestions}}", + "error.chain.modelNotFound": "Model niet gevonden: {{provider}}/{{model}}", + "error.chain.checkConfig": "Controleer de provider- en modelnamen in je configuratie (opencode.json)", + "error.chain.mcpFailed": 'MCP-server "{{name}}" is mislukt. Let op: OpenCode ondersteunt nog geen MCP-authenticatie.', + "error.chain.providerAuthFailed": "Authenticatie bij aanbieder mislukt ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Kan provider "{{provider}}" niet initialiseren. Controleer de inloggegevens en configuratie.', + "error.chain.configJsonInvalid": "Configuratiebestand op {{path}} is niet geldig JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Configuratiebestand op {{path}} is niet geldig JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'De map "{{dir}}" in {{path}} is ongeldig. Hernoem de map naar "{{suggestion}}" of verwijder deze. Dit is een veelvoorkomende typefout.', + "error.chain.configFrontmatterError": "Kan frontmatter in {{path}} niet parseren:\n{{message}}", + "error.chain.configInvalid": "Configuratiebestand op {{path}} is ongeldig", + "error.chain.configInvalidWithMessage": "Configuratiebestand op {{path}} is ongeldig: {{message}}", + "notification.permission.title": "Toestemming vereist", + "notification.permission.description": "{{sessionTitle}} in {{projectName}} heeft toestemming nodig", + "notification.question.title": "Vraag", + "notification.question.description": "{{sessionTitle}} in {{projectName}} heeft een vraag", + "notification.action.goToSession": "Ga naar sessie", + "notification.session.responseReady.title": "Reactie klaar", + "notification.session.error.title": "Sessiefout", + "notification.session.error.fallbackDescription": "Er is een fout opgetreden", + "home.recentProjects": "Recente projecten", + "home.empty.title": "Geen recente projecten", + "home.empty.description": "Ga aan de slag door een lokaal project te openen", + "home.title": "Start", + "home.projects": "Projecten", + "home.project.add": "Project toevoegen", + "home.recentlyClosed": "Onlangs gesloten", + "home.server.collapse": "Serverprojecten samenvouwen", + "home.server.expand": "Serverprojecten uitvouwen", + "home.sessions.search.placeholder": "Zoek sessies", + "home.sessions.search.placeholder.scoped": "Sessies zoeken in {{scope}}", + "home.sessions.search.sessions": "Sessies", + "home.sessions.search.noResults": "Geen sessies gevonden voor {{query}}", + "home.sessions.empty": "Hier nog niets", + "home.sessions.empty.description": "Maak een sessie aan om aan de slag te gaan", + "home.sessions.group.today": "Vandaag", + "home.sessions.group.yesterday": "Gisteren", + "home.sessions.group.older": "Ouder", + "home.providerTip": + "Maak verbinding met meer dan 75 providers om andere modellen te gebruiken, waaronder Claude, GPT, Gemini, enz.", + "session.tab.session": "Sessie", + "session.tab.review": "Beoordeling", + "session.tab.context": "Context", + "session.tab.unknown": "Onbekende sessie", + "session.panel.reviewAndFiles": "Beoordeling en bestanden", + "session.error.notFound": "Deze sessie kan niet worden gevonden", + "session.error.notFound.description": "Dit tabblad verwijst naar een sessie die niet meer bestaat op deze server.", + "session.error.notFound.closeTab": "Tabblad sluiten", + "session.error.serverConnection": "Kan geen verbinding maken met deze server", + "session.review.filesChanged": "Gewijzigde bestanden: {{count}}", + "session.review.change.one": "Wijziging", + "session.review.change.other": "Wijzigingen", + "session.review.loadingChanges": "Wijzigingen laden...", + "session.review.empty": "Er zijn nog geen wijzigingen in deze sessie", + "session.review.noVcs": "Geen Git-versiecontrolesysteem gedetecteerd, wijzigingen niet weergegeven", + "session.review.noVcs.createGit.title": "Maak een Git-repository", + "session.review.noVcs.createGit.description": "Wijzigingen in dit project bijhouden, beoordelen en ongedaan maken", + "session.review.noVcs.createGit.actionLoading": "Git-repository maken...", + "session.review.noVcs.createGit.action": "Maak een Git-repository", + "session.review.noSnapshot": + "Het bijhouden van momentopnamen is uitgeschakeld in de configuratie, dus sessiewijzigingen zijn niet beschikbaar", + "session.review.noChanges": "Geen wijzigingen", + "session.review.noUncommittedChanges": "Er zijn nog geen niet-gecommitteerde wijzigingen", + "session.review.noBranchChanges": "Er zijn nog geen branchwijzigingen", + "session.files.selectToOpen": "Selecteer een bestand om te openen", + "session.files.all": "Alle bestanden", + "session.files.empty": "Geen bestanden", + "session.files.binaryContent": "Binair bestand (inhoud kan niet worden weergegeven)", + "session.messages.renderEarlier": "Geef eerdere berichten weer", + "session.messages.loadingEarlier": "Eerdere berichten laden...", + "session.messages.loadEarlier": "Laad eerdere berichten", + "session.messages.loading": "Berichten laden...", + "session.messages.jumpToLatest": "Ga naar de laatste", + "session.context.addToContext": "Voeg {{selection}} toe aan de context", + "session.todo.title": "Taken", + "session.todo.collapse": "Samenvouwen", + "session.todo.expand": "Uitvouwen", + "session.todo.progress": "{{done}} van {{total}} taken voltooid", + "session.question.progress": "{{current}} van {{total}} vragen", + "session.question.minimize": "Minimaliseer de vraag", + "session.question.restore": "Vraag herstellen", + "session.question.pending.one": "{{count}} openstaande vraag", + "session.question.pending.other": "{{count}} openstaande vragen", + "session.followupDock.summary.one": "{{count}} bericht in de wachtrij", + "session.followupDock.summary.other": "{{count}} berichten in de wachtrij", + "session.followupDock.sendNow": "Verstuur nu", + "session.followupDock.edit": "Bewerken", + "session.followupDock.collapse": "In de wachtrij geplaatste berichten samenvouwen", + "session.followupDock.expand": "Berichten in de wachtrij uitvouwen", + "session.revertDock.summary.one": "{{count}} teruggedraaid bericht", + "session.revertDock.summary.other": "{{count}} teruggedraaide berichten", + "session.revertDock.collapse": "Teruggedraaide berichten samenvouwen", + "session.revertDock.expand": "Teruggedraaide berichten uitvouwen", + "session.revertDock.restore": "Bericht herstellen", + "session.new.title": "Bouw wat je wilt", + "session.new.project.new": "Nieuw project", + "session.new.project.search": "Zoek projecten", + "session.new.project.add": "Project toevoegen", + "session.new.worktree.main": "Hoofdbranch", + "session.new.worktree.mainWithBranch": "Hoofdbranch ({{branch}})", + "session.new.worktree.create": "Nieuwe worktree maken", + "session.new.workspace.runIn": "Sessie uitvoeren in", + "session.new.workspace.triggerLocal": "Lokaal", + "session.new.workspace.local": "Lokale repository", + "session.new.workspace.existing": "Werkruimte…", + "session.new.git.none": "Geen Git", + "session.new.lastModified": "Laatst gewijzigd", + "session.header.search.placeholder": "Zoek {{project}}", + "session.header.searchFiles": "Zoek bestanden", + "session.header.openIn": "Openen in", + "session.header.open.action": "Open {{app}}", + "session.header.open.ariaLabel": "Openen in {{app}}", + "session.header.open.menu": "Opties openen", + "session.header.open.copyPath": "Kopieer pad", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Bestandsverkenner", + "session.header.open.fileManager": "Bestandsbeheer", + "session.header.reveal.finder": "Tonen in Finder", + "session.header.reveal.fileExplorer": "Weergeven in Verkenner", + "session.header.reveal.containingFolder": "Bijbehorende map openen", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Serverconfiguraties", + "status.popover.tab.servers": "Servers", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plug-ins", + "status.popover.action.manageServers": "Beheer servers", + "session.share.popover.title": "Publiceer op internet", + "session.share.popover.description.shared": + "Deze sessie is openbaar op internet. Iedereen met de link heeft toegang.", + "session.share.popover.description.unshared": + "Deel de sessie openbaar op internet. Iedereen met de link krijgt toegang.", + "session.share.action.share": "Deel", + "session.share.action.publish": "Publiceren", + "session.share.action.publishing": "Publiceren...", + "session.share.action.unpublish": "Publicatie ongedaan maken", + "session.share.action.unpublishing": "Publicatie ongedaan maken...", + "session.share.action.view": "Bekijk", + "session.share.copy.copied": "Gekopieerd", + "session.share.copy.copyLink": "Kopieer link", + "lsp.tooltip.none": "Geen LSP-servers", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Prompt laden...", + "terminal.loading": "Terminal laden...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Terminal sluiten", + "terminal.connectionLost.title": "Verbinding verloren", + "terminal.connectionLost.abnormalClose": "WebSocket abnormaal gesloten: {{code}}", + "terminal.connectionLost.description": + "De terminalverbinding is onderbroken. Dit kan gebeuren wanneer de server opnieuw wordt opgestart.", + "terminal.connectTicket.csrfError": + "PTY-verbindingsticket geweigerd door oorsprongs- of CSRF-controles. Controleer de CORS-configuratie van de server.", + "terminal.connectTicket.statusError": "PTY-verbindingsticket mislukt met status {{status}}", + "titlebar.update": "Bijwerken", + "titlebar.updateVersion": "Bijwerken {{version}}", + "common.closeTab": "Tabblad sluiten", + "common.dismiss": "Negeren", + "common.moreCountSuffix": " (+{{count}} meer)", + "common.requestFailed": "Verzoek mislukt", + "common.moreOptions": "Meer opties", + "common.learnMore": "Meer informatie", + "common.rename": "Hernoemen", + "common.export": "Exporteren", + "common.reset": "Opnieuw instellen", + "common.archive": "Archiveren", + "common.delete": "Verwijderen", + "common.close": "Sluiten", + "common.edit": "Bewerken", + "common.loadMore": "Meer laden", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Spatie", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Verwijderen", + "common.key.home": "Home", + "common.key.end": "Einde", + "common.key.pageUp": "Pagina omhoog", + "common.key.pageDown": "Pagina omlaag", + "common.key.insert": "Invoegen", + "common.unknown": "onbekend", + "common.time.justNow": "Zojuist", + "common.time.minutesAgo.short": "{{count}}m geleden", + "common.time.hoursAgo.short": "{{count}}h geleden", + "common.time.daysAgo.short": "{{count}}d geleden", + "sidebar.menu.toggle": "Menu tonen of verbergen", + "sidebar.nav.projectsAndSessions": "Projecten en sessies", + "sidebar.settings": "Instellingen", + "sidebar.help": "Hulp", + "sidebar.workspaces.enable": "Schakel werkruimten in", + "sidebar.workspaces.disable": "Schakel werkruimten uit", + "sidebar.gettingStarted.title": "Aan de slag", + "sidebar.gettingStarted.line1": "OpenCode bevat gratis modellen, zodat je direct aan de slag kunt.", + "sidebar.gettingStarted.line2": + "Maak verbinding met een aanbieder om modellen te gebruiken, waaronder Claude, GPT en Gemini.", + "sidebar.project.recentSessions": "Recente sessies", + "sidebar.project.viewAllSessions": "Bekijk alle sessies", + "sidebar.project.clearNotifications": "Meldingen wissen", + "sidebar.empty.title": "Geen projecten geopend", + "sidebar.empty.description": "Open een project om aan de slag te gaan", + "debugBar.ariaLabel": "Diagnostiek voor ontwikkelprestaties", + "debugBar.na": "n.v.t.", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Laatst voltooide routenavigatie naar of binnen een sessiepagina, gemeten vanaf het starten van de router tot de eerste render nadat de navigatie is voltooid.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Voortschrijdend aantal frames per seconde over de afgelopen 5 seconden.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Slechtste frametijd van de afgelopen 5 seconden.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Frames van meer dan 32 ms in de afgelopen 5 seconden.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Geblokkeerde tijd en aantal lange taken in de afgelopen 5 seconden. Maximale taak: {{max}}.", + "debugBar.delay.label": "VERTRAGING", + "debugBar.delay.tip": "Slechtst waargenomen invoervertraging in de afgelopen 5 seconden.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Geschatte interactieduur gedurende de afgelopen 5 seconden. Dit is INP-achtig, niet de officiële Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Cumulatieve lay-outverschuiving sinds de app is gestart.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Gebruikte JS-heap versus heap-limiet. Alleen Chromium.", + "debugBar.mem.tip": "Gebruikte JS-heap versus heaplimiet. {{used}} van {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Dwing focusstijlen af voor alle interactieve elementen", + "debugBar.focus.on": "AAN", + "debugBar.focus.off": "UIT", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Dwing de lay-outrichting voor de hele app af zonder de geselecteerde taal te wijzigen", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Desktop", + "settings.section.server": "Server", + "settings.tab.general": "Algemeen", + "settings.tab.shortcuts": "Snelkoppelingen", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL-integratie", + "settings.desktop.wsl.description": "Voer de OpenCode-server uit in WSL op Windows.", + "settings.general.section.appearance": "Uiterlijk", + "settings.general.section.advanced": "Geavanceerd", + "settings.general.section.notifications": "Systeemmeldingen", + "settings.general.section.updates": "Updates", + "settings.general.section.sounds": "Geluidseffecten", + "settings.general.section.feed": "Feed", + "settings.general.section.display": "Weergave", + "settings.general.row.language.title": "Taal", + "settings.general.row.language.description": "Wijzig de weergavetaal voor OpenCode", + "settings.general.row.shell.title": "Terminalshell", + "settings.general.row.shell.description": "Shell gebruikt door de terminal- en agenttools", + "settings.general.row.shell.autoDefault": "Automatisch (standaard)", + "settings.general.row.shell.terminalOnly": "alleen terminal", + "settings.general.row.appearance.title": "Uiterlijk", + "settings.general.row.appearance.description": "Pas aan hoe OpenCode eruitziet op je apparaat", + "settings.general.row.colorScheme.title": "Kleurenschema", + "settings.general.row.colorScheme.description": + "Kies of OpenCode het systeemthema, het lichte thema of het donkere thema gebruikt", + "settings.general.row.theme.title": "Thema", + "settings.general.row.theme.description": "Pas het thema van OpenCode aan.", + "settings.general.row.font.title": "Codelettertype", + "settings.general.row.font.description": "Pas het lettertype aan dat in codeblokken wordt gebruikt", + "settings.general.row.terminalFont.title": "Terminal-lettertype", + "settings.general.row.terminalFont.description": "Pas het lettertype aan dat in de terminal wordt gebruikt", + "settings.general.row.uiFont.title": "UI-lettertype", + "settings.general.row.uiFont.description": "Pas het lettertype aan dat in de hele interface wordt gebruikt", + "settings.general.row.followup.title": "Vervolggedrag", + "settings.general.row.followup.description": + "Kies of vervolgprompts de sessie direct bijsturen of in een wachtrij wachten", + "settings.general.row.followup.option.queue": "Wachtrij", + "settings.general.row.followup.option.steer": "Sturen", + "settings.general.row.showFileTree.title": "Bestandsstructuur", + "settings.general.row.showFileTree.description": "Toon het paneel met de bestandsstructuur in sessies", + "settings.general.row.showNavigation.title": "Navigatiebediening", + "settings.general.row.showNavigation.description": + "Toon de knoppen Vorige en Volgende in de titelbalk van de desktop-app", + "settings.general.row.showSearch.title": "Commandopalet", + "settings.general.row.showSearch.description": "Toon de knop voor zoeken en het commandopalet in de titelbalk", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Toon de terminalknop in de titelbalk van de desktop-app", + "settings.general.row.showStatus.title": "Serverstatus", + "settings.general.row.showStatus.description": "Toon de serverstatusknop in de titelbalk", + "settings.general.row.mobileTitlebarBottom.title": "Navigatie onderaan", + "settings.general.row.mobileTitlebarBottom.description": + "Plaats de titelbalk en sessietabbladen onderaan het scherm op mobiel", + "settings.general.row.showCustomAgents.title": "Toon agent", + "settings.general.row.showCustomAgents.description": + "Schakel tussen agenten in het invoerveld. Indien verborgen wordt standaard de Build-agent gebruikt.", + "settings.general.row.reasoningSummaries.title": "Toon redeneeroverzichten", + "settings.general.row.reasoningSummaries.description": + "Geef samenvattingen van modelredeneringen weer in de tijdlijn", + "settings.general.row.shellToolPartsExpanded.title": "Shelltoolonderdelen uitvouwen", + "settings.general.row.shellToolPartsExpanded.description": + "Toon shelltoolonderdelen standaard uitgevouwen in de tijdlijn", + "settings.general.row.editToolPartsExpanded.title": "Bewerkingstoolonderdelen uitvouwen", + "settings.general.row.editToolPartsExpanded.description": + "Toon bewerkings-, schrijf- en patchtoolonderdelen standaard uitgevouwen in de tijdlijn", + "settings.general.row.newInterface.title": "Nieuwe lay-out", + "settings.general.row.newInterface.badge": "Nieuw", + "settings.general.row.newInterface.description": + "Gebruik de nieuwe tabbladen en home-indeling. Wissel gedurende een beperkte tijd tussen lay-outs.", + "settings.general.row.newInterfaceNotice.title": "Je gebruikt nu de nieuwe lay-out", + "settings.general.row.newInterfaceNotice.description": "De vorige lay-out is niet meer beschikbaar", + "settings.general.row.newInterfaceNotice.dismiss": "Negeren", + "settings.general.row.pinchZoom.title": "Knijp om te zoomen", + "settings.general.row.pinchZoom.description": + "Sta knijp- en Ctrl-scroll-bewegingen op het trackpad toe om in te zoomen", + "settings.general.row.wayland.title": "Native Wayland gebruiken", + "settings.general.row.wayland.description": "Schakel X11-fallback uit op Wayland. Vereist opnieuw opstarten.", + "settings.general.row.wayland.tooltip": + "Op Linux met monitoren met verschillende vernieuwingsfrequenties kan native Wayland stabieler zijn.", + "settings.general.row.releaseNotes.title": "Versienotities", + "settings.general.row.releaseNotes.description": "Pop-ups met nieuwe functies na updates tonen", + "settings.updates.row.startup.title": "Controleer op updates bij het opstarten", + "settings.updates.row.startup.description": "Controleer automatisch op updates wanneer OpenCode wordt gestart", + "settings.updates.row.check.title": "Controleer op updates", + "settings.updates.row.check.description": "Controleer handmatig op updates en installeer deze indien beschikbaar", + "settings.updates.action.checkNow": "Controleer nu", + "settings.updates.action.checking": "Controleren...", + "settings.updates.action.downloading": "Downloaden...", + "settings.updates.action.installing": "Installeren...", + "settings.updates.toast.latest.title": "OpenCode is bijgewerkt", + "settings.updates.toast.latest.description": "Je gebruikt de nieuwste versie van OpenCode.", + "sound.option.none": "Geen", + "sound.option.alert01": "Waarschuwing 01", + "sound.option.alert02": "Waarschuwing 02", + "sound.option.alert03": "Waarschuwing 03", + "sound.option.alert04": "Waarschuwing 04", + "sound.option.alert05": "Waarschuwing 05", + "sound.option.alert06": "Waarschuwing 06", + "sound.option.alert07": "Waarschuwing 07", + "sound.option.alert08": "Waarschuwing 08", + "sound.option.alert09": "Waarschuwing 09", + "sound.option.alert10": "Waarschuwing 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nee 01", + "sound.option.nope02": "Nee 02", + "sound.option.nope03": "Nee 03", + "sound.option.nope04": "Nee 04", + "sound.option.nope05": "Nee 05", + "sound.option.nope06": "Nee 06", + "sound.option.nope07": "Nee 07", + "sound.option.nope08": "Nee 08", + "sound.option.nope09": "Nee 09", + "sound.option.nope10": "Nee 10", + "sound.option.nope11": "Nee 11", + "sound.option.nope12": "Nee 12", + "sound.option.yup01": "Ja 01", + "sound.option.yup02": "Ja 02", + "sound.option.yup03": "Ja 03", + "sound.option.yup04": "Ja 04", + "sound.option.yup05": "Ja 05", + "sound.option.yup06": "Ja 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Toon systeemmelding wanneer de agent klaar is of aandacht nodig heeft", + "settings.general.notifications.permissions.title": "Machtigingen", + "settings.general.notifications.permissions.description": "Systeemmelding weergeven wanneer toestemming vereist is", + "settings.general.notifications.errors.title": "Fouten", + "settings.general.notifications.errors.description": "Systeemmelding weergeven wanneer er een fout optreedt", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Speel geluid af wanneer de agent klaar is of aandacht nodig heeft", + "settings.general.sounds.permissions.title": "Machtigingen", + "settings.general.sounds.permissions.description": "Speel geluid af wanneer toestemming vereist is", + "settings.general.sounds.errors.title": "Fouten", + "settings.general.sounds.errors.description": "Speel geluid af wanneer er een fout optreedt", + "settings.shortcuts.title": "Sneltoetsen", + "settings.shortcuts.reset.button": "Resetten naar standaardwaarden", + "settings.shortcuts.reset.toast.title": "Snelkoppelingen opnieuw ingesteld", + "settings.shortcuts.reset.toast.description": "Sneltoetsen zijn teruggezet naar de standaardwaarden.", + "settings.shortcuts.conflict.title": "Snelkoppeling al in gebruik", + "settings.shortcuts.conflict.description": "{{keybind}} is al toegewezen aan {{titles}}.", + "settings.shortcuts.unassigned": "Niet toegewezen", + "settings.shortcuts.pressKeys": "Druk op toetsen", + "settings.shortcuts.search.placeholder": "Zoek snelkoppelingen", + "settings.shortcuts.search.empty": "Geen snelkoppelingen gevonden", + "settings.shortcuts.group.general": "Algemeen", + "settings.shortcuts.group.session": "Sessie", + "settings.shortcuts.group.navigation": "Navigatie", + "settings.shortcuts.group.modelAndAgent": "Model en agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + "settings.providers.title": "Aanbieders", + "settings.providers.description": "Aanbiederinstellingen kunnen hier worden geconfigureerd.", + "settings.providers.section.connected": "Verbonden aanbieders", + "settings.providers.connected.empty": "Geen verbonden aanbieders", + "settings.providers.connected.environmentDescription": "Verbonden via je omgevingsvariabelen", + "settings.providers.section.popular": "Populaire aanbieders", + "settings.providers.custom.description": "Voeg een OpenAI-compatibele aanbieder toe via de basis-URL.", + "settings.providers.tag.environment": "Omgeving", + "settings.providers.tag.config": "Configuratie", + "settings.providers.tag.custom": "Aangepast", + "settings.providers.tag.other": "Anders", + "settings.models.title": "Modellen", + "settings.models.description": "Modelinstellingen kunnen hier worden geconfigureerd.", + "settings.agents.title": "Agenten", + "settings.agents.description": "Agentinstellingen kunnen hier worden geconfigureerd.", + "settings.commands.title": "Commando's", + "settings.commands.description": "Commando-instellingen kunnen hier worden geconfigureerd.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP-instellingen kunnen hier worden geconfigureerd.", + "settings.permissions.title": "Machtigingen", + "settings.permissions.description": "Bepaal welke tools de server standaard kan gebruiken.", + "settings.permissions.section.tools": "Tools", + "settings.permissions.toast.updateFailed.title": "Kan machtigingen niet bijwerken", + "settings.permissions.action.allow": "Toestaan", + "settings.permissions.action.ask": "Vragen", + "settings.permissions.action.deny": "Weigeren", + "settings.permissions.tool.read.title": "Lees", + "settings.permissions.tool.read.description": "Een bestand lezen (komt overeen met het bestandspad)", + "settings.permissions.tool.edit.title": "Bewerken", + "settings.permissions.tool.edit.description": + "Wijzig bestanden, inclusief bewerkingen, schrijfbewerkingen en patches", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Bestanden zoeken met globpatronen", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Zoek bestandsinhoud met behulp van reguliere expressies", + "settings.permissions.tool.list.title": "Lijst", + "settings.permissions.tool.list.description": "Geef bestanden in een map weer", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Voer shell-opdrachten uit", + "settings.permissions.tool.task.title": "Taak", + "settings.permissions.tool.task.description": "Start subagenten", + "settings.permissions.tool.skill.title": "Vaardigheid", + "settings.permissions.tool.skill.description": "Laad een vaardigheid op naam", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Voer taalserverquery's uit", + "settings.permissions.tool.todowrite.title": "Taken schrijven", + "settings.permissions.tool.todowrite.description": "Werk de takenlijst bij", + "settings.permissions.tool.webfetch.title": "Web ophalen", + "settings.permissions.tool.webfetch.description": "Haal inhoud op van een URL", + "settings.permissions.tool.websearch.title": "Zoeken op internet", + "settings.permissions.tool.websearch.description": "Zoek op internet", + "settings.permissions.tool.external_directory.title": "Externe map", + "settings.permissions.tool.external_directory.description": "Toegang tot bestanden buiten de projectmap", + "settings.permissions.tool.doom_loop.title": "Doom-lus", + "settings.permissions.tool.doom_loop.description": "Herhaalde toolaanroepen met identieke invoer detecteren", + "session.delete.failed.title": "Kan sessie niet verwijderen", + "session.delete.title": "Sessie verwijderen", + "session.delete.confirm": 'Sessie "{{name}}" verwijderen?', + "session.delete.button": "Sessie verwijderen", + "workspace.new": "Nieuwe werkruimte", + "workspace.type.local": "lokaal", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "Kan werkruimte niet maken", + "workspace.delete.failed.title": "Kan werkruimte niet verwijderen", + "workspace.resetting.title": "Werkruimte opnieuw instellen", + "workspace.resetting.description": "Dit kan een minuut duren.", + "workspace.reset.failed.title": "Kan de werkruimte niet opnieuw instellen", + "workspace.reset.success.title": "Werkruimte opnieuw ingesteld", + "workspace.reset.success.description": "De werkruimte komt nu overeen met de standaardbranch.", + "workspace.error.stillPreparing": "Werkruimte is nog in voorbereiding", + "workspace.status.checking": "Controleren op niet-samengevoegde wijzigingen...", + "workspace.status.error": "Kan de git-status niet verifiëren.", + "workspace.status.clean": "Er zijn geen niet-samengevoegde wijzigingen gedetecteerd.", + "workspace.status.dirty": "Niet-samengevoegde wijzigingen gedetecteerd in deze werkruimte.", + "workspace.delete.title": "Werkruimte verwijderen", + "workspace.delete.confirm": 'Werkruimte "{{name}}" verwijderen?', + "workspace.delete.button": "Werkruimte verwijderen", + "workspace.reset.title": "Werkruimte opnieuw instellen", + "workspace.reset.confirm": 'Werkruimte "{{name}}" opnieuw instellen?', + "workspace.reset.button": "Werkruimte opnieuw instellen", + "workspace.reset.archived.none": "Er worden geen actieve sessies gearchiveerd.", + "workspace.reset.archived.one": "Er wordt 1 sessie gearchiveerd.", + "workspace.reset.archived.many": "{{count}} sessies worden gearchiveerd.", + "workspace.reset.note": + "Hierdoor wordt de werkruimte opnieuw ingesteld zodat deze overeenkomt met de standaardbranch.", +} diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts new file mode 100644 index 0000000000000000000000000000000000000000..11958029dc382d3955364ad44968379acdae8422 --- /dev/null +++ b/packages/app/src/i18n/no.ts @@ -0,0 +1,1267 @@ +import { dict as en } from "./en" +type Keys = keyof typeof en + +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Arkiv", + "desktop.menu.edit": "Rediger", + "desktop.menu.view": "Vis", + "desktop.menu.go": "Gå", + "desktop.menu.window": "Vindu", + "desktop.menu.help": "Hjelp", + "desktop.menu.checkForUpdates": "Søk etter oppdateringer...", + "desktop.menu.settings": "Innstillinger", + "desktop.menu.reloadWebview": "Last inn WebView på nytt", + "desktop.menu.restart": "Start på nytt", + "desktop.menu.exportLogs": "Eksporter logger...", + "desktop.menu.newSession": "Ny sesjon", + "desktop.menu.openProject": "Åpne prosjekt...", + "desktop.menu.newWindow": "Nytt vindu", + "desktop.menu.closeWindow": "Lukk vindu", + "desktop.menu.undo": "Angre", + "desktop.menu.redo": "Gjør om", + "desktop.menu.cut": "Klipp ut", + "desktop.menu.copy": "Kopier", + "desktop.menu.paste": "Lim inn", + "desktop.menu.delete": "Slett", + "desktop.menu.selectAll": "Merk alt", + "desktop.menu.toggleSidebar": "Vis eller skjul sidepanel", + "desktop.menu.toggleTerminal": "Vis eller skjul terminal", + "desktop.menu.toggleFileTree": "Vis eller skjul filtreet", + "desktop.menu.reload": "Last inn på nytt", + "desktop.menu.toggleDeveloperTools": "Vis eller skjul utviklerverktøy", + "desktop.menu.actualSize": "Faktisk størrelse", + "desktop.menu.zoomIn": "Zoom inn", + "desktop.menu.zoomOut": "Zoom ut", + "desktop.menu.toggleFullScreen": "Slå fullskjerm av eller på", + "desktop.menu.back": "Tilbake", + "desktop.menu.forward": "Frem", + "desktop.menu.previousSession": "Forrige sesjon", + "desktop.menu.nextSession": "Neste sesjon", + "desktop.menu.previousProject": "Forrige prosjekt", + "desktop.menu.nextProject": "Neste prosjekt", + "desktop.menu.minimize": "Minimer", + "desktop.menu.maximize": "Maksimer", + "desktop.menu.documentation": "OpenCode-dokumentasjon", + "desktop.menu.supportForum": "Brukerstøtteforum", + "desktop.menu.shareFeedback": "Del tilbakemelding", + "desktop.menu.reportBug": "Rapporter en feil", + "desktop.menu.ariaLabel": "OpenCode-meny", + + "desktop.updater.dialog.checkFailed.message": "Søket etter oppdateringer mislyktes.", + "desktop.updater.dialog.checkFailed.title": "Oppdateringsfeil", + "desktop.updater.dialog.upToDate.message": "Du har den nyeste versjonen.", + "desktop.updater.dialog.upToDate.title": "Ingen oppdateringer", + "desktop.updater.dialog.ready.message": "Oppdateringen {{version}} er lastet ned. Vil du starte på nytt nå?", + "desktop.updater.dialog.ready.title": "Oppdateringen er klar", + "desktop.updater.dialog.restart": "Start på nytt", + "desktop.updater.dialog.later": "Senere", + + "desktop.recovery.action.relaunch": "Start appen på nytt", + "desktop.recovery.action.exportLogs": "Eksporter logger", + "desktop.recovery.action.keepWaiting": "Fortsett å vente", + "desktop.recovery.action.quit": "Avslutt", + "desktop.recovery.loadFailed": "OpenCode kunne ikke lastes inn", + "desktop.recovery.terminated": "OpenCode-vinduet ble uventet avsluttet", + "desktop.recovery.unresponsive": "OpenCode svarer ikke", + "desktop.recovery.unresponsive.detail": "Du kan starte appen på nytt, åpne loggene eller fortsette å vente.", + "desktop.recovery.loadFailed.detail": "Vindu: {{window}}\nURL: {{url}}\nFeil: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Vindu: {{window}}\nÅrsak: {{reason}}\nKode: {{code}}", + "desktop.recovery.unknown": "", + + "desktop.dialog.chooseFolder": "Velg en mappe", + "desktop.dialog.chooseFile": "Velg en fil", + "desktop.dialog.saveFile": "Lagre fil", + "desktop.dialog.files": "Filer", + "desktop.server.local": "Lokal server", + + "desktop.wsl.error.windowsOnly": "WSL er bare tilgjengelig på Windows", + "desktop.wsl.error.unavailable": "WSL er utilgjengelig", + "desktop.wsl.error.listInstalled": "Kunne ikke vise installerte WSL-distribusjoner", + "desktop.wsl.error.listOnline": "Kunne ikke vise tilgjengelige WSL-distribusjoner", + "desktop.wsl.error.executeDistro": "Kan ikke kjøre kommandoer i distribusjonen", + "desktop.wsl.error.installWsl": "Installasjonen av WSL mislyktes", + "desktop.wsl.error.installDistro": "Kunne ikke installere distribusjonen: {{distro}}", + "desktop.wsl.error.installOpencode": "Installasjonen av OpenCode mislyktes", + "desktop.wsl.error.alreadyAdded": "{{distro}} er allerede lagt til", + "desktop.wsl.error.opencodeMissing": "opencode er ikke installert i denne distribusjonen", + "desktop.wsl.error.opencodeCannotRun": "opencode er installert, men kunne ikke kjøres", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode er ikke installert i {{distro}}", + "desktop.wsl.error.updateVersion": + "Oppdateringen av OpenCode er fullført, men {{distro}} rapporterer fortsatt {{installed}}; forventet {{expected}}", + "desktop.wsl.error.noVersion": "ingen versjon", + "desktop.wsl.error.serverExited": "WSL-serveren ble avsluttet etter oppstart (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL-serveren ble avsluttet før den var klar (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Tidsavbrudd for tilstandskontrollen av sidecar for {{distro}} etter {{timeout}} ms", + "desktop.wsl.error.commandTimeout": "Tidsavbrudd for {{command}} {{args}} etter {{timeout}} ms", + "desktop.wsl.error.failedPort": "Kunne ikke hente port", + + "desktop.picker.error.notSelected": "Ingen fil ble valgt i filvelgeren", + "desktop.picker.error.sizeLimit": "De valgte vedleggene overskrider grensen på {{limit}} MB", + + "command.category.suggested": "Foreslått", + "command.category.view": "Visning", + "command.category.project": "Prosjekt", + "command.category.provider": "Leverandør", + "command.category.server": "Server", + "command.category.session": "Sesjon", + "command.category.theme": "Tema", + "command.category.language": "Språk", + "command.category.file": "Fil", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Modell", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Tillatelser", + "command.category.workspace": "Arbeidsområde", + "command.category.settings": "Innstillinger", + "command.logs.export": "Eksporter logger", + + "theme.scheme.system": "System", + "theme.scheme.light": "Lys", + "theme.scheme.dark": "Mørk", + + "command.sidebar.toggle": "Veksle sidepanel", + "command.project.open": "Åpne prosjekt", + "command.provider.connect": "Koble til leverandør", + "command.server.switch": "Bytt server", + "command.settings.open": "Åpne innstillinger", + "command.session.previous": "Forrige sesjon", + "command.session.next": "Neste sesjon", + "command.session.previous.unseen": "Forrige uleste sesjon", + "command.session.next.unseen": "Neste uleste sesjon", + "command.session.archive": "Arkiver sesjon", + + "command.palette": "Kommandopalett", + + "command.theme.cycle": "Bytt tema", + "command.theme.set": "Bruk tema: {{theme}}", + "command.theme.scheme.cycle": "Bytt fargevalg", + "command.theme.scheme.set": "Bruk fargevalg: {{scheme}}", + + "command.language.cycle": "Bytt språk", + "command.language.set": "Bruk språk: {{language}}", + + "command.session.new": "Ny sesjon", + "command.file.open": "Åpne fil", + "command.tab.close": "Lukk fane", + "command.context.addSelection": "Legg til markering i kontekst", + "command.context.addSelection.description": "Legg til valgte linjer fra gjeldende fil", + "command.input.focus": "Fokuser inndata", + "command.terminal.toggle": "Veksle terminal", + "command.fileTree.toggle": "Veksle filtre", + "command.review.toggle": "Veksle gjennomgang", + "command.terminal.new": "Ny terminal", + "command.terminal.new.description": "Opprett en ny terminalfane", + "command.steps.toggle": "Veksle trinn", + "command.steps.toggle.description": "Vis eller skjul trinn for gjeldende melding", + "command.message.previous": "Forrige melding", + "command.message.previous.description": "Gå til forrige brukermelding", + "command.message.next": "Neste melding", + "command.message.next.description": "Gå til neste brukermelding", + "command.model.choose": "Velg modell", + "command.model.choose.description": "Velg en annen modell", + "command.mcp.toggle": "Veksle MCP-er", + "command.mcp.toggle.description": "Veksle MCP-er", + "command.agent.cycle": "Bytt agent", + "command.agent.cycle.description": "Bytt til neste agent", + "command.agent.cycle.reverse": "Bytt agent bakover", + "command.agent.cycle.reverse.description": "Bytt til forrige agent", + "command.model.variant.cycle": "Bytt tenkeinnsats", + "command.model.variant.cycle.description": "Bytt til neste innsatsnivå", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Aksepter tillatelser automatisk", + "command.permissions.autoaccept.disable": "Stopp automatisk akseptering av tillatelser", + "command.workspace.toggle": "Veksle arbeidsområder", + "command.workspace.toggle.description": "Aktiver eller deaktiver flere arbeidsområder i sidefeltet", + "command.session.undo": "Angre", + "command.session.undo.description": "Angre siste melding", + "command.session.redo": "Gjør om", + "command.session.redo.description": "Gjør om siste angrede melding", + "command.session.compact": "Komprimer sesjon", + "command.session.compact.description": "Oppsummer sesjonen for å redusere kontekststørrelsen", + "command.session.fork": "Forgren fra melding", + "command.session.fork.description": "Opprett en ny sesjon fra en tidligere melding", + "command.session.share": "Del sesjon", + "command.session.share.description": "Del denne sesjonen og kopier URL-en til utklippstavlen", + "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", + "palette.group.commands": "Kommandoer", + "palette.group.files": "Filer", + + "dialog.provider.search.placeholder": "Søk etter leverandører", + "dialog.provider.empty": "Ingen leverandører funnet", + "dialog.provider.group.popular": "Populære", + "dialog.provider.group.other": "Andre", + "dialog.provider.custom.label": "Egendefinert OpenAI-kompatibel leverandør", + "dialog.provider.tag.recommended": "Anbefalt", + "dialog.provider.opencode.note": "Utvalgte modeller inkludert Claude, GPT, Gemini og mer", + "dialog.provider.opencode.tagline": "Pålitelige, optimaliserte modeller", + "dialog.provider.opencodeGo.tagline": "Rimelig abonnement for alle", + "dialog.provider.anthropic.note": "Direkte tilgang til Claude-modeller, inkludert Pro og Max", + "dialog.provider.copilot.note": "AI-modeller for kodeassistanse via GitHub Copilot", + "dialog.provider.openai.note": "GPT-modeller for raske, dyktige generelle AI-oppgaver", + "dialog.provider.google.note": "Gemini-modeller for raske, strukturerte svar", + "dialog.provider.openrouter.note": "Tilgang til alle støttede modeller fra én leverandør", + "dialog.provider.vercel.note": "Enhetlig tilgang til AI-modeller med smart ruting", + + "dialog.model.select.title": "Velg modell", + "dialog.model.search.placeholder": "Søk etter modeller", + "dialog.model.empty": "Ingen modellresultater", + "dialog.model.manage": "Administrer modeller", + "dialog.model.manage.description": "Tilpass hvilke modeller som vises i modellvelgeren.", + "dialog.model.manage.provider.toggle": "Veksle alle {{provider}}-modeller", + + "dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode", + "dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører", + "dialog.model.unpaid.viewMoreProviders": "Se over 70 flere leverandører", + + "dialog.provider.viewAll": "Vis flere leverandører", + + "provider.connect.title": "Koble til {{provider}}", + "provider.connect.title.anthropicProMax": "Logg inn med Claude Pro/Max", + "provider.connect.selectMethod": "Velg innloggingsmetode for {{provider}}.", + "provider.connect.method.apiKey": "API-nøkkel", + "provider.connect.method.browser": "Nettleser", + "provider.connect.method.headless": "Uten nettleser", + "provider.connect.status.inProgress": "Autorisering pågår...", + "provider.connect.status.waiting": "Venter på autorisering...", + "provider.connect.status.failed": "Autorisering mislyktes: {{error}}", + "provider.connect.apiKey.description": + "Skriv inn API-nøkkelen din for {{provider}} for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.", + "provider.connect.apiKey.label": "{{provider}}-API-nøkkel", + "provider.connect.apiKey.placeholder": "API-nøkkel", + "provider.connect.apiKey.required": "API-nøkkel er påkrevd", + "provider.connect.opencodeZen.line1": + "OpenCode Zen gir deg tilgang til et utvalg av pålitelige optimaliserte modeller for kodeagenter.", + "provider.connect.opencodeZen.line2": + "Med én enkelt API-nøkkel får du tilgang til modeller som Claude, GPT, Gemini, GLM og flere.", + "provider.connect.opencodeZen.visit.prefix": "Besøk ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " for å hente API-nøkkelen din.", + "provider.connect.oauth.code.visit.prefix": "Besøk ", + "provider.connect.oauth.code.visit.link": "denne lenken", + "provider.connect.oauth.code.visit.suffix": + " for å hente autorisasjonskoden din for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.", + "provider.connect.oauth.code.label": "{{method}}-autorisasjonskode", + "provider.connect.oauth.code.placeholder": "Autorisasjonskode", + "provider.connect.oauth.code.required": "Autorisasjonskode er påkrevd", + "provider.connect.oauth.code.invalid": "Ugyldig autorisasjonskode", + "provider.connect.oauth.auto.visit.prefix": "Besøk ", + "provider.connect.oauth.auto.visit.link": "denne lenken", + "provider.connect.oauth.auto.visit.suffix": + " og skriv inn koden nedenfor for å koble til kontoen din og bruke {{provider}}-modeller i OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Bekreftelseskode", + "provider.connect.toast.connected.title": "{{provider}} tilkoblet", + "provider.connect.toast.connected.description": "{{provider}}-modeller er nå tilgjengelige.", + + "provider.custom.title": "Egendefinert leverandør", + "provider.custom.unavailable": "Egendefinerte leverandører er ikke tilgjengelige på denne serveren", + "provider.custom.description.prefix": "Konfigurer en OpenAI-kompatibel leverandør. Se ", + "provider.custom.description.link": "dokumentasjon for leverandørkonfigurasjon", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Leverandør-ID", + "provider.custom.field.providerID.placeholder": "minleverandør", + "provider.custom.field.providerID.description": "Små bokstaver, tall, bindestreker eller understreker", + "provider.custom.field.name.label": "Visningsnavn", + "provider.custom.field.name.placeholder": "Min AI-leverandør", + "provider.custom.field.baseURL.label": "Basis-URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API-nøkkel", + "provider.custom.field.apiKey.placeholder": "API-nøkkel", + "provider.custom.field.apiKey.description": + "Valgfritt. La stå tomt hvis du administrerer autentisering via HTTP-hoder.", + "provider.custom.models.label": "Modeller", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "modell-id", + "provider.custom.models.name.label": "Navn", + "provider.custom.models.name.placeholder": "Visningsnavn", + "provider.custom.models.remove": "Fjern modell", + "provider.custom.models.add": "Legg til modell", + "provider.custom.headers.label": "HTTP-hoder (valgfritt)", + "provider.custom.headers.key.label": "HTTP-hode", + "provider.custom.headers.key.placeholder": "Navn på HTTP-hode", + "provider.custom.headers.value.label": "Verdi", + "provider.custom.headers.value.placeholder": "verdi", + "provider.custom.headers.remove": "Fjern HTTP-hode", + "provider.custom.headers.add": "Legg til HTTP-hode", + "provider.custom.error.providerID.required": "Leverandør-ID er påkrevd", + "provider.custom.error.providerID.format": "Bruk små bokstaver, tall, bindestreker eller understreker", + "provider.custom.error.providerID.exists": "Denne leverandør-ID-en finnes allerede", + "provider.custom.error.name.required": "Visningsnavn er påkrevd", + "provider.custom.error.baseURL.required": "Basis-URL er påkrevd", + "provider.custom.error.baseURL.format": "Må starte med http:// eller https://", + "provider.custom.error.required": "Påkrevd", + "provider.custom.error.duplicate": "Duplikat", + + "provider.disconnect.toast.disconnected.title": "{{provider}} frakoblet", + "provider.disconnect.toast.disconnected.description": "Modeller fra {{provider}} er ikke lenger tilgjengelige.", + + "model.tag.free": "Gratis", + "model.tag.latest": "Nyeste", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekst", + "model.input.image": "bilde", + "model.input.audio": "lyd", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Tillater: {{inputs}}", + "model.tooltip.reasoning.allowed": "Tillater resonnering", + "model.tooltip.reasoning.none": "Ingen resonnering", + "model.tooltip.context": "Kontekstgrense {{limit}}", + + "common.search.placeholder": "Søk", + "common.goBack": "Gå tilbake", + "common.goForward": "Gå frem", + "common.loading": "Laster", + "common.loading.ellipsis": "...", + "common.cancel": "Avbryt", + "common.connect": "Koble til", + "common.disconnect": "Koble fra", + "common.continue": "Fortsett", + "common.submit": "Send inn", + "common.save": "Lagre", + "common.saving": "Lagrer...", + "common.default": "Standard", + "common.attachment": "vedlegg", + + "prompt.placeholder.shell": "Skriv inn shell-kommando... {{example}}", + "prompt.placeholder.normal": 'Spør om hva som helst... "{{example}}"', + "prompt.placeholder.simple": "Spør om hva som helst...", + "prompt.placeholder.summarizeComments": "Oppsummer kommentarer…", + "prompt.placeholder.summarizeComment": "Oppsummer kommentar…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "ESC for å avslutte", + + "prompt.example.1": "Fiks en TODO i kodebasen", + "prompt.example.2": "Hva er teknologistabelen i dette prosjektet?", + "prompt.example.3": "Fiks ødelagte tester", + "prompt.example.4": "Forklar hvordan autentisering fungerer", + "prompt.example.5": "Finn og fiks sikkerhetssårbarheter", + "prompt.example.6": "Legg til enhetstester for brukertjenesten", + "prompt.example.7": "Refaktorer denne funksjonen for bedre lesbarhet", + "prompt.example.8": "Hva betyr denne feilen?", + "prompt.example.9": "Hjelp meg med å feilsøke dette problemet", + "prompt.example.10": "Generer API-dokumentasjon", + "prompt.example.11": "Optimaliser databasespørringer", + "prompt.example.12": "Legg til inndatavalidering", + "prompt.example.13": "Lag en ny komponent for...", + "prompt.example.14": "Hvordan deployer jeg dette prosjektet?", + "prompt.example.15": "Gjennomgå koden min for beste praksis", + "prompt.example.16": "Legg til feilhåndtering i denne funksjonen", + "prompt.example.17": "Forklar dette regex-mønsteret", + "prompt.example.18": "Konverter dette til TypeScript", + "prompt.example.19": "Legg til logging i hele kodebasen", + "prompt.example.20": "Hvilke avhengigheter er utdaterte?", + "prompt.example.21": "Hjelp meg med å skrive et migreringsskript", + "prompt.example.22": "Implementer hurtigbufring for dette endepunktet", + "prompt.example.23": "Legg til paginering i denne listen", + "prompt.example.24": "Lag en CLI-kommando for...", + "prompt.example.25": "Hvordan fungerer miljøvariabler her?", + + "prompt.popover.emptyResults": "Ingen matchende resultater", + "prompt.popover.emptyCommands": "Ingen matchende kommandoer", + "prompt.dropzone.label": "Slipp bilder, PDF-er eller tekstfiler her", + "prompt.dropzone.file.label": "Slipp for å @nevne fil", + "prompt.slash.badge.custom": "egendefinert", + "prompt.slash.badge.skill": "ferdighet", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktiv", + "prompt.context.includeActiveFile": "Inkluder aktiv fil", + "prompt.context.removeActiveFile": "Fjern aktiv fil fra kontekst", + "prompt.context.removeFile": "Fjern fil fra kontekst", + "prompt.action.attachFile": "Legg ved fil", + "prompt.menu.addImagesAndFiles": "Legg til filer og mer", + "prompt.menu.imagesAndFiles": "Bilder og filer", + "prompt.menu.commands": "Kommandoer", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Shell-kommando", + "prompt.attachment.remove": "Fjern vedlegg", + "prompt.action.send": "Send", + "prompt.action.stop": "Stopp", + + "prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg", + "prompt.toast.attachmentDuplicate.title": "Denne filen er allerede lastet opp", + "prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.", + "prompt.toast.modelAgentRequired.title": "Velg en agent og modell", + "prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.", + "prompt.toast.worktreeCreateFailed.title": "Kunne ikke opprette worktree", + "prompt.toast.sessionCreateFailed.title": "Kunne ikke opprette sesjon", + "prompt.toast.shellSendFailed.title": "Kunne ikke sende shell-kommando", + "prompt.toast.commandSendFailed.title": "Kunne ikke sende kommando", + "prompt.toast.promptSendFailed.title": "Kunne ikke sende forespørsel", + "prompt.toast.promptSendFailed.description": "Kunne ikke hente sesjon", + + "dialog.mcp.title": "MCP-er", + "dialog.mcp.description": "{{enabled}} av {{total}} aktivert", + "dialog.mcp.empty": "Ingen MCP-er konfigurert", + + "dialog.lsp.empty": "LSP-er automatisk oppdaget fra filtyper", + "dialog.plugins.empty": "Programtillegg konfigurert i opencode.json", + + "mcp.status.connected": "tilkoblet", + "mcp.status.failed": "mislyktes", + "mcp.status.needs_auth": "trenger autentisering", + "mcp.auth.clickToAuthenticate": "Klikk for å autentisere", + "mcp.status.disabled": "deaktivert", + + "dialog.fork.empty": "Ingen meldinger å forgrene fra", + + "dialog.directory.search.placeholder": "Søk etter mapper", + "dialog.directory.empty": "Ingen mapper funnet", + "dialog.directory.action.selectFile": "Velg fil", + "dialog.directory.action.selectFolder": "Velg mappe", + "dialog.directory.root": "Rot", + "dialog.directory.parent": "Overordnet", + "dialog.directory.readError": "Kan ikke lese denne mappen", + + "dialog.server.title": "Servere", + "dialog.server.description": "Bytt hvilken OpenCode-server denne appen kobler til.", + "dialog.server.search.placeholder": "Søk etter servere", + "dialog.server.empty": "Ingen servere ennå", + "dialog.server.add.title": "Legg til en server", + "dialog.server.add.url": "Server-URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Kunne ikke koble til server", + "dialog.server.add.checking": "Sjekker...", + "dialog.server.add.button": "Legg til server", + "dialog.server.add.name": "Servernavn (valgfritt)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Brukernavn (valgfritt)", + "dialog.server.add.password": "Passord (valgfritt)", + "dialog.server.edit.title": "Rediger server", + "dialog.server.default.title": "Standardserver", + "dialog.server.default.description": + "Koble til denne serveren ved oppstart i stedet for å starte en lokal server. Krever omstart.", + "dialog.server.default.none": "Ingen server valgt", + "dialog.server.default.set": "Sett gjeldende server som standard", + "dialog.server.default.clear": "Tøm", + "dialog.server.action.remove": "Fjern server", + + "dialog.server.menu.edit": "Rediger", + "dialog.server.menu.default": "Sett som standard", + "dialog.server.menu.defaultRemove": "Fjern standard", + "dialog.server.menu.delete": "Slett", + "dialog.server.current": "Gjeldende server", + "dialog.server.status.default": "Standard", + + "dialog.project.edit.title": "Rediger prosjekt", + "dialog.project.edit.name": "Navn", + "dialog.project.edit.icon": "Ikon", + "dialog.project.edit.icon.alt": "Prosjektikon", + "dialog.project.edit.icon.hint": "Klikk eller dra et bilde", + "dialog.project.edit.icon.recommended": "Anbefalt: 128x128px", + "dialog.project.edit.color": "Farge", + "dialog.project.edit.color.select": "Velg fargen {{color}}", + "dialog.project.edit.worktree.startup": "Oppstartsskript for arbeidsområde", + "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.', + "context.breakdown.system": "System", + "context.breakdown.user": "Bruker", + "context.breakdown.assistant": "Assistent", + "context.breakdown.tool": "Verktøykall", + "context.breakdown.other": "Annet", + + "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", + "context.stats.model": "Modell", + "context.stats.limit": "Kontekstgrense", + "context.stats.totalTokens": "Totalt antall tokener", + "context.stats.usage": "Forbruk", + "context.stats.inputTokens": "Inndatatokener", + "context.stats.outputTokens": "Utdatatokener", + "context.stats.reasoningTokens": "Resonneringstokener", + "context.stats.cacheTokens": "Hurtigbuffertokener (les/skriv)", + "context.stats.userMessages": "Brukermeldinger", + "context.stats.assistantMessages": "Assistentmeldinger", + "context.stats.totalCost": "Total kostnad", + "context.stats.sessionCreated": "Sesjon opprettet", + "context.stats.lastActivity": "Siste aktivitet", + + "context.usage.tokens": "Tokener", + "context.usage.usage": "Forbruk", + "context.usage.cost": "Kostnad", + "context.usage.clickToView": "Klikk for å se kontekst", + "context.usage.view": "Se kontekstforbruk", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "Språk", + "toast.language.description": "Byttet til {{language}}", + + "toast.theme.title": "Tema byttet", + "toast.scheme.title": "Fargevalg", + + "toast.workspace.enabled.title": "Arbeidsområder aktivert", + "toast.workspace.enabled.description": "Flere arbeidsområder vises nå i sidefeltet", + "toast.workspace.disabled.title": "Arbeidsområder deaktivert", + "toast.workspace.disabled.description": "Kun hovedarbeidsområdet vises i sidefeltet", + + "toast.permissions.autoaccept.on.title": "Aksepterer tillatelser automatisk", + "toast.permissions.autoaccept.on.description": "Forespørsler om tillatelse vil bli godkjent automatisk", + "toast.permissions.autoaccept.off.title": "Stoppet automatisk akseptering av tillatelser", + "toast.permissions.autoaccept.off.description": "Forespørsler om tillatelse vil kreve godkjenning", + + "toast.model.none.title": "Ingen modell valgt", + "toast.model.none.description": "Koble til en leverandør for å oppsummere denne sesjonen", + + "toast.file.loadFailed.title": "Kunne ikke laste fil", + "toast.file.listFailed.title": "Kunne ikke liste filer", + + "toast.context.noLineSelection.title": "Ingen linjevalg", + "toast.context.noLineSelection.description": "Velg først et linjeområde i en filfane.", + + "toast.session.share.copyFailed.title": "Kunne ikke kopiere URL til utklippstavlen", + "toast.session.share.success.title": "Sesjon delt", + "toast.session.share.success.description": "Delings-URL kopiert til utklippstavlen!", + "toast.session.share.failed.title": "Kunne ikke dele sesjon", + "toast.session.share.failed.description": "Det oppstod en feil under deling av sesjonen", + + "toast.session.unshare.success.title": "Deling av sesjon stoppet", + "toast.session.unshare.success.description": "Sesjonen deles ikke lenger!", + "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", + "toast.update.description": "En ny versjon av OpenCode ({{version}}) er nå tilgjengelig for installasjon.", + "toast.update.action.installRestart": "Installer og start på nytt", + "toast.update.action.notYet": "Ikke nå", + + "error.page.title": "Noe gikk galt", + "error.page.description": "Det oppstod en feil under lasting av applikasjonen.", + "error.page.details.label": "Feildetaljer", + "error.page.action.restart": "Start på nytt", + "error.page.action.report": "Rapporter feil", + "error.page.action.reported": "Feil rapportert", + "error.page.action.checking": "Sjekker...", + "error.page.action.checkUpdates": "Se etter oppdateringer", + "error.page.action.updateTo": "Oppdater til {{version}}", + "error.page.report.prefix": "Vennligst rapporter denne feilen til OpenCode-teamet", + "error.page.report.discord": "på Discord", + "error.page.version": "Versjon: {{version}}", + + "error.dev.rootNotFound": + "Rotelement ikke funnet. Glemte du å legge det til i index.html? Eller kanskje id-attributtet er feilstavet?", + + "error.serverSync.connectFailed": "Kunne ikke koble til server. Kjører det en server på `{{url}}`?", + "directory.error.invalidUrl": "Ugyldig mappe i URL-en.", + + "error.chain.unknown": "Ukjent feil", + "error.chain.causedBy": "Forårsaket av:", + "error.chain.apiError": "API-feil", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Kan prøves på nytt: {{retryable}}", + "error.chain.responseBody": "Responsinnhold:\n{{body}}", + "error.chain.didYouMean": "Mente du: {{suggestions}}", + "error.chain.modelNotFound": "Modell ikke funnet: {{provider}}/{{model}}", + "error.chain.checkConfig": "Sjekk leverandør-/modellnavnene i konfigurasjonen din (opencode.json)", + "error.chain.mcpFailed": 'MCP-server "{{name}}" mislyktes. Merk at OpenCode ikke støtter MCP-autentisering ennå.', + "error.chain.providerAuthFailed": "Leverandørautentisering mislyktes ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Kunne ikke initialisere leverandør "{{provider}}". Sjekk legitimasjon og konfigurasjon.', + "error.chain.configJsonInvalid": "Konfigurasjonsfilen på {{path}} er ikke gyldig JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Konfigurasjonsfilen på {{path}} er ikke gyldig JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Mappen "{{dir}}" i {{path}} er ikke gyldig. Gi mappen nytt navn til "{{suggestion}}" eller fjern den. Dette er en vanlig skrivefeil.', + "error.chain.configFrontmatterError": "Kunne ikke analysere frontmatter i {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfigurasjonsfilen på {{path}} er ugyldig", + "error.chain.configInvalidWithMessage": "Konfigurasjonsfilen på {{path}} er ugyldig: {{message}}", + + "notification.permission.title": "Tillatelse påkrevd", + "notification.permission.description": "{{sessionTitle}} i {{projectName}} trenger tillatelse", + "notification.question.title": "Spørsmål", + "notification.question.description": "{{sessionTitle}} i {{projectName}} har et spørsmål", + "notification.action.goToSession": "Gå til sesjon", + + "notification.session.responseReady.title": "Svar klart", + "notification.session.error.title": "Sesjonsfeil", + "notification.session.error.fallbackDescription": "Det oppstod en feil", + + "home.recentProjects": "Nylige prosjekter", + "home.empty.title": "Ingen nylige prosjekter", + "home.empty.description": "Kom i gang ved å åpne et lokalt prosjekt", + + "session.tab.session": "Sesjon", + "session.tab.review": "Gjennomgang", + "session.tab.context": "Kontekst", + "session.panel.reviewAndFiles": "Gjennomgang og filer", + "session.review.filesChanged": "{{count}} filer endret", + "session.review.change.one": "Endring", + "session.review.change.other": "Endringer", + "session.review.loadingChanges": "Laster endringer...", + "session.review.empty": "Ingen endringer i denne sesjonen ennå", + "session.review.noVcs": "Ingen Git-versjonskontrollsystem oppdaget, endringer vises ikke", + "session.review.noSnapshot": + "Snapshot-sporing er deaktivert i konfigurasjonen, så sesjonsendringer er ikke tilgjengelige", + "session.review.noChanges": "Ingen endringer", + + "session.files.selectToOpen": "Velg en fil å åpne", + "session.files.all": "Alle filer", + "session.files.empty": "Ingen filer", + "session.files.binaryContent": "Binær fil (innhold kan ikke vises)", + + "session.messages.renderEarlier": "Vis tidligere meldinger", + "session.messages.loadingEarlier": "Laster inn tidligere meldinger...", + "session.messages.loadEarlier": "Last inn tidligere meldinger", + "session.messages.loading": "Laster meldinger...", + "session.messages.jumpToLatest": "Hopp til nyeste", + + "session.context.addToContext": "Legg til {{selection}} i kontekst", + "session.todo.title": "Oppgaver", + "session.todo.collapse": "Skjul", + "session.todo.expand": "Utvid", + "session.followupDock.summary.one": "{{count}} melding i kø", + "session.followupDock.summary.other": "{{count}} meldinger i kø", + "session.followupDock.sendNow": "Send nå", + "session.followupDock.edit": "Rediger", + "session.followupDock.collapse": "Skjul meldinger i kø", + "session.followupDock.expand": "Utvid meldinger i kø", + "session.revertDock.summary.one": "{{count}} tilbakestilt melding", + "session.revertDock.summary.other": "{{count}} tilbakestilte meldinger", + "session.revertDock.collapse": "Skjul tilbakestilte meldinger", + "session.revertDock.expand": "Utvid tilbakestilte meldinger", + "session.revertDock.restore": "Gjenopprett melding", + + "session.new.title": "Bygg hva som helst", + "session.new.worktree.main": "Hovedgren", + "session.new.worktree.mainWithBranch": "Hovedgren ({{branch}})", + "session.new.worktree.create": "Opprett nytt worktree", + "session.new.lastModified": "Sist endret", + + "session.header.search.placeholder": "Søk i {{project}}", + "session.header.searchFiles": "Søk etter filer", + "session.header.openIn": "Åpne i", + "session.header.open.action": "Åpne {{app}}", + "session.header.open.ariaLabel": "Åpne i {{app}}", + "session.header.open.menu": "Åpne alternativer", + "session.header.open.copyPath": "Kopier bane", + + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Serverkonfigurasjoner", + "status.popover.tab.servers": "Servere", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Programtillegg", + "status.popover.action.manageServers": "Administrer servere", + + "session.share.popover.title": "Publiser på nett", + "session.share.popover.description.shared": + "Denne sesjonen er offentlig på nettet. Den er tilgjengelig for alle med lenken.", + "session.share.popover.description.unshared": + "Del sesjonen offentlig på nettet. Den vil være tilgjengelig for alle med lenken.", + "session.share.action.share": "Del", + "session.share.action.publish": "Publiser", + "session.share.action.publishing": "Publiserer...", + "session.share.action.unpublish": "Avpubliser", + "session.share.action.unpublishing": "Avpubliserer...", + "session.share.action.view": "Vis", + "session.share.copy.copied": "Kopiert", + "session.share.copy.copyLink": "Kopier lenke", + + "lsp.tooltip.none": "Ingen LSP-servere", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Laster prompt...", + "terminal.loading": "Laster terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Lukk terminal", + "terminal.connectionLost.title": "Tilkobling mistet", + "terminal.connectionLost.description": + "Terminalforbindelsen ble avbrutt. Dette kan skje når serveren starter på nytt.", + "terminal.connectTicket.csrfError": + "Tilkoblingsbilletten for PTY ble avvist av opprinnelses- eller CSRF-kontroller. Kontroller serverens CORS-konfigurasjon.", + "terminal.connectTicket.statusError": "Tilkoblingsbilletten for PTY mislyktes med {{status}}", + + "titlebar.update": "Oppdater", + "titlebar.updateVersion": "Oppdater {{version}}", + + "common.closeTab": "Lukk fane", + "common.dismiss": "Avvis", + "common.requestFailed": "Forespørsel mislyktes", + "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", + "common.close": "Lukk", + "common.edit": "Rediger", + "common.loadMore": "Last inn flere", + "common.key.esc": "ESC", + + "sidebar.menu.toggle": "Veksle meny", + "sidebar.nav.projectsAndSessions": "Prosjekter og sesjoner", + "sidebar.settings": "Innstillinger", + "sidebar.help": "Hjelp", + "sidebar.workspaces.enable": "Aktiver arbeidsområder", + "sidebar.workspaces.disable": "Deaktiver arbeidsområder", + "sidebar.gettingStarted.title": "Kom i gang", + "sidebar.gettingStarted.line1": "OpenCode inkluderer gratis modeller så du kan starte umiddelbart.", + "sidebar.gettingStarted.line2": "Koble til en leverandør for å bruke modeller, inkl. Claude, GPT, Gemini osv.", + "sidebar.project.recentSessions": "Nylige sesjoner", + "sidebar.project.viewAllSessions": "Vis alle sesjoner", + "sidebar.project.clearNotifications": "Fjern varsler", + + "app.name.desktop": "OpenCode Desktop", + + "settings.section.desktop": "Skrivebord", + "settings.section.server": "Server", + "settings.tab.general": "Generelt", + "settings.tab.shortcuts": "Snarveier", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL-integrasjon", + "settings.desktop.wsl.description": "Kjør OpenCode-serveren i WSL på Windows.", + + "settings.general.section.appearance": "Utseende", + "settings.general.section.notifications": "Systemvarsler", + "settings.general.section.updates": "Oppdateringer", + "settings.general.section.sounds": "Lydeffekter", + "settings.general.section.feed": "Feed", + "settings.general.section.display": "Skjerm", + + "settings.general.row.language.title": "Språk", + "settings.general.row.language.description": "Endre visningsspråket for OpenCode", + "settings.general.row.appearance.title": "Utseende", + "settings.general.row.appearance.description": "Tilpass hvordan OpenCode ser ut på enheten din", + "settings.general.row.colorScheme.title": "Fargevalg", + "settings.general.row.colorScheme.description": + "Velg om OpenCode skal følge systeminnstillingen eller bruke lyst eller mørkt tema", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Tilpass temaet i OpenCode.", + "settings.general.row.font.title": "Kodefont", + "settings.general.row.font.description": "Tilpass skrifttypen som brukes i kodeblokker", + "settings.general.row.terminalFont.title": "Terminalskrift", + "settings.general.row.terminalFont.description": "Tilpass skrifttypen som brukes i terminalen", + "settings.general.row.uiFont.title": "UI-skrift", + "settings.general.row.uiFont.description": "Tilpass skrifttypen som brukes i hele grensesnittet", + "settings.general.row.followup.title": "Oppfølgingsatferd", + "settings.general.row.followup.description": "Velg om oppfølgingsspørsmål skal kjøres umiddelbart eller vente i kø", + "settings.general.row.followup.option.queue": "Kø", + "settings.general.row.followup.option.steer": "Styr", + "settings.general.row.reasoningSummaries.title": "Vis resonneringssammendrag", + "settings.general.row.reasoningSummaries.description": "Vis sammendrag av modellresonnering i tidslinjen", + "settings.general.row.shellToolPartsExpanded.title": "Utvid shell-verktøydeler", + "settings.general.row.shellToolPartsExpanded.description": "Vis shell-verktøydeler utvidet som standard i tidslinjen", + "settings.general.row.editToolPartsExpanded.title": "Utvid edit-verktøydeler", + "settings.general.row.editToolPartsExpanded.description": + "Vis edit-, write- og patch-verktøydeler utvidet som standard i tidslinjen", + "settings.general.row.wayland.title": "Bruk Wayland direkte", + "settings.general.row.wayland.description": "Deaktiver X11-reserveløsningen på Wayland. Krever omstart.", + "settings.general.row.wayland.tooltip": + "På Linux med skjermer med blandet oppdateringsfrekvens kan direkte Wayland-støtte være mer stabilt.", + + "settings.general.row.releaseNotes.title": "Utgivelsesnotater", + "settings.general.row.releaseNotes.description": 'Vis "Hva er nytt"-vinduer etter oppdateringer', + + "settings.updates.row.startup.title": "Se etter oppdateringer ved oppstart", + "settings.updates.row.startup.description": "Se automatisk etter oppdateringer når OpenCode starter", + "settings.updates.row.check.title": "Se etter oppdateringer", + "settings.updates.row.check.description": "Se etter oppdateringer manuelt og installer hvis tilgjengelig", + "settings.updates.action.checkNow": "Sjekk nå", + "settings.updates.action.checking": "Sjekker...", + "settings.updates.toast.latest.title": "OpenCode er oppdatert", + "settings.updates.toast.latest.description": "Du bruker den nyeste versjonen av OpenCode.", + "sound.option.none": "Ingen", + "sound.option.alert01": "Varsel 01", + "sound.option.alert02": "Varsel 02", + "sound.option.alert03": "Varsel 03", + "sound.option.alert04": "Varsel 04", + "sound.option.alert05": "Varsel 05", + "sound.option.alert06": "Varsel 06", + "sound.option.alert07": "Varsel 07", + "sound.option.alert08": "Varsel 08", + "sound.option.alert09": "Varsel 09", + "sound.option.alert10": "Varsel 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nei 01", + "sound.option.nope02": "Nei 02", + "sound.option.nope03": "Nei 03", + "sound.option.nope04": "Nei 04", + "sound.option.nope05": "Nei 05", + "sound.option.nope06": "Nei 06", + "sound.option.nope07": "Nei 07", + "sound.option.nope08": "Nei 08", + "sound.option.nope09": "Nei 09", + "sound.option.nope10": "Nei 10", + "sound.option.nope11": "Nei 11", + "sound.option.nope12": "Nei 12", + "sound.option.yup01": "Ja 01", + "sound.option.yup02": "Ja 02", + "sound.option.yup03": "Ja 03", + "sound.option.yup04": "Ja 04", + "sound.option.yup05": "Ja 05", + "sound.option.yup06": "Ja 06", + + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Vis systemvarsel når agenten er ferdig eller trenger oppmerksomhet", + "settings.general.notifications.permissions.title": "Tillatelser", + "settings.general.notifications.permissions.description": "Vis systemvarsel når en tillatelse er påkrevd", + "settings.general.notifications.errors.title": "Feil", + "settings.general.notifications.errors.description": "Vis systemvarsel når det oppstår en feil", + + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Spill av lyd når agenten er ferdig eller trenger oppmerksomhet", + "settings.general.sounds.permissions.title": "Tillatelser", + "settings.general.sounds.permissions.description": "Spill av lyd når en tillatelse er påkrevd", + "settings.general.sounds.errors.title": "Feil", + "settings.general.sounds.errors.description": "Spill av lyd når det oppstår en feil", + + "settings.shortcuts.title": "Tastatursnarveier", + "settings.shortcuts.reset.button": "Tilbakestill til standard", + "settings.shortcuts.reset.toast.title": "Snarveier tilbakestilt", + "settings.shortcuts.reset.toast.description": "Tastatursnarveier er tilbakestilt til standard.", + "settings.shortcuts.conflict.title": "Snarvei allerede i bruk", + "settings.shortcuts.conflict.description": "{{keybind}} er allerede tilordnet {{titles}}.", + "settings.shortcuts.unassigned": "Ikke tilordnet", + "settings.shortcuts.pressKeys": "Trykk taster", + "settings.shortcuts.search.placeholder": "Søk etter snarveier", + "settings.shortcuts.search.empty": "Ingen snarveier funnet", + + "settings.shortcuts.group.general": "Generelt", + "settings.shortcuts.group.session": "Sesjon", + "settings.shortcuts.group.navigation": "Navigasjon", + "settings.shortcuts.group.modelAndAgent": "Modell og agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + + "settings.providers.title": "Leverandører", + "settings.providers.description": "Leverandørinnstillinger vil kunne konfigureres her.", + "settings.providers.section.connected": "Tilkoblede leverandører", + "settings.providers.connected.empty": "Ingen tilkoblede leverandører", + "settings.providers.section.popular": "Populære leverandører", + "settings.providers.tag.environment": "Miljø", + "settings.providers.tag.config": "Konfigurasjon", + "settings.providers.tag.custom": "Tilpasset", + "settings.providers.tag.other": "Annet", + "settings.models.title": "Modeller", + "settings.models.description": "Modellinnstillinger vil kunne konfigureres her.", + "settings.agents.title": "Agenter", + "settings.agents.description": "Agentinnstillinger vil kunne konfigureres her.", + "settings.commands.title": "Kommandoer", + "settings.commands.description": "Kommandoinnstillinger vil kunne konfigureres her.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP-innstillinger vil kunne konfigureres her.", + + "settings.permissions.title": "Tillatelser", + "settings.permissions.description": "Kontroller hvilke verktøy serveren kan bruke som standard.", + "settings.permissions.section.tools": "Verktøy", + "settings.permissions.toast.updateFailed.title": "Kunne ikke oppdatere tillatelser", + + "settings.permissions.action.allow": "Tillat", + "settings.permissions.action.ask": "Spør", + "settings.permissions.action.deny": "Avslå", + + "settings.permissions.tool.read.title": "Les", + "settings.permissions.tool.read.description": "Lesing av en fil (matcher filbanen)", + "settings.permissions.tool.edit.title": "Rediger", + "settings.permissions.tool.edit.description": + "Endre filer, inkludert redigeringer, skriving, patcher og multi-redigeringer", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Match filer ved hjelp av glob-mønstre", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Søk i filinnhold ved hjelp av regulære uttrykk", + "settings.permissions.tool.list.title": "Liste", + "settings.permissions.tool.list.description": "List opp filer i en mappe", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Kjør shell-kommandoer", + "settings.permissions.tool.task.title": "Oppgave", + "settings.permissions.tool.task.description": "Start underagenter", + "settings.permissions.tool.skill.title": "Ferdighet", + "settings.permissions.tool.skill.description": "Last en ferdighet etter navn", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Kjør språkserverforespørsler", + "settings.permissions.tool.todowrite.title": "Skriv gjøremål", + "settings.permissions.tool.todowrite.description": "Oppdater gjøremålslisten", + "settings.permissions.tool.webfetch.title": "Webhenting", + "settings.permissions.tool.webfetch.description": "Hent innhold fra en URL", + "settings.permissions.tool.websearch.title": "Websøk", + "settings.permissions.tool.websearch.description": "Søk på nettet", + "settings.permissions.tool.external_directory.title": "Ekstern mappe", + "settings.permissions.tool.external_directory.description": "Få tilgang til filer utenfor prosjektmappen", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Oppdager gjentatte verktøykall med identiske inndata", + + "session.delete.failed.title": "Kunne ikke slette sesjon", + "session.delete.title": "Slett sesjon", + "session.delete.confirm": 'Slette sesjonen "{{name}}"?', + "session.delete.button": "Slett sesjon", + + "workspace.new": "Nytt arbeidsområde", + "workspace.type.local": "lokal", + "workspace.type.sandbox": "sandkasse", + "workspace.create.failed.title": "Kunne ikke opprette arbeidsområde", + "workspace.delete.failed.title": "Kunne ikke slette arbeidsområde", + "workspace.resetting.title": "Tilbakestiller arbeidsområde", + "workspace.resetting.description": "Dette kan ta et minutt.", + "workspace.reset.failed.title": "Kunne ikke tilbakestille arbeidsområde", + "workspace.reset.success.title": "Arbeidsområde tilbakestilt", + "workspace.reset.success.description": "Arbeidsområdet samsvarer nå med standardgrenen.", + "workspace.error.stillPreparing": "Arbeidsområdet klargjøres fortsatt", + "workspace.status.checking": "Sjekker for ikke-sammenslåtte endringer...", + "workspace.status.error": "Kunne ikke bekrefte git-status.", + "workspace.status.clean": "Ingen ikke-sammenslåtte endringer oppdaget.", + "workspace.status.dirty": "Ikke-sammenslåtte endringer oppdaget i dette arbeidsområdet.", + "workspace.delete.title": "Slett arbeidsområde", + "workspace.delete.confirm": 'Slette arbeidsområdet "{{name}}"?', + "workspace.delete.button": "Slett arbeidsområde", + "workspace.reset.title": "Tilbakestill arbeidsområde", + "workspace.reset.confirm": 'Tilbakestille arbeidsområdet "{{name}}"?', + "workspace.reset.button": "Tilbakestill arbeidsområde", + "workspace.reset.archived.none": "Ingen aktive sesjoner vil bli arkivert.", + "workspace.reset.archived.one": "1 sesjon vil bli arkivert.", + "workspace.reset.archived.many": "{{count}} sesjoner vil bli arkivert.", + "workspace.reset.note": "Dette vil tilbakestille arbeidsområdet til å samsvare med standardgrenen.", + "common.open": "Åpne", + "dialog.releaseNotes.action.getStarted": "Kom i gang", + "dialog.releaseNotes.action.next": "Neste", + "dialog.releaseNotes.action.hideFuture": "Ikke vis disse igjen", + "dialog.releaseNotes.media.alt": "Forhåndsvisning av utgivelse", + "toast.project.reloadFailed.title": "Kunne ikke laste inn {{project}} på nytt", + "error.server.invalidConfiguration": "Ugyldig konfigurasjon", + "common.moreCountSuffix": " (+{{count}} mer)", + "common.time.justNow": "Akkurat nå", + "common.time.minutesAgo.short": "{{count}} m siden", + "common.time.hoursAgo.short": "{{count}} t siden", + "common.time.daysAgo.short": "{{count}} d siden", + "settings.providers.connected.environmentDescription": "Koblet til fra miljøvariablene dine", + "settings.providers.custom.description": "Legg til en OpenAI-kompatibel leverandør via en basis-URL.", + + "app.server.unreachable": "Kunne ikke nå {{server}}", + "app.server.retrying": "Prøver på nytt automatisk...", + "app.server.otherServers": "Andre servere", + "dialog.server.add.usernamePlaceholder": "brukernavn", + "dialog.server.add.passwordPlaceholder": "passord", + "server.row.noUsername": "ikke noe brukernavn", + "session.review.noVcs.createGit.title": "Opprett et Git-depot", + "session.review.noVcs.createGit.description": "Spor, gjennomgå og angre endringer i dette prosjektet", + "session.review.noVcs.createGit.actionLoading": "Oppretter Git-depot...", + "session.review.noVcs.createGit.action": "Opprett Git-depot", + "session.todo.progress": "{{done}} av {{total}} oppgaver fullført", + "session.question.progress": "{{current}} av {{total}} spørsmål", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Filutforsker", + "session.header.reveal.finder": "Vis i Finder", + "session.header.reveal.fileExplorer": "Vis i Filutforsker", + "session.header.reveal.containingFolder": "Åpne mappen som inneholder dette elementet", + "session.header.open.fileManager": "Filbehandler", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Utviklingsytelsesdiagnostikk", + "debugBar.na": "i/t", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Siste fullførte ruteovergang som berører en sesjonsside, målt fra ruterstart til første gjengivelse etter at den har roet seg.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Rullende bilder per sekund over de siste 5 sekundene.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Lengste bildetid over de siste 5 sekundene.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Bilder som tar over 32 ms i de siste 5 sekundene.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blokkert tid og antall lange oppgaver i de siste 5 sekundene. Lengste oppgave: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Lengste observerte inndataforsinkelse i de siste 5 sekundene.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Omtrentlig interaksjonsvarighet over de siste 5 sekundene. Dette er INP-lignende, ikke den offisielle Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativ layoutforskyvning for gjeldende app-levetid.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Brukt JS-heap kontra heap-grense. Kun Chromium.", + "debugBar.mem.tip": "Brukt JS-heap kontra heap-grense. {{used}} av {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Tving fokusstiler på alle interaktive elementer", + "debugBar.focus.on": "PÅ", + "debugBar.focus.off": "AV", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Tving layoutretningen for hele appen uten å endre valgt språk", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Mellomrom", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "ukjent", + "error.page.circular": "[Sirkulær]", + "error.serverSDK.noServerAvailable": "Ingen server tilgjengelig", + "error.serverSDK.serverNotAvailable": "Server ikke tilgjengelig", + "error.childStore.persistedCacheCreateFailed": "Kunne ikke opprette vedvarende hurtigbuffer", + "error.childStore.persistedProjectMetadataCreateFailed": "Kunne ikke opprette vedvarende prosjektmetadata", + "error.childStore.persistedProjectIconCreateFailed": "Kunne ikke opprette vedvarende prosjektikon", + "error.childStore.storeCreateFailed": "Kunne ikke opprette lager", + "terminal.connectionLost.abnormalClose": "WebSocket lukket unormalt: {{code}}", + + "command.project.previous": "Forrige prosjekt", + "command.project.next": "Neste prosjekt", + "command.project.index": "Bytt til prosjekt {{index}}", + "command.tab.reopenClosed": "Åpne lukket fane på nytt", + + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Inndata", + "model.tooltip.model": "Modell", + "model.tooltip.provider": "Leverandør", + "model.tooltip.reasoning": "Resonnering", + + "common.clear": "Tøm", + + "session.child.promptDisabled": "Underagentsesjoner kan ikke motta forespørsler.", + "session.child.backToParent": "Tilbake til hovedsesjonen.", + + "wsl.server.add": "Legg til WSL-server", + "wsl.server.addShort": "Legg til WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL-server", + "wsl.server.retryStart": "Prøv å starte på nytt", + "wsl.server.updating": "Oppdaterer...", + "wsl.onboarding.step.distro": "Velg distribusjon", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Sjekker WSL...", + "wsl.onboarding.restartRequired": "Windows må startes på nytt for å fullføre installasjonen av WSL.", + "wsl.onboarding.ready": "WSL er klart.", + "wsl.onboarding.required": "WSL kreves for å fortsette.", + "wsl.onboarding.checkingDistros": "Sjekker distribusjoner...", + "wsl.onboarding.installingDistro": "Installerer {{distro}}...", + "wsl.onboarding.checkingDistro": "Sjekker {{distro}}...", + "wsl.onboarding.listingDistros": "Lister distribusjoner...", + "wsl.onboarding.distroReady": "{{distro}} er klar.", + "wsl.onboarding.distroNotInstalled": "{{distro}} er ikke installert ennå.", + "wsl.onboarding.openDistroOnce": "Åpne {{distro}} én gang for å fullføre oppsettet.", + "wsl.onboarding.finishingDistro": "Fullfører oppsettet for {{distro}}.", + "wsl.onboarding.pickDistro": "Velg en distribusjon eller installer en nedenfor.", + "wsl.onboarding.checkingOpencode": "Sjekker OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Sjekker OpenCode i {{distro}}...", + "wsl.onboarding.updatingOpencode": "Oppdaterer OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Oppdaterer OpenCode i {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Oppdater OpenCode i {{distro}}.", + "wsl.onboarding.updateOpencode": "Oppdater OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode er klar i {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode er klar.", + "wsl.onboarding.installOpencodeIn": "Installer OpenCode i {{distro}}.", + "wsl.onboarding.installOpencode": "Installer OpenCode", + "wsl.onboarding.chooseDistroFirst": "Velg en distribusjon først.", + "wsl.onboarding.loadFailed": "Kunne ikke laste inn WSL-status.", + "wsl.onboarding.loading": "Laster...", + "wsl.onboarding.installedDistros": "Installerte distribusjoner", + "wsl.onboarding.checkAgain": "Sjekk på nytt", + "wsl.onboarding.distroStatus.ready": "Klar", + "wsl.onboarding.distroStatus.checking": "Sjekker...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode er ikke installert", + "wsl.onboarding.distroStatus.missingTools": "Mangler bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Ikke støttet · Bruk WSL 2", + "wsl.onboarding.needAnotherDistro": "Trenger du en annen distribusjon?", + "wsl.onboarding.needAnotherDistroHint": "Installer en Linux-distribusjon fra WSL-katalogen", + "wsl.onboarding.wslNotInstalled.title": "WSL er ikke installert", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) kreves før OpenCode kan legge til en WSL-server", + "wsl.onboarding.wslUnavailable.title": "WSL er utilgjengelig", + "wsl.onboarding.wslUnavailable.description": "OpenCode kunne ikke bekrefte WSL på denne maskinen.", + "wsl.onboarding.installWsl": "Installer WSL", + "wsl.onboarding.windowsRestartRequired": + "Start Windows på nytt for å fullføre installasjonen av WSL, og åpne deretter OpenCode på nytt.", + "wsl.onboarding.next": "Neste", + "wsl.onboarding.refresh": "Oppdater", + "wsl.onboarding.allDistrosAdded": "Alle installerte distribusjoner er allerede lagt til.", + "wsl.onboarding.noDistros": "Ingen distribusjoner oppdaget ennå.", + "wsl.onboarding.install": "Installer", + "wsl.onboarding.installing": "Installerer...", + "wsl.onboarding.installDistro": "Installer distribusjon", + "wsl.onboarding.searchDistros": "Søk etter distribusjoner", + "wsl.onboarding.wsl2Required": "WSL 2 er påkrevd.", + "wsl.onboarding.toolsRequired": "Denne distribusjonen trenger bash og curl.", + "wsl.onboarding.openTerminal": "Åpne terminal", + "wsl.onboarding.path": "Bane: {{path}}", + "wsl.onboarding.notFound": "ikke funnet", + "wsl.onboarding.version": "Versjon: {{version}}", + "wsl.onboarding.unknown": "ukjent", + "wsl.onboarding.desktopVersion": "skrivebordsapp {{version}}", + "wsl.onboarding.versionMismatch": "Den installerte versjonen samsvarer ikke med versjonen av skrivebordsappen.", + "wsl.onboarding.adding": "Legger til...", + + "help.tabs.toast.ariaLabel": "Vi introduserer faner. Organiser arbeidet og de aktive sesjonene dine med faner", + "help.tabs.toast.dismiss": "Avvis informasjon om faner", + "help.tabs.title": "Vi introduserer faner", + "help.tabs.description": "Organiser arbeidet og de aktive sesjonene dine med faner", + "help.tabs.date": "14. juli", + "help.tabs.introduction": "OpenCode Desktop er nå bygget rundt faner.", + "help.tabs.sessions": + "Start en ny sesjon i en fane, eller åpne en eksisterende sesjon fra et av prosjektene dine. Åpne en ny fane når du skal begynne på noe nytt, og lukk den når du er ferdig.", + "help.tabs.organize": + "Når du har noen få faner åpne, blir det enklere å organisere de aktive sesjonene. Gi fanene navn som er lette å huske, hvis du planlegger å beholde dem.", + "help.tabs.home": + "Du finner alle sesjonene og prosjektene dine på den nye Hjem-skjermen. Når du velger en sesjon, åpnes den i en fane.", + "help.tabs.persistence": "Fanene er fortsatt åpne når du åpner appen på nytt.", + "help.tabs.worktrees": + "Det nye oppsettet støtter ikke Git Worktrees ennå, men støtte kommer snart. Hvis du foretrekker å fortsette med det forrige oppsettet, kan du bytte mellom oppsettene i Innstillinger. Vær oppmerksom på at det nye oppsettet blir permanent om noen uker.", + + "language.uk": "Українська", + + "error.page.description.localServerStartup": "Det oppstod en feil under oppstart av den lokale serveren.", + "error.page.action.exportLogs": "Eksporter logger", + + "home.title": "Hjem", + "home.projects": "Prosjekter", + "home.project.add": "Legg til prosjekt", + "home.recentlyClosed": "Nylig lukket", + "home.server.collapse": "Fold sammen serverprosjekter", + "home.server.expand": "Utvid serverprosjekter", + "home.sessions.search.placeholder": "Søk etter sesjoner", + "home.sessions.search.placeholder.scoped": "Søk etter sesjoner i {{scope}}", + "home.sessions.search.sessions": "Sesjoner", + "home.sessions.search.noResults": "Ingen sesjoner funnet for {{query}}", + "home.sessions.empty": "Ingenting her ennå", + "home.sessions.empty.description": "Opprett en sesjon for å komme i gang", + "home.sessions.group.today": "I dag", + "home.sessions.group.yesterday": "I går", + "home.sessions.group.older": "Eldre", + "home.providerTip": + "Koble til over 75 leverandører for å bruke andre modeller, inkludert Claude, GPT, Gemini og flere", + + "session.tab.unknown": "Ukjent sesjon", + "session.error.notFound": "Denne sesjonen finnes ikke", + "session.error.notFound.description": "Denne fanen peker til en sesjon som ikke lenger finnes på denne serveren.", + "session.error.notFound.closeTab": "Lukk fane", + "session.error.serverConnection": "Kan ikke koble til denne serveren", + "session.review.noUncommittedChanges": "Ingen endringer som ikke er sjekket inn ennå", + "session.review.noBranchChanges": "Ingen grenendringer ennå", + "session.question.minimize": "Minimer spørsmål", + "session.question.restore": "Gjenopprett spørsmål", + "session.question.pending.one": "{{count}} ventende spørsmål", + "session.question.pending.other": "{{count}} ventende spørsmål", + "session.new.project.new": "Nytt prosjekt", + "session.new.project.search": "Søk etter prosjekter", + "session.new.project.add": "Legg til prosjekt", + "session.new.workspace.runIn": "Kjør sesjonen i", + "session.new.workspace.triggerLocal": "Lokalt", + "session.new.workspace.local": "Lokalt depot", + "session.new.workspace.existing": "Arbeidsområde…", + "session.new.git.none": "Ingen Git", + + "sidebar.empty.title": "Ingen åpne prosjekter", + "sidebar.empty.description": "Åpne et prosjekt for å komme i gang", + + "settings.general.section.advanced": "Avansert", + "settings.general.row.shell.title": "Terminalskall", + "settings.general.row.shell.description": + "Velg skallet som skal brukes i terminalen. Kompatible skall brukes også til agentens verktøykall.", + "settings.general.row.shell.autoDefault": "Automatisk (standard)", + "settings.general.row.shell.terminalOnly": "kun terminal", + "settings.general.row.showFileTree.title": "Filtre", + "settings.general.row.showFileTree.description": "Vis filtrepanelet i sesjoner", + "settings.general.row.showNavigation.title": "Navigasjonskontroller", + "settings.general.row.showNavigation.description": + "Vis tilbake- og fremoverknappene på tittellinjen i skrivebordsappen", + "settings.general.row.showSearch.title": "Kommandopalett", + "settings.general.row.showSearch.description": "Vis knappen for søk og kommandopalett på tittellinjen", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Vis terminalknappen på tittellinjen i skrivebordsappen", + "settings.general.row.showStatus.title": "Serverstatus", + "settings.general.row.showStatus.description": "Vis serverstatusknappen på tittellinjen", + "settings.general.row.mobileTitlebarBottom.title": "Navigasjon nederst", + "settings.general.row.mobileTitlebarBottom.description": + "Plasser tittellinjen og sesjonsfanene nederst på mobilskjermen", + "settings.general.row.showCustomAgents.title": "Vis agent", + "settings.general.row.showCustomAgents.description": + "Bytt mellom agenter i skrivefeltet. Når velgeren er skjult, brukes Build-agenten som standard.", + "settings.general.row.newInterface.title": "Nytt oppsett", + "settings.general.row.newInterface.badge": "Ny", + "settings.general.row.newInterface.description": + "Bruk de nye fanene og startsidens oppsett. I en begrenset periode kan du bytte mellom oppsettene.", + "settings.general.row.newInterfaceNotice.title": "Du bruker nå det nye oppsettet", + "settings.general.row.newInterfaceNotice.description": "Det forrige oppsettet er ikke lenger tilgjengelig", + "settings.general.row.newInterfaceNotice.dismiss": "Avvis", + "settings.general.row.pinchZoom.title": "Knip for å zoome", + "settings.general.row.pinchZoom.description": "Tillat knipebevegelser på styreflaten og Ctrl-rulling for å zoome", + "settings.updates.action.downloading": "Laster ned...", + "settings.updates.action.installing": "Installerer...", +} satisfies Partial> diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b3c927498d8f8da1cf6b828b9a1c22d689b843b --- /dev/null +++ b/packages/app/src/i18n/pl.ts @@ -0,0 +1,1184 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Plik", + "desktop.menu.edit": "Edycja", + "desktop.menu.view": "Widok", + "desktop.menu.go": "Przejdź", + "desktop.menu.window": "Okno", + "desktop.menu.help": "Pomoc", + "desktop.menu.checkForUpdates": "Sprawdź aktualizacje...", + "desktop.menu.settings": "Ustawienia", + "desktop.menu.reloadWebview": "Wczytaj ponownie widok internetowy", + "desktop.menu.restart": "Uruchom ponownie", + "desktop.menu.exportLogs": "Eksportuj dzienniki...", + "desktop.menu.newSession": "Nowa sesja", + "desktop.menu.openProject": "Otwórz projekt...", + "desktop.menu.newWindow": "Nowe okno", + "desktop.menu.closeWindow": "Zamknij okno", + "desktop.menu.undo": "Cofnij", + "desktop.menu.redo": "Ponów", + "desktop.menu.cut": "Wytnij", + "desktop.menu.copy": "Kopiuj", + "desktop.menu.paste": "Wklej", + "desktop.menu.delete": "Usuń", + "desktop.menu.selectAll": "Zaznacz wszystko", + "desktop.menu.toggleSidebar": "Przełącz pasek boczny", + "desktop.menu.toggleTerminal": "Przełącz terminal", + "desktop.menu.toggleFileTree": "Przełącz drzewo plików", + "desktop.menu.reload": "Wczytaj ponownie", + "desktop.menu.toggleDeveloperTools": "Przełącz narzędzia deweloperskie", + "desktop.menu.actualSize": "Rozmiar rzeczywisty", + "desktop.menu.zoomIn": "Powiększ", + "desktop.menu.zoomOut": "Pomniejsz", + "desktop.menu.toggleFullScreen": "Przełącz tryb pełnoekranowy", + "desktop.menu.back": "Wstecz", + "desktop.menu.forward": "Dalej", + "desktop.menu.previousSession": "Poprzednia sesja", + "desktop.menu.nextSession": "Następna sesja", + "desktop.menu.previousProject": "Poprzedni projekt", + "desktop.menu.nextProject": "Następny projekt", + "desktop.menu.minimize": "Minimalizuj", + "desktop.menu.maximize": "Maksymalizuj", + "desktop.menu.documentation": "Dokumentacja OpenCode", + "desktop.menu.supportForum": "Forum pomocy technicznej", + "desktop.menu.shareFeedback": "Prześlij opinię", + "desktop.menu.reportBug": "Zgłoś błąd", + "desktop.menu.ariaLabel": "Menu OpenCode", + + "desktop.updater.dialog.checkFailed.message": "Nie udało się sprawdzić dostępności aktualizacji.", + "desktop.updater.dialog.checkFailed.title": "Błąd aktualizacji", + "desktop.updater.dialog.upToDate.message": "Masz najnowszą wersję.", + "desktop.updater.dialog.upToDate.title": "Brak aktualizacji", + "desktop.updater.dialog.ready.message": "Pobrano aktualizację {{version}}. Uruchomić ponownie teraz?", + "desktop.updater.dialog.ready.title": "Aktualizacja jest gotowa", + "desktop.updater.dialog.restart": "Uruchom ponownie", + "desktop.updater.dialog.later": "Później", + + "desktop.recovery.action.relaunch": "Uruchom ponownie", + "desktop.recovery.action.exportLogs": "Eksportuj dzienniki", + "desktop.recovery.action.keepWaiting": "Czekaj dalej", + "desktop.recovery.action.quit": "Zakończ", + "desktop.recovery.loadFailed": "Nie udało się wczytać OpenCode", + "desktop.recovery.terminated": "Okno OpenCode zostało nieoczekiwanie zamknięte", + "desktop.recovery.unresponsive": "OpenCode nie odpowiada", + "desktop.recovery.unresponsive.detail": "Możesz ponownie uruchomić aplikację, otworzyć dzienniki lub nadal czekać.", + "desktop.recovery.loadFailed.detail": "Okno: {{window}}\nURL: {{url}}\nBłąd: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Okno: {{window}}\nPrzyczyna: {{reason}}\nKod: {{code}}", + "desktop.recovery.unknown": "", + + "desktop.dialog.chooseFolder": "Wybierz folder", + "desktop.dialog.chooseFile": "Wybierz plik", + "desktop.dialog.saveFile": "Zapisz plik", + "desktop.dialog.files": "Pliki", + "desktop.server.local": "Serwer lokalny", + + "desktop.wsl.error.windowsOnly": "WSL jest dostępny tylko w systemie Windows", + "desktop.wsl.error.unavailable": "WSL jest niedostępny", + "desktop.wsl.error.listInstalled": "Nie udało się wyświetlić listy zainstalowanych dystrybucji WSL", + "desktop.wsl.error.listOnline": "Nie udało się wyświetlić listy dystrybucji WSL dostępnych online", + "desktop.wsl.error.executeDistro": "Nie można wykonywać poleceń w dystrybucji", + "desktop.wsl.error.installWsl": "Nie udało się zainstalować WSL", + "desktop.wsl.error.installDistro": "Nie udało się zainstalować dystrybucji: {{distro}}", + "desktop.wsl.error.installOpencode": "Nie udało się zainstalować OpenCode", + "desktop.wsl.error.alreadyAdded": "Dystrybucja {{distro}} została już dodana", + "desktop.wsl.error.opencodeMissing": "Program opencode nie jest zainstalowany w tej dystrybucji", + "desktop.wsl.error.opencodeCannotRun": "Program opencode jest zainstalowany, ale nie można go uruchomić", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode nie jest zainstalowany w dystrybucji {{distro}}", + "desktop.wsl.error.updateVersion": + "Aktualizacja OpenCode została ukończona, ale dystrybucja {{distro}} nadal zgłasza wersję {{installed}}; oczekiwano {{expected}}", + "desktop.wsl.error.noVersion": "brak wersji", + "desktop.wsl.error.serverExited": "Serwer WSL zakończył działanie po uruchomieniu (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Serwer WSL zakończył działanie przed osiągnięciem gotowości (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Przekroczono limit czasu {{timeout}}ms podczas sprawdzania kondycji procesu sidecar dla dystrybucji {{distro}}", + "desktop.wsl.error.commandTimeout": "Polecenie {{command}} {{args}} przekroczyło limit czasu {{timeout}}ms", + "desktop.wsl.error.failedPort": "Nie udało się uzyskać portu", + + "desktop.picker.error.notSelected": "Nie wybrano pliku w oknie wyboru", + "desktop.picker.error.sizeLimit": "Wybrane załączniki przekraczają limit {{limit}} MB", + + "command.category.suggested": "Sugerowane", + "command.category.view": "Widok", + "command.category.project": "Projekt", + "command.category.provider": "Dostawca", + "command.category.server": "Serwer", + "command.category.session": "Sesja", + "command.category.theme": "Motyw", + "command.category.language": "Język", + "command.category.file": "Plik", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Uprawnienia", + "command.category.workspace": "Przestrzeń robocza", + "command.category.settings": "Ustawienia", + "command.logs.export": "Eksportuj dzienniki", + "theme.scheme.system": "Systemowy", + "theme.scheme.light": "Jasny", + "theme.scheme.dark": "Ciemny", + "command.sidebar.toggle": "Przełącz pasek boczny", + "command.project.open": "Otwórz projekt", + "command.project.previous": "Poprzedni projekt", + "command.project.next": "Następny projekt", + "command.project.index": "Przełącz na projekt {{index}}", + "command.provider.connect": "Połącz dostawcę", + "command.server.switch": "Przełącz serwer", + "command.settings.open": "Otwórz ustawienia", + "command.session.previous": "Poprzednia sesja", + "command.session.next": "Następna sesja", + "command.session.previous.unseen": "Poprzednia nieprzeczytana sesja", + "command.session.next.unseen": "Następna nieprzeczytana sesja", + "command.session.archive": "Zarchiwizuj sesję", + "command.palette": "Paleta poleceń", + "command.theme.cycle": "Przełącz motyw", + "command.theme.set": "Użyj motywu: {{theme}}", + "command.theme.scheme.cycle": "Przełącz schemat kolorów", + "command.theme.scheme.set": "Użyj schematu kolorów: {{scheme}}", + "command.language.cycle": "Przełącz język", + "command.language.set": "Użyj języka: {{language}}", + "command.session.new": "Nowa sesja", + "command.file.open": "Otwórz plik", + "command.tab.close": "Zamknij kartę", + "command.tab.reopenClosed": "Otwórz ponownie zamkniętą kartę", + "command.context.addSelection": "Dodaj zaznaczenie do kontekstu", + "command.context.addSelection.description": "Dodaj zaznaczone linie z bieżącego pliku", + "command.input.focus": "Przejdź do pola wprowadzania", + "command.terminal.toggle": "Przełącz terminal", + "command.fileTree.toggle": "Przełącz drzewo plików", + "command.review.toggle": "Przełącz przegląd", + "command.terminal.new": "Nowy terminal", + "command.terminal.new.description": "Utwórz nową kartę terminala", + "command.steps.toggle": "Przełącz kroki", + "command.steps.toggle.description": "Pokaż lub ukryj kroki dla bieżącej wiadomości", + "command.message.previous": "Poprzednia wiadomość", + "command.message.previous.description": "Przejdź do poprzedniej wiadomości użytkownika", + "command.message.next": "Następna wiadomość", + "command.message.next.description": "Przejdź do następnej wiadomości użytkownika", + "command.model.choose": "Wybierz model", + "command.model.choose.description": "Wybierz inny model", + "command.mcp.toggle": "Przełącz MCP", + "command.mcp.toggle.description": "Przełącz MCP", + "command.agent.cycle": "Przełącz agenta", + "command.agent.cycle.description": "Przełącz na następnego agenta", + "command.agent.cycle.reverse": "Przełącz agenta wstecz", + "command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta", + "command.model.variant.cycle": "Przełącz wysiłek myślowy", + "command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku", + "command.prompt.mode.shell": "Terminal", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Automatycznie akceptuj uprawnienia", + "command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie uprawnień", + "command.workspace.toggle": "Przełącz przestrzenie robocze", + "command.workspace.toggle.description": "Włącz lub wyłącz wiele przestrzeni roboczych na pasku bocznym", + "command.session.undo": "Cofnij", + "command.session.undo.description": "Cofnij ostatnią wiadomość", + "command.session.redo": "Ponów", + "command.session.redo.description": "Ponów ostatnią cofniętą wiadomość", + "command.session.compact": "Kompaktuj sesję", + "command.session.compact.description": "Podsumuj sesję, aby zmniejszyć rozmiar kontekstu", + "command.session.fork": "Rozwidlij od wiadomości", + "command.session.fork.description": "Utwórz nową sesję od poprzedniej wiadomości", + "command.session.share": "Udostępnij sesję", + "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", + "palette.group.commands": "Polecenia", + "palette.group.files": "Pliki", + "dialog.provider.search.placeholder": "Szukaj dostawców", + "dialog.provider.empty": "Nie znaleziono dostawców", + "dialog.provider.group.popular": "Popularne", + "dialog.provider.group.other": "Inne", + "dialog.provider.custom.label": "Niestandardowy dostawca zgodny z OpenAI", + "dialog.provider.tag.recommended": "Zalecane", + "dialog.provider.opencode.note": "Starannie dobrane modele, w tym Claude, GPT, Gemini i inne", + "dialog.provider.opencode.tagline": "Niezawodne, zoptymalizowane modele", + "dialog.provider.opencodeGo.tagline": "Niedroga subskrypcja dla każdego", + "dialog.provider.anthropic.note": "Bezpośredni dostęp do modeli Claude, w tym Pro i Max", + "dialog.provider.copilot.note": "Modele AI do pomocy w kodowaniu przez GitHub Copilot", + "dialog.provider.openai.note": "Modele GPT do szybkich i wszechstronnych zadań AI", + "dialog.provider.google.note": "Modele Gemini do szybkich i ustrukturyzowanych odpowiedzi", + "dialog.provider.openrouter.note": "Dostęp do wszystkich obsługiwanych modeli od jednego dostawcy", + "dialog.provider.vercel.note": "Ujednolicony dostęp do modeli AI z inteligentnym routingiem", + "dialog.model.select.title": "Wybierz model", + "dialog.model.search.placeholder": "Szukaj modeli", + "dialog.model.empty": "Nie znaleziono modeli", + "dialog.model.manage": "Zarządzaj modelami", + "dialog.model.manage.description": "Dostosuj, które modele pojawiają się w wyborze modelu.", + "dialog.model.manage.provider.toggle": "Przełącz wszystkie modele {{provider}}", + "dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode", + "dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców", + "dialog.model.unpaid.viewMoreProviders": "Zobacz ponad 70 innych dostawców", + "dialog.provider.viewAll": "Zobacz więcej dostawców", + "provider.connect.title": "Połącz {{provider}}", + "provider.connect.title.anthropicProMax": "Zaloguj się za pomocą konta Claude Pro/Max", + "provider.connect.selectMethod": "Wybierz metodę logowania dla {{provider}}.", + "provider.connect.method.apiKey": "Klucz API", + "provider.connect.method.browser": "Przeglądarka", + "provider.connect.method.headless": "Bez przeglądarki", + "provider.connect.status.inProgress": "Autoryzacja w toku...", + "provider.connect.status.waiting": "Oczekiwanie na autoryzację...", + "provider.connect.status.failed": "Autoryzacja nie powiodła się: {{error}}", + "provider.connect.apiKey.description": + "Wprowadź swój klucz API {{provider}}, aby połączyć konto i używać modeli {{provider}} w OpenCode.", + "provider.connect.apiKey.label": "Klucz API {{provider}}", + "provider.connect.apiKey.placeholder": "Klucz API", + "provider.connect.apiKey.required": "Klucz API jest wymagany", + "provider.connect.opencodeZen.line1": + "OpenCode Zen daje dostęp do wybranego zestawu niezawodnych, zoptymalizowanych modeli dla agentów programistycznych.", + "provider.connect.opencodeZen.line2": + "Za pomocą jednego klucza API uzyskasz dostęp do modeli takich jak Claude, GPT, Gemini i GLM, a także wielu innych.", + "provider.connect.opencodeZen.visit.prefix": "Odwiedź ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": ", aby odebrać swój klucz API.", + "provider.connect.oauth.code.visit.prefix": "Odwiedź ", + "provider.connect.oauth.code.visit.link": "ten link", + "provider.connect.oauth.code.visit.suffix": + ", aby odebrać kod autoryzacyjny, połączyć konto i używać modeli {{provider}} w OpenCode.", + "provider.connect.oauth.code.label": "Kod autoryzacyjny {{method}}", + "provider.connect.oauth.code.placeholder": "Kod autoryzacyjny", + "provider.connect.oauth.code.required": "Kod autoryzacyjny jest wymagany", + "provider.connect.oauth.code.invalid": "Nieprawidłowy kod autoryzacyjny", + "provider.connect.oauth.auto.visit.prefix": "Odwiedź ", + "provider.connect.oauth.auto.visit.link": "ten link", + "provider.connect.oauth.auto.visit.suffix": + " i wprowadź poniższy kod, aby połączyć konto i używać modeli {{provider}} w OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Kod potwierdzający", + "provider.connect.toast.connected.title": "Połączono {{provider}}", + "provider.connect.toast.connected.description": "Modele {{provider}} są teraz dostępne do użycia.", + "provider.custom.title": "Dostawca niestandardowy", + "provider.custom.unavailable": "Niestandardowi dostawcy są niedostępni na tym serwerze", + "provider.custom.description.prefix": "Skonfiguruj dostawcę zgodnego z OpenAI. Zobacz ", + "provider.custom.description.link": "dokumentację konfiguracji dostawcy", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID dostawcy", + "provider.custom.field.providerID.placeholder": "mojdostawca", + "provider.custom.field.providerID.description": "Małe litery, cyfry, łączniki lub podkreślenia", + "provider.custom.field.name.label": "Nazwa wyświetlana", + "provider.custom.field.name.placeholder": "Mój Dostawca AI", + "provider.custom.field.baseURL.label": "Bazowy adres URL", + "provider.custom.field.baseURL.placeholder": "https://api.mojdostawca.com/v1", + "provider.custom.field.apiKey.label": "Klucz API", + "provider.custom.field.apiKey.placeholder": "Klucz API", + "provider.custom.field.apiKey.description": + "Opcjonalne. Pozostaw puste, jeśli zarządzasz autoryzacją przez nagłówki.", + "provider.custom.models.label": "Modele", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Nazwa", + "provider.custom.models.name.placeholder": "Nazwa wyświetlana", + "provider.custom.models.remove": "Usuń model", + "provider.custom.models.add": "Dodaj model", + "provider.custom.headers.label": "Nagłówki (opcjonalne)", + "provider.custom.headers.key.label": "Nagłówek", + "provider.custom.headers.key.placeholder": "Nazwa-Naglowka", + "provider.custom.headers.value.label": "Wartość", + "provider.custom.headers.value.placeholder": "wartość", + "provider.custom.headers.remove": "Usuń nagłówek", + "provider.custom.headers.add": "Dodaj nagłówek", + "provider.custom.error.providerID.required": "ID dostawcy jest wymagane", + "provider.custom.error.providerID.format": "Używaj małych liter, cyfr, łączników lub podkreśleń", + "provider.custom.error.providerID.exists": "To ID dostawcy już istnieje", + "provider.custom.error.name.required": "Nazwa wyświetlana jest wymagana", + "provider.custom.error.baseURL.required": "Bazowy adres URL jest wymagany", + "provider.custom.error.baseURL.format": "Musi zaczynać się od http:// lub https://", + "provider.custom.error.required": "Wymagane", + "provider.custom.error.duplicate": "Duplikat", + "provider.disconnect.toast.disconnected.title": "Rozłączono {{provider}}", + "provider.disconnect.toast.disconnected.description": "Modele {{provider}} nie są już dostępne.", + "model.tag.free": "Darmowy", + "model.tag.latest": "Najnowszy", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekst", + "model.input.image": "obraz", + "model.input.audio": "audio", + "model.input.video": "wideo", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Obsługuje: {{inputs}}", + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Dane wejściowe", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Dostawca", + "model.tooltip.reasoning": "Wnioskowanie", + "model.tooltip.reasoning.allowed": "Obsługuje wnioskowanie", + "model.tooltip.reasoning.none": "Brak wnioskowania", + "model.tooltip.context": "Limit kontekstu {{limit}}", + "common.search.placeholder": "Szukaj", + "common.goBack": "Wstecz", + "common.goForward": "Dalej", + "common.loading": "Ładowanie", + "common.loading.ellipsis": "...", + "common.cancel": "Anuluj", + "common.clear": "Wyczyść", + "common.connect": "Połącz", + "common.disconnect": "Rozłącz", + "common.continue": "Kontynuuj", + "common.submit": "Prześlij", + "common.save": "Zapisz", + "common.saving": "Zapisywanie...", + "common.default": "Domyślny", + "common.attachment": "załącznik", + "prompt.placeholder.shell": "Wpisz polecenie terminala... {{example}}", + "prompt.placeholder.normal": 'Zapytaj o cokolwiek... "{{example}}"', + "prompt.placeholder.simple": "Zapytaj o cokolwiek...", + "prompt.placeholder.summarizeComments": "Podsumuj komentarze…", + "prompt.placeholder.summarizeComment": "Podsumuj komentarz…", + "prompt.mode.shell": "Terminal", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "Naciśnij Esc, aby wyjść", + "session.child.promptDisabled": "Nie można wysyłać promptów w sesjach podagentów.", + "session.child.backToParent": "Wróć do głównej sesji.", + "prompt.example.1": "Napraw TODO w bazie kodu", + "prompt.example.2": "Jaki jest stos technologiczny tego projektu?", + "prompt.example.3": "Napraw zepsute testy", + "prompt.example.4": "Wyjaśnij, jak działa uwierzytelnianie", + "prompt.example.5": "Znajdź i napraw luki w zabezpieczeniach", + "prompt.example.6": "Dodaj testy jednostkowe dla serwisu użytkownika", + "prompt.example.7": "Zrefaktoryzuj tę funkcję, aby była bardziej czytelna", + "prompt.example.8": "Co oznacza ten błąd?", + "prompt.example.9": "Pomóż mi zdiagnozować ten problem", + "prompt.example.10": "Wygeneruj dokumentację API", + "prompt.example.11": "Zoptymalizuj zapytania do bazy danych", + "prompt.example.12": "Dodaj walidację danych wejściowych", + "prompt.example.13": "Utwórz nowy komponent dla...", + "prompt.example.14": "Jak wdrożyć ten projekt?", + "prompt.example.15": "Sprawdź mój kod pod kątem najlepszych praktyk", + "prompt.example.16": "Dodaj obsługę błędów do tej funkcji", + "prompt.example.17": "Wyjaśnij ten wzorzec regex", + "prompt.example.18": "Przekonwertuj to na TypeScript", + "prompt.example.19": "Dodaj logowanie w całej bazie kodu", + "prompt.example.20": "Które zależności są przestarzałe?", + "prompt.example.21": "Pomóż mi napisać skrypt migracyjny", + "prompt.example.22": "Zaimplementuj buforowanie dla tego punktu końcowego", + "prompt.example.23": "Dodaj stronicowanie do tej listy", + "prompt.example.24": "Utwórz polecenie CLI dla...", + "prompt.example.25": "Jak działają tutaj zmienne środowiskowe?", + "prompt.popover.emptyResults": "Brak pasujących wyników", + "prompt.popover.emptyCommands": "Brak pasujących poleceń", + "prompt.dropzone.label": "Upuść tutaj obrazy, pliki PDF lub pliki tekstowe", + "prompt.dropzone.file.label": "Upuść, aby oznaczyć plik za pomocą @", + "prompt.slash.badge.custom": "własne", + "prompt.slash.badge.skill": "umiejętność", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktywny", + "prompt.context.includeActiveFile": "Dołącz aktywny plik", + "prompt.context.removeActiveFile": "Usuń aktywny plik z kontekstu", + "prompt.context.removeFile": "Usuń plik z kontekstu", + "prompt.action.attachFile": "Dodaj pliki", + "prompt.menu.addImagesAndFiles": "Dodaj pliki i inne elementy", + "prompt.menu.imagesAndFiles": "Obrazy i pliki", + "prompt.menu.commands": "Polecenia", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Polecenie powłoki", + "prompt.attachment.remove": "Usuń załącznik", + "prompt.action.send": "Wyślij", + "prompt.action.stop": "Zatrzymaj", + "prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik", + "prompt.toast.attachmentDuplicate.title": "Ten plik został już przesłany", + "prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.", + "prompt.toast.modelAgentRequired.title": "Wybierz agenta i model", + "prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.", + "prompt.toast.worktreeCreateFailed.title": "Nie udało się utworzyć drzewa roboczego", + "prompt.toast.sessionCreateFailed.title": "Nie udało się utworzyć sesji", + "prompt.toast.shellSendFailed.title": "Nie udało się wysłać polecenia powłoki", + "prompt.toast.commandSendFailed.title": "Nie udało się wysłać polecenia", + "prompt.toast.promptSendFailed.title": "Nie udało się wysłać zapytania", + "prompt.toast.promptSendFailed.description": "Nie udało się pobrać sesji", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "Włączone: {{enabled}} z {{total}}", + "dialog.mcp.empty": "Brak skonfigurowanych MCP", + "dialog.lsp.empty": "Serwery LSP są wykrywane automatycznie na podstawie typów plików", + "dialog.plugins.empty": "Wtyczki są skonfigurowane w pliku opencode.json", + "mcp.status.connected": "połączono", + "mcp.status.failed": "błąd", + "mcp.status.needs_auth": "wymaga autoryzacji", + "mcp.auth.clickToAuthenticate": "Kliknij, aby się uwierzytelnić", + "mcp.status.disabled": "wyłączone", + "dialog.fork.empty": "Brak wiadomości do rozwidlenia", + "dialog.directory.search.placeholder": "Szukaj folderów", + "dialog.directory.empty": "Nie znaleziono folderów", + "dialog.directory.action.selectFile": "Wybierz plik", + "dialog.directory.action.selectFolder": "Wybierz folder", + "dialog.directory.root": "Katalog główny", + "dialog.directory.parent": "Folder nadrzędny", + "dialog.directory.readError": "Nie można odczytać tego folderu", + "dialog.server.title": "Serwery", + "dialog.server.description": "Przełącz serwer OpenCode, z którym łączy się ta aplikacja.", + "dialog.server.search.placeholder": "Szukaj serwerów", + "dialog.server.empty": "Brak serwerów", + "dialog.server.add.title": "Dodaj serwer", + "dialog.server.add.url": "Adres URL serwera", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Nie można połączyć się z serwerem", + "dialog.server.add.checking": "Sprawdzanie...", + "dialog.server.add.button": "Dodaj serwer", + "dialog.server.add.name": "Nazwa serwera (opcjonalnie)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Nazwa użytkownika (opcjonalnie)", + "dialog.server.add.password": "Hasło (opcjonalnie)", + "dialog.server.edit.title": "Edytuj serwer", + "dialog.server.default.title": "Domyślny serwer", + "dialog.server.default.description": + "Łącz się z tym serwerem przy uruchamianiu aplikacji zamiast uruchamiać serwer lokalny. Wymaga ponownego uruchomienia.", + "dialog.server.default.none": "Nie wybrano serwera", + "dialog.server.default.set": "Ustaw bieżący serwer jako domyślny", + "dialog.server.default.clear": "Wyczyść", + "dialog.server.action.remove": "Usuń serwer", + "dialog.server.menu.edit": "Edytuj", + "dialog.server.menu.default": "Ustaw jako domyślny", + "dialog.server.menu.defaultRemove": "Usuń domyślny", + "dialog.server.menu.delete": "Usuń", + "dialog.server.current": "Bieżący serwer", + "dialog.server.status.default": "Domyślny", + "wsl.server.add": "Dodaj serwer WSL", + "wsl.server.addShort": "Dodaj WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "Serwer WSL", + "wsl.server.retryStart": "Ponów uruchamianie", + "wsl.server.updating": "Aktualizowanie...", + "wsl.onboarding.step.distro": "Wybierz dystrybucję", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Sprawdzanie WSL...", + "wsl.onboarding.restartRequired": "Aby dokończyć instalację WSL, uruchom ponownie system Windows.", + "wsl.onboarding.ready": "WSL jest gotowy.", + "wsl.onboarding.required": "Aby kontynuować, wymagany jest WSL.", + "wsl.onboarding.checkingDistros": "Sprawdzanie dystrybucji...", + "wsl.onboarding.installingDistro": "Instalowanie {{distro}}...", + "wsl.onboarding.checkingDistro": "Sprawdzanie {{distro}}...", + "wsl.onboarding.listingDistros": "Pobieranie listy dystrybucji...", + "wsl.onboarding.distroReady": "Dystrybucja {{distro}} jest gotowa.", + "wsl.onboarding.distroNotInstalled": "Dystrybucja {{distro}} nie jest jeszcze zainstalowana.", + "wsl.onboarding.openDistroOnce": "Uruchom raz {{distro}}, aby dokończyć konfigurację.", + "wsl.onboarding.finishingDistro": "Kończenie konfiguracji {{distro}}.", + "wsl.onboarding.pickDistro": "Wybierz dystrybucję lub zainstaluj jedną z poniższych.", + "wsl.onboarding.checkingOpencode": "Sprawdzanie OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Sprawdzanie OpenCode w dystrybucji {{distro}}...", + "wsl.onboarding.updatingOpencode": "Aktualizowanie OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Aktualizowanie OpenCode w dystrybucji {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Zaktualizuj OpenCode w dystrybucji {{distro}}.", + "wsl.onboarding.updateOpencode": "Zaktualizuj OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode jest gotowy w dystrybucji {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode jest gotowy.", + "wsl.onboarding.installOpencodeIn": "Zainstaluj OpenCode w dystrybucji {{distro}}.", + "wsl.onboarding.installOpencode": "Zainstaluj OpenCode", + "wsl.onboarding.chooseDistroFirst": "Najpierw wybierz dystrybucję.", + "wsl.onboarding.loadFailed": "Nie udało się załadować stanu WSL.", + "wsl.onboarding.loading": "Ładowanie...", + "wsl.onboarding.installedDistros": "Zainstalowane dystrybucje", + "wsl.onboarding.checkAgain": "Sprawdź ponownie", + "wsl.onboarding.distroStatus.ready": "Gotowa", + "wsl.onboarding.distroStatus.checking": "Sprawdzanie...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode nie jest zainstalowany", + "wsl.onboarding.distroStatus.missingTools": "Brak narzędzi bash i curl", + "wsl.onboarding.distroStatus.unsupported": "Nieobsługiwana · Użyj WSL 2", + "wsl.onboarding.needAnotherDistro": "Potrzebujesz innej dystrybucji?", + "wsl.onboarding.needAnotherDistroHint": "Zainstaluj dystrybucję systemu Linux z katalogu WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL nie jest zainstalowany", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Podsystem Windows dla systemu Linux) jest wymagany, zanim OpenCode będzie mógł dodać serwer WSL", + "wsl.onboarding.wslUnavailable.title": "WSL jest niedostępny", + "wsl.onboarding.wslUnavailable.description": "OpenCode nie mógł zweryfikować WSL na tym komputerze.", + "wsl.onboarding.installWsl": "Zainstaluj WSL", + "wsl.onboarding.windowsRestartRequired": + "Uruchom ponownie system Windows, aby dokończyć instalację WSL, a następnie ponownie otwórz OpenCode.", + "wsl.onboarding.next": "Dalej", + "wsl.onboarding.refresh": "Odśwież", + "wsl.onboarding.allDistrosAdded": "Wszystkie zainstalowane dystrybucje zostały już dodane.", + "wsl.onboarding.noDistros": "Nie wykryto jeszcze żadnych dystrybucji.", + "wsl.onboarding.install": "Zainstaluj", + "wsl.onboarding.installing": "Instalowanie...", + "wsl.onboarding.installDistro": "Zainstaluj dystrybucję", + "wsl.onboarding.searchDistros": "Szukaj dystrybucji", + "wsl.onboarding.wsl2Required": "Wymagany jest WSL 2.", + "wsl.onboarding.toolsRequired": "Ta dystrybucja wymaga narzędzi bash i curl.", + "wsl.onboarding.openTerminal": "Otwórz terminal", + "wsl.onboarding.path": "Ścieżka: {{path}}", + "wsl.onboarding.notFound": "nie znaleziono", + "wsl.onboarding.version": "Wersja: {{version}}", + "wsl.onboarding.unknown": "nieznana", + "wsl.onboarding.desktopVersion": "aplikacja komputerowa {{version}}", + "wsl.onboarding.versionMismatch": "Zainstalowana wersja nie jest zgodna z wersją aplikacji komputerowej.", + "wsl.onboarding.adding": "Dodawanie...", + "help.tabs.toast.ariaLabel": "Przedstawiamy karty. Organizuj pracę i aktywne sesje za pomocą kart", + "help.tabs.toast.dismiss": "Odrzuć informacje o kartach", + "help.tabs.title": "Przedstawiamy karty", + "help.tabs.description": "Organizuj pracę i aktywne sesje za pomocą kart", + "help.tabs.date": "14 lipca", + "help.tabs.introduction": "OpenCode Desktop korzysta teraz z układu opartego na kartach.", + "help.tabs.sessions": + "Rozpocznij nową sesję na karcie lub otwórz istniejącą sesję z dowolnego projektu. Otwórz nową kartę, gdy zaczynasz coś nowego, i zamknij ją po zakończeniu.", + "help.tabs.organize": + "Kilka otwartych kart ułatwia organizowanie aktywnych sesji. Jeśli chcesz zachować karty na dłużej, nadaj im łatwe do zapamiętania nazwy.", + "help.tabs.home": + "Wszystkie sesje i projekty znajdziesz na nowym ekranie głównym. Wybranie sesji powoduje otwarcie jej na karcie.", + "help.tabs.persistence": "Po ponownym otwarciu aplikacji karty pozostają otwarte.", + "help.tabs.worktrees": + "Nowy układ nie obsługuje jeszcze drzew roboczych Git, ale ta funkcja pojawi się wkrótce. Jeśli wolisz nadal korzystać z poprzedniego układu, możesz przełączać układy w Ustawieniach. Pamiętaj jednak, że za kilka tygodni nowy układ stanie się jedynym dostępnym.", + "dialog.project.edit.title": "Edytuj projekt", + "dialog.project.edit.name": "Nazwa", + "dialog.project.edit.icon": "Ikona", + "dialog.project.edit.icon.alt": "Ikona projektu", + "dialog.project.edit.icon.hint": "Kliknij, aby wybrać obraz, lub przeciągnij go tutaj", + "dialog.project.edit.icon.recommended": "Zalecane: 128 × 128 px", + "dialog.project.edit.color": "Kolor", + "dialog.project.edit.color.select": "Wybierz kolor {{color}}", + "dialog.project.edit.worktree.startup": "Skrypt uruchamiania przestrzeni roboczej", + "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", + "context.breakdown.user": "Użytkownik", + "context.breakdown.assistant": "Asystent", + "context.breakdown.tool": "Wywołania narzędzi", + "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", + "context.stats.model": "Model", + "context.stats.limit": "Limit kontekstu", + "context.stats.totalTokens": "Całkowita liczba tokenów", + "context.stats.usage": "Użycie", + "context.stats.inputTokens": "Tokeny wejściowe", + "context.stats.outputTokens": "Tokeny wyjściowe", + "context.stats.reasoningTokens": "Tokeny wnioskowania", + "context.stats.cacheTokens": "Tokeny pamięci podręcznej (odczyt/zapis)", + "context.stats.userMessages": "Wiadomości użytkownika", + "context.stats.assistantMessages": "Wiadomości asystenta", + "context.stats.totalCost": "Całkowity koszt", + "context.stats.sessionCreated": "Utworzono sesję", + "context.stats.lastActivity": "Ostatnia aktywność", + "context.usage.tokens": "Tokeny", + "context.usage.usage": "Użycie", + "context.usage.cost": "Koszt", + "context.usage.clickToView": "Kliknij, aby zobaczyć kontekst", + "context.usage.view": "Pokaż użycie kontekstu", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Język", + "toast.language.description": "Przełączono na {{language}}", + "toast.theme.title": "Przełączono motyw", + "toast.scheme.title": "Schemat kolorów", + "toast.workspace.enabled.title": "Przestrzenie robocze włączone", + "toast.workspace.enabled.description": "Kilka drzew roboczych jest teraz wyświetlanych na pasku bocznym", + "toast.workspace.disabled.title": "Przestrzenie robocze wyłączone", + "toast.workspace.disabled.description": "Tylko główne drzewo robocze jest wyświetlane na pasku bocznym", + "toast.permissions.autoaccept.on.title": "Automatyczne akceptowanie uprawnień", + "toast.permissions.autoaccept.on.description": "Żądania uprawnień będą automatycznie zatwierdzane", + "toast.permissions.autoaccept.off.title": "Zatrzymano automatyczne akceptowanie uprawnień", + "toast.permissions.autoaccept.off.description": "Żądania uprawnień będą wymagały zatwierdzenia", + "toast.model.none.title": "Nie wybrano modelu", + "toast.model.none.description": "Połącz dostawcę, aby podsumować tę sesję", + "toast.file.loadFailed.title": "Nie udało się załadować pliku", + "toast.file.listFailed.title": "Nie udało się wyświetlić listy plików", + "toast.context.noLineSelection.title": "Brak zaznaczenia linii", + "toast.context.noLineSelection.description": "Najpierw wybierz zakres linii w zakładce pliku.", + "toast.session.share.copyFailed.title": "Nie udało się skopiować adresu URL do schowka", + "toast.session.share.success.title": "Sesja udostępniona", + "toast.session.share.success.description": "Adres URL udostępnionej sesji skopiowano do schowka!", + "toast.session.share.failed.title": "Nie udało się udostępnić sesji", + "toast.session.share.failed.description": "Wystąpił błąd podczas udostępniania sesji", + "toast.session.unshare.success.title": "Zatrzymano udostępnianie sesji", + "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.", + "toast.update.action.installRestart": "Zainstaluj i uruchom ponownie", + "toast.update.action.notYet": "Jeszcze nie", + "error.page.title": "Coś poszło nie tak", + "error.page.description": "Wystąpił błąd podczas ładowania aplikacji.", + "error.page.description.localServerStartup": "Wystąpił błąd podczas uruchamiania serwera lokalnego.", + "error.page.details.label": "Szczegóły błędu", + "error.page.action.restart": "Uruchom ponownie", + "error.page.action.report": "Zgłoś błąd", + "error.page.action.reported": "Błąd zgłoszony", + "error.page.action.exportLogs": "Eksportuj dzienniki", + "error.page.action.checking": "Sprawdzanie...", + "error.page.action.checkUpdates": "Sprawdź aktualizacje", + "error.page.action.updateTo": "Zaktualizuj do {{version}}", + "error.page.report.prefix": "Proszę zgłosić ten błąd do zespołu OpenCode", + "error.page.report.discord": "na Discordzie", + "error.page.version": "Wersja: {{version}}", + "error.dev.rootNotFound": + "Nie znaleziono elementu głównego. Czy zapomniałeś dodać go do swojego index.html? A może atrybut id został błędnie wpisany?", + "error.serverSync.connectFailed": "Nie można połączyć się z serwerem. Czy serwer działa pod adresem `{{url}}`?", + "directory.error.invalidUrl": "Nieprawidłowy katalog w adresie URL.", + "error.chain.unknown": "Nieznany błąd", + "error.chain.causedBy": "Spowodowany przez:", + "error.chain.apiError": "Błąd API", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Można ponowić: {{retryable}}", + "error.chain.responseBody": "Treść odpowiedzi:\n{{body}}", + "error.chain.didYouMean": "Czy miałeś na myśli: {{suggestions}}", + "error.chain.modelNotFound": "Model nie znaleziony: {{provider}}/{{model}}", + "error.chain.checkConfig": "Sprawdź nazwy dostawców i modeli w konfiguracji (opencode.json)", + "error.chain.mcpFailed": + "Serwer MCP „{{name}}” nie uruchomił się. Uwaga: OpenCode nie obsługuje jeszcze uwierzytelniania MCP.", + "error.chain.providerAuthFailed": "Uwierzytelnianie dostawcy nie powiodło się ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Nie udało się zainicjować dostawcy "{{provider}}". Sprawdź poświadczenia i konfigurację.', + "error.chain.configJsonInvalid": "Plik konfiguracyjny w {{path}} nie jest poprawnym JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Plik konfiguracyjny w {{path}} nie jest poprawnym JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Katalog "{{dir}}" w {{path}} jest nieprawidłowy. Zmień nazwę katalogu na "{{suggestion}}" lub usuń go. To częsta literówka.', + "error.chain.configFrontmatterError": "Nie udało się przetworzyć frontmatter w {{path}}:\n{{message}}", + "error.chain.configInvalid": "Plik konfiguracyjny w {{path}} jest nieprawidłowy", + "error.chain.configInvalidWithMessage": "Plik konfiguracyjny w {{path}} jest nieprawidłowy: {{message}}", + "notification.permission.title": "Wymagane uprawnienie", + "notification.permission.description": "{{sessionTitle}} w {{projectName}} potrzebuje uprawnienia", + "notification.question.title": "Pytanie", + "notification.question.description": "{{sessionTitle}} w {{projectName}} ma pytanie", + "notification.action.goToSession": "Przejdź do sesji", + "notification.session.responseReady.title": "Odpowiedź gotowa", + "notification.session.error.title": "Błąd sesji", + "notification.session.error.fallbackDescription": "Wystąpił błąd", + "home.recentProjects": "Ostatnie projekty", + "home.empty.title": "Brak ostatnich projektów", + "home.empty.description": "Zacznij od otwarcia lokalnego projektu", + "home.title": "Strona główna", + "home.projects": "Projekty", + "home.project.add": "Dodaj projekt", + "home.recentlyClosed": "Ostatnio zamknięte", + "home.server.collapse": "Zwiń projekty serwera", + "home.server.expand": "Rozwiń projekty serwera", + "home.sessions.search.placeholder": "Szukaj sesji", + "home.sessions.search.placeholder.scoped": "Szukaj sesji w {{scope}}", + "home.sessions.search.sessions": "Sesje", + "home.sessions.search.noResults": "Nie znaleziono sesji dla {{query}}", + "home.sessions.empty": "Jeszcze nic tu nie ma", + "home.sessions.empty.description": "Utwórz sesję, aby rozpocząć", + "home.sessions.group.today": "Dzisiaj", + "home.sessions.group.yesterday": "Wczoraj", + "home.sessions.group.older": "Starsze", + "home.providerTip": + "Połącz się z ponad 75 dostawcami, aby korzystać z innych modeli, w tym Claude, GPT, Gemini i innych", + "session.tab.session": "Sesja", + "session.tab.review": "Przegląd", + "session.tab.context": "Kontekst", + "session.tab.unknown": "Nieznana sesja", + "session.panel.reviewAndFiles": "Przegląd i pliki", + "session.error.notFound": "Nie można znaleźć tej sesji", + "session.error.notFound.description": "Ta karta wskazuje sesję, która nie istnieje już na tym serwerze.", + "session.error.notFound.closeTab": "Zamknij kartę", + "session.error.serverConnection": "Nie można połączyć się z tym serwerem", + "session.review.filesChanged": "Zmieniono {{count}} plików", + "session.review.change.one": "Zmiana", + "session.review.change.other": "Zmiany", + "session.review.loadingChanges": "Ładowanie zmian...", + "session.review.empty": "Brak zmian w tej sesji", + "session.review.noVcs": "Nie wykryto systemu kontroli wersji Git, dlatego zmiany nie są wyświetlane", + "session.review.noSnapshot": "Śledzenie migawek jest wyłączone w konfiguracji, więc zmiany w sesji są niedostępne", + "session.review.noChanges": "Brak zmian", + "session.review.noUncommittedChanges": "Brak jeszcze niezatwierdzonych zmian", + "session.review.noBranchChanges": "Brak jeszcze zmian w gałęzi", + "session.files.selectToOpen": "Wybierz plik do otwarcia", + "session.files.all": "Wszystkie pliki", + "session.files.empty": "Brak plików", + "session.files.binaryContent": "Plik binarny (zawartość nie może być wyświetlona)", + "session.messages.renderEarlier": "Renderuj wcześniejsze wiadomości", + "session.messages.loadingEarlier": "Ładowanie wcześniejszych wiadomości...", + "session.messages.loadEarlier": "Załaduj wcześniejsze wiadomości", + "session.messages.loading": "Ładowanie wiadomości...", + "session.messages.jumpToLatest": "Przejdź do najnowszych", + "session.context.addToContext": "Dodaj {{selection}} do kontekstu", + "session.todo.title": "Zadania", + "session.todo.collapse": "Zwiń", + "session.todo.expand": "Rozwiń", + "session.question.minimize": "Zminimalizuj pytanie", + "session.question.restore": "Przywróć pytanie", + "session.question.pending.one": "{{count}} oczekujące pytanie", + "session.question.pending.few": "{{count}} oczekujące pytania", + "session.question.pending.many": "{{count}} oczekujących pytań", + "session.question.pending.other": "Pytania oczekujące: {{count}}", + "session.followupDock.summary.one": "{{count}} wiadomość w kolejce", + "session.followupDock.summary.few": "{{count}} wiadomości w kolejce", + "session.followupDock.summary.many": "{{count}} wiadomości w kolejce", + "session.followupDock.summary.other": "{{count}} wiadomości w kolejce", + "session.followupDock.sendNow": "Wyślij teraz", + "session.followupDock.edit": "Edytuj", + "session.followupDock.collapse": "Zwiń wiadomości w kolejce", + "session.followupDock.expand": "Rozwiń wiadomości w kolejce", + "session.revertDock.summary.one": "{{count}} cofnięta wiadomość", + "session.revertDock.summary.few": "{{count}} cofnięte wiadomości", + "session.revertDock.summary.many": "{{count}} cofniętych wiadomości", + "session.revertDock.summary.other": "{{count}} cofnięte wiadomości", + "session.revertDock.collapse": "Zwiń cofnięte wiadomości", + "session.revertDock.expand": "Rozwiń cofnięte wiadomości", + "session.revertDock.restore": "Przywróć wiadomość", + "session.new.title": "Zbuduj cokolwiek", + "session.new.project.new": "Nowy projekt", + "session.new.project.search": "Szukaj projektów", + "session.new.project.add": "Dodaj projekt", + "session.new.worktree.main": "Główna gałąź", + "session.new.worktree.mainWithBranch": "Główna gałąź ({{branch}})", + "session.new.worktree.create": "Utwórz nowe drzewo robocze", + "session.new.workspace.runIn": "Uruchom sesję w", + "session.new.workspace.triggerLocal": "Lokalnie", + "session.new.workspace.local": "Lokalne repozytorium", + "session.new.workspace.existing": "Przestrzeń robocza…", + "session.new.git.none": "Brak Git", + "session.new.lastModified": "Ostatnio zmodyfikowano", + "session.header.search.placeholder": "Szukaj {{project}}", + "session.header.searchFiles": "Szukaj plików", + "session.header.openIn": "Otwórz w", + "session.header.open.action": "Otwórz {{app}}", + "session.header.open.ariaLabel": "Otwórz w {{app}}", + "session.header.open.menu": "Opcje otwierania", + "session.header.open.copyPath": "Kopiuj ścieżkę", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Konfiguracje serwerów", + "status.popover.tab.servers": "Serwery", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Wtyczki", + "status.popover.action.manageServers": "Zarządzaj serwerami", + "session.share.popover.title": "Opublikuj w sieci", + "session.share.popover.description.shared": + "Ta sesja jest publiczna w sieci. Jest dostępna dla każdego, kto posiada link.", + "session.share.popover.description.unshared": + "Udostępnij sesję publicznie w sieci. Będzie dostępna dla każdego, kto posiada link.", + "session.share.action.share": "Udostępnij", + "session.share.action.publish": "Opublikuj", + "session.share.action.publishing": "Publikowanie...", + "session.share.action.unpublish": "Cofnij publikację", + "session.share.action.unpublishing": "Cofanie publikacji...", + "session.share.action.view": "Wyświetl", + "session.share.copy.copied": "Skopiowano", + "session.share.copy.copyLink": "Kopiuj link", + "lsp.tooltip.none": "Brak serwerów LSP", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Ładowanie promptu...", + "terminal.loading": "Ładowanie terminala...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Zamknij terminal", + "terminal.connectionLost.title": "Utracono połączenie", + "terminal.connectionLost.description": + "Połączenie z terminalem zostało przerwane. Może się to zdarzyć podczas ponownego uruchamiania serwera.", + "terminal.connectTicket.csrfError": + "Bilet połączenia PTY został odrzucony podczas kontroli źródła lub CSRF. Sprawdź konfigurację CORS serwera.", + "terminal.connectTicket.statusError": "Bilet połączenia PTY zakończył się niepowodzeniem ze statusem {{status}}", + + "titlebar.update": "Aktualizuj", + "titlebar.updateVersion": "Aktualizuj {{version}}", + "common.closeTab": "Zamknij kartę", + "common.dismiss": "Odrzuć", + "common.requestFailed": "Żądanie nie powiodło się", + "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ń", + "common.close": "Zamknij", + "common.edit": "Edytuj", + "common.loadMore": "Załaduj więcej", + "common.key.esc": "ESC", + "sidebar.menu.toggle": "Przełącz menu", + "sidebar.nav.projectsAndSessions": "Projekty i sesje", + "sidebar.settings": "Ustawienia", + "sidebar.help": "Pomoc", + "sidebar.workspaces.enable": "Włącz przestrzenie robocze", + "sidebar.workspaces.disable": "Wyłącz przestrzenie robocze", + "sidebar.gettingStarted.title": "Pierwsze kroki", + "sidebar.gettingStarted.line1": "OpenCode zawiera darmowe modele, więc możesz zacząć od razu.", + "sidebar.gettingStarted.line2": "Połącz dowolnego dostawcę, aby używać modeli, w tym Claude, GPT, Gemini itp.", + "sidebar.project.recentSessions": "Ostatnie sesje", + "sidebar.project.viewAllSessions": "Zobacz wszystkie sesje", + "sidebar.project.clearNotifications": "Wyczyść powiadomienia", + "sidebar.empty.title": "Brak otwartych projektów", + "sidebar.empty.description": "Otwórz projekt, aby rozpocząć", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Aplikacja komputerowa", + "settings.section.server": "Serwer", + "settings.tab.general": "Ogólne", + "settings.tab.shortcuts": "Skróty", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Integracja z WSL", + "settings.desktop.wsl.description": "Uruchamiaj serwer OpenCode wewnątrz WSL w systemie Windows.", + "settings.general.section.appearance": "Wygląd", + "settings.general.section.advanced": "Zaawansowane", + "settings.general.section.notifications": "Powiadomienia systemowe", + "settings.general.section.updates": "Aktualizacje", + "settings.general.section.sounds": "Efekty dźwiękowe", + "settings.general.section.feed": "Kanał", + "settings.general.section.display": "Ekran", + "settings.general.row.language.title": "Język", + "settings.general.row.language.description": "Zmień język wyświetlania dla OpenCode", + "settings.general.row.shell.title": "Powłoka terminala", + "settings.general.row.shell.description": + "Wybierz powłokę używaną w terminalu. Zgodne powłoki są również używane przez wywołania narzędzi agenta.", + "settings.general.row.shell.autoDefault": "Automatycznie (domyślna)", + "settings.general.row.shell.terminalOnly": "tylko terminal", + "settings.general.row.appearance.title": "Wygląd", + "settings.general.row.appearance.description": "Dostosuj wygląd OpenCode na swoim urządzeniu", + "settings.general.row.colorScheme.title": "Schemat kolorów", + "settings.general.row.colorScheme.description": + "Wybierz, czy OpenCode ma używać motywu systemowego, jasnego czy ciemnego", + "settings.general.row.theme.title": "Motyw", + "settings.general.row.theme.description": "Dostosuj motyw OpenCode.", + "settings.general.row.font.title": "Czcionka kodu", + "settings.general.row.font.description": "Dostosuj czcionkę używaną w blokach kodu", + "settings.general.row.terminalFont.title": "Czcionka terminala", + "settings.general.row.terminalFont.description": "Dostosuj czcionkę używaną w terminalu", + "settings.general.row.uiFont.title": "Czcionka interfejsu", + "settings.general.row.uiFont.description": "Dostosuj czcionkę używaną w całym interfejsie", + "settings.general.row.followup.title": "Zachowanie kontynuacji", + "settings.general.row.followup.description": "Wybierz, czy kontynuacja ma być natychmiastowa, czy czekać w kolejce", + "settings.general.row.followup.option.queue": "Kolejka", + "settings.general.row.followup.option.steer": "Sterowanie", + "settings.general.row.showFileTree.title": "Drzewo plików", + "settings.general.row.showFileTree.description": "Pokaż panel drzewa plików w sesjach", + "settings.general.row.showNavigation.title": "Elementy nawigacji", + "settings.general.row.showNavigation.description": + "Pokaż przyciski Wstecz i Dalej na pasku tytułu aplikacji komputerowej", + "settings.general.row.showSearch.title": "Paleta poleceń", + "settings.general.row.showSearch.description": "Pokaż przycisk wyszukiwania i palety poleceń na pasku tytułu", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Pokaż przycisk terminala na pasku tytułu aplikacji komputerowej", + "settings.general.row.showStatus.title": "Status serwera", + "settings.general.row.showStatus.description": "Pokaż przycisk statusu serwera na pasku tytułu", + "settings.general.row.mobileTitlebarBottom.title": "Dolna nawigacja", + "settings.general.row.mobileTitlebarBottom.description": + "Umieść pasek tytułu i karty sesji u dołu ekranu na urządzeniach mobilnych", + "settings.general.row.showCustomAgents.title": "Wybór agenta", + "settings.general.row.showCustomAgents.description": + "Umożliwiaj przełączanie agentów w edytorze wiadomości. Po ukryciu domyślnie używany jest agent Build.", + "settings.general.row.reasoningSummaries.title": "Pokaż podsumowania wnioskowania", + "settings.general.row.reasoningSummaries.description": "Wyświetlaj podsumowania wnioskowania modelu na osi czasu", + "settings.general.row.shellToolPartsExpanded.title": "Rozwijaj elementy narzędzia shell", + "settings.general.row.shellToolPartsExpanded.description": + "Domyślnie pokazuj rozwinięte elementy narzędzia shell na osi czasu", + "settings.general.row.editToolPartsExpanded.title": "Rozwijaj elementy narzędzia edit", + "settings.general.row.editToolPartsExpanded.description": + "Domyślnie pokazuj rozwinięte elementy narzędzi edit, write i patch na osi czasu", + "settings.general.row.newInterface.title": "Nowy układ", + "settings.general.row.newInterface.badge": "Nowość", + "settings.general.row.newInterface.description": + "Używaj nowych kart i układu strony głównej. Przez ograniczony czas możesz przełączać się między układami.", + "settings.general.row.newInterfaceNotice.title": "Korzystasz teraz z nowego układu", + "settings.general.row.newInterfaceNotice.description": "Poprzedni układ nie jest już dostępny", + "settings.general.row.newInterfaceNotice.dismiss": "Odrzuć", + "settings.general.row.pinchZoom.title": "Powiększanie gestem szczypania", + "settings.general.row.pinchZoom.description": + "Zezwalaj na powiększanie gestem szczypania na gładziku lub przez przewijanie z wciśniętym klawiszem Ctrl", + "settings.general.row.wayland.title": "Użyj natywnego protokołu Wayland", + "settings.general.row.wayland.description": + "Wyłącz awaryjne korzystanie z X11 w środowisku Wayland. Wymaga ponownego uruchomienia.", + "settings.general.row.wayland.tooltip": + "W systemie Linux z monitorami o różnej częstotliwości odświeżania natywny protokół Wayland może być stabilniejszy.", + "settings.general.row.releaseNotes.title": "Informacje o wydaniu", + "settings.general.row.releaseNotes.description": "Pokazuj wyskakujące okna „Co nowego” po aktualizacjach", + "settings.updates.row.startup.title": "Sprawdzaj aktualizacje przy uruchomieniu", + "settings.updates.row.startup.description": "Automatycznie sprawdzaj aktualizacje podczas uruchamiania OpenCode", + "settings.updates.row.check.title": "Sprawdź aktualizacje", + "settings.updates.row.check.description": "Ręcznie sprawdź aktualizacje i zainstaluj, jeśli są dostępne", + "settings.updates.action.checkNow": "Sprawdź teraz", + "settings.updates.action.checking": "Sprawdzanie...", + "settings.updates.action.downloading": "Pobieranie...", + "settings.updates.action.installing": "Instalowanie...", + "settings.updates.toast.latest.title": "Masz najnowszą wersję", + "settings.updates.toast.latest.description": "Korzystasz z najnowszej wersji OpenCode.", + "sound.option.none": "Brak", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Pokaż powiadomienie systemowe, gdy agent zakończy pracę lub wymaga uwagi", + "settings.general.notifications.permissions.title": "Uprawnienia", + "settings.general.notifications.permissions.description": + "Pokaż powiadomienie systemowe, gdy wymagane jest uprawnienie", + "settings.general.notifications.errors.title": "Błędy", + "settings.general.notifications.errors.description": "Pokaż powiadomienie systemowe, gdy wystąpi błąd", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Odtwórz dźwięk, gdy agent zakończy pracę lub wymaga uwagi", + "settings.general.sounds.permissions.title": "Uprawnienia", + "settings.general.sounds.permissions.description": "Odtwórz dźwięk, gdy wymagane jest uprawnienie", + "settings.general.sounds.errors.title": "Błędy", + "settings.general.sounds.errors.description": "Odtwórz dźwięk, gdy wystąpi błąd", + "settings.shortcuts.title": "Skróty klawiszowe", + "settings.shortcuts.reset.button": "Przywróć domyślne", + "settings.shortcuts.reset.toast.title": "Zresetowano skróty", + "settings.shortcuts.reset.toast.description": "Skróty klawiszowe zostały przywrócone do ustawień domyślnych.", + "settings.shortcuts.conflict.title": "Skrót już w użyciu", + "settings.shortcuts.conflict.description": "{{keybind}} jest już przypisany do {{titles}}.", + "settings.shortcuts.unassigned": "Nieprzypisany", + "settings.shortcuts.pressKeys": "Naciśnij klawisze", + "settings.shortcuts.search.placeholder": "Szukaj skrótów", + "settings.shortcuts.search.empty": "Nie znaleziono skrótów", + "settings.shortcuts.group.general": "Ogólne", + "settings.shortcuts.group.session": "Sesja", + "settings.shortcuts.group.navigation": "Nawigacja", + "settings.shortcuts.group.modelAndAgent": "Model i agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + "settings.providers.title": "Dostawcy", + "settings.providers.description": "Ustawienia dostawców będą tutaj konfigurowalne.", + "settings.providers.section.connected": "Połączeni dostawcy", + "settings.providers.connected.empty": "Brak połączonych dostawców", + "settings.providers.section.popular": "Popularni dostawcy", + "settings.providers.tag.environment": "Środowisko", + "settings.providers.tag.config": "Konfiguracja", + "settings.providers.tag.custom": "Niestandardowe", + "settings.providers.tag.other": "Inne", + "settings.models.title": "Modele", + "settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.", + "settings.agents.title": "Agenci", + "settings.agents.description": "Ustawienia agentów będą tutaj konfigurowalne.", + "settings.commands.title": "Polecenia", + "settings.commands.description": "Ustawienia poleceń będą tutaj konfigurowalne.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Ustawienia MCP będą tutaj konfigurowalne.", + "settings.permissions.title": "Uprawnienia", + "settings.permissions.description": "Kontroluj, jakich narzędzi serwer może używać domyślnie.", + "settings.permissions.section.tools": "Narzędzia", + "settings.permissions.toast.updateFailed.title": "Nie udało się zaktualizować uprawnień", + "settings.permissions.action.allow": "Zezwól", + "settings.permissions.action.ask": "Pytaj", + "settings.permissions.action.deny": "Odmów", + "settings.permissions.tool.read.title": "Odczyt", + "settings.permissions.tool.read.description": "Odczyt pliku (pasuje do ścieżki pliku)", + "settings.permissions.tool.edit.title": "Edycja", + "settings.permissions.tool.edit.description": + "Modyfikowanie plików, w tym edytowanie, zapisywanie, stosowanie łatek i edytowanie wielu plików", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Dopasowywanie plików za pomocą wzorców glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Przeszukiwanie zawartości plików za pomocą wyrażeń regularnych", + "settings.permissions.tool.list.title": "Lista", + "settings.permissions.tool.list.description": "Wyświetlanie listy plików w katalogu", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Uruchamianie poleceń powłoki", + "settings.permissions.tool.task.title": "Zadanie", + "settings.permissions.tool.task.description": "Uruchamianie podagentów", + "settings.permissions.tool.skill.title": "Umiejętność", + "settings.permissions.tool.skill.description": "Ładowanie umiejętności według nazwy", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Uruchamianie zapytań serwera językowego", + "settings.permissions.tool.todowrite.title": "Zapis Todo", + "settings.permissions.tool.todowrite.description": "Aktualizacja listy zadań", + "settings.permissions.tool.webfetch.title": "Pobieranie z sieci", + "settings.permissions.tool.webfetch.description": "Pobieranie zawartości z adresu URL", + "settings.permissions.tool.websearch.title": "Wyszukiwanie w sieci", + "settings.permissions.tool.websearch.description": "Przeszukiwanie sieci", + "settings.permissions.tool.external_directory.title": "Katalog zewnętrzny", + "settings.permissions.tool.external_directory.description": "Dostęp do plików poza katalogiem projektu", + "settings.permissions.tool.doom_loop.title": "Zapętlenie", + "settings.permissions.tool.doom_loop.description": "Wykrywanie powtarzających się wywołań narzędzi (doom loop)", + "session.delete.failed.title": "Nie udało się usunąć sesji", + "session.delete.title": "Usuń sesję", + "session.delete.confirm": 'Usunąć sesję "{{name}}"?', + "session.delete.button": "Usuń sesję", + "workspace.new": "Nowa przestrzeń robocza", + "workspace.type.local": "lokalna", + "workspace.type.sandbox": "piaskownica", + "workspace.create.failed.title": "Nie udało się utworzyć przestrzeni roboczej", + "workspace.delete.failed.title": "Nie udało się usunąć przestrzeni roboczej", + "workspace.resetting.title": "Resetowanie przestrzeni roboczej", + "workspace.resetting.description": "To może potrwać minutę.", + "workspace.reset.failed.title": "Nie udało się zresetować przestrzeni roboczej", + "workspace.reset.success.title": "Przestrzeń robocza zresetowana", + "workspace.reset.success.description": "Przestrzeń robocza odpowiada teraz domyślnej gałęzi.", + "workspace.error.stillPreparing": "Przestrzeń robocza jest wciąż przygotowywana", + "workspace.status.checking": "Sprawdzanie niezscalonych zmian...", + "workspace.status.error": "Nie można zweryfikować statusu Git.", + "workspace.status.clean": "Nie wykryto niezscalonych zmian.", + "workspace.status.dirty": "Wykryto niezscalone zmiany w tej przestrzeni roboczej.", + "workspace.delete.title": "Usuń przestrzeń roboczą", + "workspace.delete.confirm": 'Usunąć przestrzeń roboczą "{{name}}"?', + "workspace.delete.button": "Usuń przestrzeń roboczą", + "workspace.reset.title": "Resetuj przestrzeń roboczą", + "workspace.reset.confirm": 'Zresetować przestrzeń roboczą "{{name}}"?', + "workspace.reset.button": "Resetuj przestrzeń roboczą", + "workspace.reset.archived.none": "Żadne aktywne sesje nie zostaną zarchiwizowane.", + "workspace.reset.archived.one": "1 sesja zostanie zarchiwizowana.", + "workspace.reset.archived.many": "{{count}} sesji zostanie zarchiwizowanych.", + "workspace.reset.note": "To zresetuje przestrzeń roboczą, aby odpowiadała domyślnej gałęzi.", + "common.open": "Otwórz", + "dialog.releaseNotes.action.getStarted": "Rozpocznij", + "dialog.releaseNotes.action.next": "Dalej", + "dialog.releaseNotes.action.hideFuture": "Nie pokazuj tego w przyszłości", + "dialog.releaseNotes.media.alt": "Podgląd wydania", + "toast.project.reloadFailed.title": "Nie udało się ponownie wczytać {{project}}", + "error.server.invalidConfiguration": "Nieprawidłowa konfiguracja", + "common.moreCountSuffix": " (jeszcze {{count}})", + "common.time.justNow": "Przed chwilą", + "common.time.minutesAgo.short": "{{count}} min temu", + "common.time.hoursAgo.short": "{{count}} godz. temu", + "common.time.daysAgo.short": "{{count}} dni temu", + "settings.providers.connected.environmentDescription": "Połączono ze zmiennymi środowiskowymi", + "settings.providers.custom.description": "Dodaj dostawcę zgodnego z OpenAI za pomocą bazowego adresu URL.", + + "app.server.unreachable": "Nie można połączyć z {{server}}", + "app.server.retrying": "Ponawianie automatycznie...", + "app.server.otherServers": "Inne serwery", + "dialog.server.add.usernamePlaceholder": "nazwa użytkownika", + "dialog.server.add.passwordPlaceholder": "hasło", + "server.row.noUsername": "brak nazwy użytkownika", + "session.review.noVcs.createGit.title": "Utwórz repozytorium Git", + "session.review.noVcs.createGit.description": "Śledź, przeglądaj i cofaj zmiany w tym projekcie", + "session.review.noVcs.createGit.actionLoading": "Tworzenie repozytorium Git...", + "session.review.noVcs.createGit.action": "Utwórz repozytorium Git", + "session.todo.progress": "Ukończono {{done}} z {{total}} zadań", + "session.question.progress": "{{current}} z {{total}} pytań", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Eksplorator plików", + "session.header.reveal.finder": "Pokaż w Finderze", + "session.header.reveal.fileExplorer": "Pokaż w Eksploratorze plików", + "session.header.reveal.containingFolder": "Otwórz folder zawierający", + "session.header.open.fileManager": "Menedżer plików", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Diagnostyka wydajności w środowisku deweloperskim", + "debugBar.na": "n.d.", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Ostatnie zakończone przejście między trasami obejmujące stronę sesji, mierzone od uruchomienia routera do pierwszego odrysowania po ustabilizowaniu widoku.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Średnia liczba klatek na sekundę w ciągu ostatnich 5 sekund.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Najgorszy czas klatki w ciągu ostatnich 5 sekund.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Klatki powyżej 32 ms w ciągu ostatnich 5 sekund.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Zablokowany czas i liczba długich zadań w ciągu ostatnich 5 sekund. Maksymalne zadanie: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Najgorsze zaobserwowane opóźnienie wejścia w ciągu ostatnich 5 sekund.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Przybliżony czas trwania interakcji w ciągu ostatnich 5 sekund. Jest to podobne do INP, a nie oficjalne Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Skumulowane przesunięcie układu od bieżącego uruchomienia aplikacji.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Wykorzystana pamięć sterty JS względem jej limitu. Tylko Chromium.", + "debugBar.mem.tip": "Wykorzystana pamięć sterty JS względem jej limitu: {{used}} z {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Wymuś style fokusu dla wszystkich elementów interaktywnych", + "debugBar.focus.on": "WŁ.", + "debugBar.focus.off": "WYŁ.", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Wymuś kierunek układu całej aplikacji bez zmiany wybranego języka", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Spacja", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "nieznany", + "error.page.circular": "[Cykliczne]", + "error.serverSDK.noServerAvailable": "Brak dostępnego serwera", + "error.serverSDK.serverNotAvailable": "Serwer niedostępny", + "error.childStore.persistedCacheCreateFailed": "Nie udało się utworzyć trwałej pamięci podręcznej", + "error.childStore.persistedProjectMetadataCreateFailed": "Nie udało się utworzyć trwałych metadanych projektu", + "error.childStore.persistedProjectIconCreateFailed": "Nie udało się utworzyć trwałej ikony projektu", + "error.childStore.storeCreateFailed": "Nie udało się utworzyć magazynu", + "terminal.connectionLost.abnormalClose": "WebSocket zamknięty nieprawidłowo: {{code}}", +} diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf4fa8d8f4ca990837802d9d2b00ecd5a5b73e84 --- /dev/null +++ b/packages/app/src/i18n/ru.ts @@ -0,0 +1,1262 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Файл", + "desktop.menu.edit": "Правка", + "desktop.menu.view": "Вид", + "desktop.menu.go": "Переход", + "desktop.menu.window": "Окно", + "desktop.menu.help": "Справка", + "desktop.menu.checkForUpdates": "Проверить наличие обновлений...", + "desktop.menu.settings": "Настройки", + "desktop.menu.reloadWebview": "Перезагрузить веб-представление", + "desktop.menu.restart": "Перезапустить", + "desktop.menu.exportLogs": "Экспортировать журналы...", + "desktop.menu.newSession": "Новая сессия", + "desktop.menu.openProject": "Открыть проект...", + "desktop.menu.newWindow": "Новое окно", + "desktop.menu.closeWindow": "Закрыть окно", + "desktop.menu.undo": "Отменить", + "desktop.menu.redo": "Повторить", + "desktop.menu.cut": "Вырезать", + "desktop.menu.copy": "Копировать", + "desktop.menu.paste": "Вставить", + "desktop.menu.delete": "Удалить", + "desktop.menu.selectAll": "Выбрать всё", + "desktop.menu.toggleSidebar": "Переключить боковую панель", + "desktop.menu.toggleTerminal": "Переключить терминал", + "desktop.menu.toggleFileTree": "Переключить дерево файлов", + "desktop.menu.reload": "Перезагрузить", + "desktop.menu.toggleDeveloperTools": "Переключить инструменты разработчика", + "desktop.menu.actualSize": "Фактический размер", + "desktop.menu.zoomIn": "Увеличить", + "desktop.menu.zoomOut": "Уменьшить", + "desktop.menu.toggleFullScreen": "Переключить полноэкранный режим", + "desktop.menu.back": "Назад", + "desktop.menu.forward": "Вперёд", + "desktop.menu.previousSession": "Предыдущая сессия", + "desktop.menu.nextSession": "Следующая сессия", + "desktop.menu.previousProject": "Предыдущий проект", + "desktop.menu.nextProject": "Следующий проект", + "desktop.menu.minimize": "Свернуть", + "desktop.menu.maximize": "Развернуть", + "desktop.menu.documentation": "Документация OpenCode", + "desktop.menu.supportForum": "Форум поддержки", + "desktop.menu.shareFeedback": "Отправить отзыв", + "desktop.menu.reportBug": "Сообщить об ошибке", + "desktop.menu.ariaLabel": "Меню OpenCode", + + "desktop.updater.dialog.checkFailed.message": "Не удалось проверить наличие обновлений.", + "desktop.updater.dialog.checkFailed.title": "Ошибка обновления", + "desktop.updater.dialog.upToDate.message": "У вас установлена последняя версия.", + "desktop.updater.dialog.upToDate.title": "Обновлений нет", + "desktop.updater.dialog.ready.message": "Обновление {{version}} загружено. Перезапустить сейчас?", + "desktop.updater.dialog.ready.title": "Обновление готово", + "desktop.updater.dialog.restart": "Перезапустить", + "desktop.updater.dialog.later": "Позже", + + "desktop.recovery.action.relaunch": "Запустить снова", + "desktop.recovery.action.exportLogs": "Экспортировать журналы", + "desktop.recovery.action.keepWaiting": "Продолжить ожидание", + "desktop.recovery.action.quit": "Выйти", + "desktop.recovery.loadFailed": "Не удалось загрузить OpenCode", + "desktop.recovery.terminated": "Окно OpenCode неожиданно завершило работу", + "desktop.recovery.unresponsive": "OpenCode не отвечает", + "desktop.recovery.unresponsive.detail": "Можно снова запустить приложение, открыть журналы или продолжить ожидание.", + "desktop.recovery.loadFailed.detail": "Окно: {{window}}\nURL: {{url}}\nОшибка: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Окно: {{window}}\nПричина: {{reason}}\nКод: {{code}}", + "desktop.recovery.unknown": "<неизвестно>", + + "desktop.dialog.chooseFolder": "Выберите папку", + "desktop.dialog.chooseFile": "Выберите файл", + "desktop.dialog.saveFile": "Сохранить файл", + "desktop.dialog.files": "Файлы", + "desktop.server.local": "Локальный сервер", + + "desktop.wsl.error.windowsOnly": "WSL доступна только в Windows", + "desktop.wsl.error.unavailable": "WSL недоступна", + "desktop.wsl.error.listInstalled": "Не удалось получить список установленных дистрибутивов WSL", + "desktop.wsl.error.listOnline": "Не удалось получить список доступных дистрибутивов WSL", + "desktop.wsl.error.executeDistro": "Не удаётся выполнять команды в дистрибутиве", + "desktop.wsl.error.installWsl": "Не удалось установить WSL", + "desktop.wsl.error.installDistro": "Не удалось установить дистрибутив: {{distro}}", + "desktop.wsl.error.installOpencode": "Не удалось установить OpenCode", + "desktop.wsl.error.alreadyAdded": "Дистрибутив {{distro}} уже добавлен", + "desktop.wsl.error.opencodeMissing": "opencode не установлен в этом дистрибутиве", + "desktop.wsl.error.opencodeCannotRun": "opencode установлен, но его не удалось запустить", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode не установлен в {{distro}}", + "desktop.wsl.error.updateVersion": + "Обновление OpenCode завершено, но {{distro}} по-прежнему сообщает версию {{installed}}; ожидалась {{expected}}", + "desktop.wsl.error.noVersion": "нет версии", + "desktop.wsl.error.serverExited": "Сервер WSL завершил работу после запуска (код={{code}}, сигнал={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Сервер WSL завершил работу до перехода в рабочее состояние (код={{code}}, сигнал={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Истекло время ожидания проверки работоспособности Sidecar для {{distro}} ({{timeout}} мс)", + "desktop.wsl.error.commandTimeout": "Истекло время ожидания выполнения {{command}} {{args}} ({{timeout}} мс)", + "desktop.wsl.error.failedPort": "Не удалось получить порт", + + "desktop.picker.error.notSelected": "В средстве выбора не выбран файл", + "desktop.picker.error.sizeLimit": "Размер выбранных вложений превышает лимит {{limit}} МБ", + + "command.category.suggested": "Предложено", + "command.category.view": "Просмотр", + "command.category.project": "Проект", + "command.category.provider": "Провайдер", + "command.category.server": "Сервер", + "command.category.session": "Сессия", + "command.category.theme": "Тема", + "command.category.language": "Язык", + "command.category.file": "Файл", + "command.category.context": "Контекст", + "command.category.terminal": "Терминал", + "command.category.model": "Модель", + "command.category.mcp": "MCP", + "command.category.agent": "Агент", + "command.category.permissions": "Разрешения", + "command.category.workspace": "Рабочее пространство", + "command.category.settings": "Настройки", + "command.logs.export": "Экспортировать журналы", + + "theme.scheme.system": "Системная", + "theme.scheme.light": "Светлая", + "theme.scheme.dark": "Тёмная", + + "command.sidebar.toggle": "Переключить боковую панель", + "command.project.open": "Открыть проект", + "command.project.previous": "Предыдущий проект", + "command.project.next": "Следующий проект", + "command.project.index": "Переключиться на проект {{index}}", + "command.provider.connect": "Подключить провайдера", + "command.server.switch": "Переключить сервер", + "command.settings.open": "Открыть настройки", + "command.session.previous": "Предыдущая сессия", + "command.session.next": "Следующая сессия", + "command.session.previous.unseen": "Предыдущая непрочитанная сессия", + "command.session.next.unseen": "Следующая непрочитанная сессия", + "command.session.archive": "Архивировать сессию", + + "command.palette": "Палитра команд", + + "command.theme.cycle": "Переключить тему", + "command.theme.set": "Использовать тему: {{theme}}", + "command.theme.scheme.cycle": "Переключить цветовую схему", + "command.theme.scheme.set": "Использовать цветовую схему: {{scheme}}", + + "command.language.cycle": "Переключить язык", + "command.language.set": "Использовать язык: {{language}}", + + "command.session.new": "Новая сессия", + "command.file.open": "Открыть файл", + "command.tab.close": "Закрыть вкладку", + "command.tab.reopenClosed": "Повторно открыть закрытую вкладку", + "command.context.addSelection": "Добавить выделение в контекст", + "command.context.addSelection.description": "Добавить выбранные строки из текущего файла", + "command.input.focus": "Перейти к полю ввода", + "command.terminal.toggle": "Переключить терминал", + "command.fileTree.toggle": "Переключить дерево файлов", + "command.review.toggle": "Переключить обзор", + "command.terminal.new": "Новый терминал", + "command.terminal.new.description": "Создать новую вкладку терминала", + "command.steps.toggle": "Переключить шаги", + "command.steps.toggle.description": "Показать или скрыть шаги для текущего сообщения", + "command.message.previous": "Предыдущее сообщение", + "command.message.previous.description": "Перейти к предыдущему сообщению пользователя", + "command.message.next": "Следующее сообщение", + "command.message.next.description": "Перейти к следующему сообщению пользователя", + "command.model.choose": "Выбрать модель", + "command.model.choose.description": "Выбрать другую модель", + "command.mcp.toggle": "Переключить MCP", + "command.mcp.toggle.description": "Переключить MCP", + "command.agent.cycle": "Следующий агент", + "command.agent.cycle.description": "Переключиться к следующему агенту", + "command.agent.cycle.reverse": "Предыдущий агент", + "command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту", + "command.model.variant.cycle": "Переключить уровень усилий", + "command.model.variant.cycle.description": "Переключиться к следующему уровню усилий", + "command.prompt.mode.shell": "Оболочка", + "command.prompt.mode.normal": "Промпт", + "command.permissions.autoaccept.enable": "Автоматически принимать разрешения", + "command.permissions.autoaccept.disable": "Остановить автоматическое принятие разрешений", + "command.workspace.toggle": "Переключить рабочие пространства", + "command.workspace.toggle.description": "Включить или отключить несколько рабочих пространств в боковой панели", + "command.session.undo": "Отменить", + "command.session.undo.description": "Отменить последнее сообщение", + "command.session.redo": "Повторить", + "command.session.redo.description": "Повторить отменённое сообщение", + "command.session.compact": "Сжать сессию", + "command.session.compact.description": "Сократить сессию для уменьшения размера контекста", + "command.session.fork": "Создать ответвление", + "command.session.fork.description": "Создать новую сессию из сообщения", + "command.session.share": "Поделиться сессией", + "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": "Ничего не найдено", + "palette.group.commands": "Команды", + "palette.group.files": "Файлы", + + "dialog.provider.search.placeholder": "Поиск провайдеров", + "dialog.provider.empty": "Провайдеры не найдены", + "dialog.provider.group.popular": "Популярные", + "dialog.provider.group.other": "Другие", + "dialog.provider.custom.label": "Пользовательский провайдер, совместимый с OpenAI", + "dialog.provider.tag.recommended": "Рекомендуемые", + "dialog.provider.opencode.note": "Отобранные модели, включая Claude, GPT, Gemini и другие", + "dialog.provider.opencode.tagline": "Надёжные оптимизированные модели", + "dialog.provider.opencodeGo.tagline": "Доступная подписка для всех", + "dialog.provider.anthropic.note": "Прямой доступ к моделям Claude, включая Pro и Max", + "dialog.provider.copilot.note": "ИИ-модели для помощи в кодировании через GitHub Copilot", + "dialog.provider.openai.note": "Модели GPT для быстрых и мощных задач общего ИИ", + "dialog.provider.google.note": "Модели Gemini для быстрых и структурированных ответов", + "dialog.provider.openrouter.note": "Доступ ко всем поддерживаемым моделям через одного провайдера", + "dialog.provider.vercel.note": "Единый доступ к ИИ-моделям с умной маршрутизацией", + + "dialog.model.select.title": "Выбрать модель", + "dialog.model.search.placeholder": "Поиск моделей", + "dialog.model.empty": "Модели не найдены", + "dialog.model.manage": "Управление моделями", + "dialog.model.manage.description": "Настройте, какие модели отображаются при выборе модели", + "dialog.model.manage.provider.toggle": "Переключить все модели {{provider}}", + + "dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode", + "dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров", + "dialog.model.unpaid.viewMoreProviders": "Посмотреть ещё более 70 провайдеров", + + "dialog.provider.viewAll": "Показать больше провайдеров", + + "provider.connect.title": "Подключить {{provider}}", + "provider.connect.title.anthropicProMax": "Войти с помощью Claude Pro/Max", + "provider.connect.selectMethod": "Выберите способ входа для {{provider}}.", + "provider.connect.method.apiKey": "Ключ API", + "provider.connect.method.browser": "Браузер", + "provider.connect.method.headless": "Без браузера", + "provider.connect.status.inProgress": "Авторизация...", + "provider.connect.status.waiting": "Ожидание авторизации...", + "provider.connect.status.failed": "Ошибка авторизации: {{error}}", + "provider.connect.apiKey.description": + "Введите ключ API {{provider}}, чтобы подключить аккаунт и использовать модели {{provider}} в OpenCode.", + "provider.connect.apiKey.label": "Ключ API {{provider}}", + "provider.connect.apiKey.placeholder": "Ключ API", + "provider.connect.apiKey.required": "Требуется ключ API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen даёт вам доступ к отобранным надёжным оптимизированным моделям для агентов программирования.", + "provider.connect.opencodeZen.line2": + "С одним ключом API вы получите доступ к таким моделям, как Claude, GPT, Gemini, GLM и другие.", + "provider.connect.opencodeZen.visit.prefix": "Посетите ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": ", чтобы получить ключ API.", + "provider.connect.oauth.code.visit.prefix": "Посетите ", + "provider.connect.oauth.code.visit.link": "эту ссылку", + "provider.connect.oauth.code.visit.suffix": + " чтобы получить код авторизации для подключения аккаунта и использования моделей {{provider}} в OpenCode.", + "provider.connect.oauth.code.label": "{{method}} код авторизации", + "provider.connect.oauth.code.placeholder": "Код авторизации", + "provider.connect.oauth.code.required": "Код авторизации обязателен", + "provider.connect.oauth.code.invalid": "Неверный код авторизации", + "provider.connect.oauth.auto.visit.prefix": "Посетите ", + "provider.connect.oauth.auto.visit.link": "эту ссылку", + "provider.connect.oauth.auto.visit.suffix": + " и введите код ниже для подключения аккаунта и использования моделей {{provider}} в OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Код подтверждения", + "provider.connect.toast.connected.title": "{{provider}} подключён", + "provider.connect.toast.connected.description": "Модели {{provider}} теперь доступны.", + + "provider.custom.title": "Пользовательский провайдер", + "provider.custom.unavailable": "Пользовательские провайдеры недоступны на этом сервере", + "provider.custom.description.prefix": "Настройте провайдера, совместимого с OpenAI. См. ", + "provider.custom.description.link": "документацию по настройке провайдера", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID провайдера", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Строчные буквы, цифры, дефисы или подчёркивания", + "provider.custom.field.name.label": "Отображаемое имя", + "provider.custom.field.name.placeholder": "Мой ИИ-провайдер", + "provider.custom.field.baseURL.label": "Базовый URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "Ключ API", + "provider.custom.field.apiKey.placeholder": "Ключ API", + "provider.custom.field.apiKey.description": + "Необязательно. Оставьте пустым, если управляете авторизацией через заголовки.", + "provider.custom.models.label": "Модели", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Имя", + "provider.custom.models.name.placeholder": "Отображаемое имя", + "provider.custom.models.remove": "Удалить модель", + "provider.custom.models.add": "Добавить модель", + "provider.custom.headers.label": "Заголовки (необязательно)", + "provider.custom.headers.key.label": "Заголовок", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Значение", + "provider.custom.headers.value.placeholder": "значение", + "provider.custom.headers.remove": "Удалить заголовок", + "provider.custom.headers.add": "Добавить заголовок", + "provider.custom.error.providerID.required": "Требуется ID провайдера", + "provider.custom.error.providerID.format": "Используйте строчные буквы, цифры, дефисы или подчёркивания", + "provider.custom.error.providerID.exists": "Такой ID провайдера уже существует", + "provider.custom.error.name.required": "Требуется отображаемое имя", + "provider.custom.error.baseURL.required": "Требуется базовый URL", + "provider.custom.error.baseURL.format": "Должен начинаться с http:// или https://", + "provider.custom.error.required": "Обязательно", + "provider.custom.error.duplicate": "Дубликат", + + "provider.disconnect.toast.disconnected.title": "{{provider}} отключён", + "provider.disconnect.toast.disconnected.description": "Модели {{provider}} больше недоступны.", + "model.tag.free": "Бесплатно", + "model.tag.latest": "Последняя", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "текст", + "model.input.image": "изображение", + "model.input.audio": "аудио", + "model.input.video": "видео", + "model.input.pdf": "PDF", + "model.tooltip.allows": "Поддерживает: {{inputs}}", + "model.tooltip.context.label": "Контекст", + "model.tooltip.inputs": "Входные данные", + "model.tooltip.model": "Модель", + "model.tooltip.provider": "Провайдер", + "model.tooltip.reasoning": "Рассуждение", + "model.tooltip.reasoning.allowed": "Поддерживает рассуждения", + "model.tooltip.reasoning.none": "Без рассуждений", + "model.tooltip.context": "Лимит контекста {{limit}}", + + "common.search.placeholder": "Поиск", + "common.goBack": "Назад", + "common.goForward": "Вперёд", + "common.loading": "Загрузка", + "common.loading.ellipsis": "...", + "common.cancel": "Отмена", + "common.clear": "Очистить", + "common.connect": "Подключить", + "common.disconnect": "Отключить", + "common.continue": "Продолжить", + "common.submit": "Отправить", + "common.save": "Сохранить", + "common.saving": "Сохранение...", + "common.default": "По умолчанию", + "common.attachment": "вложение", + + "prompt.placeholder.shell": "Введите команду оболочки... {{example}}", + "prompt.placeholder.normal": 'Спросите что угодно... "{{example}}"', + "prompt.placeholder.simple": "Спросите что угодно...", + "prompt.placeholder.summarizeComments": "Обобщить комментарии…", + "prompt.placeholder.summarizeComment": "Обобщить комментарий…", + "prompt.mode.shell": "Оболочка", + "prompt.mode.normal": "Промпт", + "prompt.mode.shell.exit": "esc для выхода", + "session.child.promptDisabled": "В сессиях субагентов нельзя отправлять запросы.", + "session.child.backToParent": "Вернуться к основной сессии.", + + "prompt.example.1": "Исправить TODO в коде", + "prompt.example.2": "Какой технологический стек этого проекта?", + "prompt.example.3": "Исправить сломанные тесты", + "prompt.example.4": "Объясни, как работает аутентификация", + "prompt.example.5": "Найти и исправить уязвимости безопасности", + "prompt.example.6": "Добавить модульные тесты для службы пользователей", + "prompt.example.7": "Рефакторить эту функцию для лучшей читаемости", + "prompt.example.8": "Что означает эта ошибка?", + "prompt.example.9": "Помоги мне отладить эту проблему", + "prompt.example.10": "Сгенерировать документацию API", + "prompt.example.11": "Оптимизировать запросы к базе данных", + "prompt.example.12": "Добавить проверку входных данных", + "prompt.example.13": "Создать новый компонент для...", + "prompt.example.14": "Как развернуть этот проект?", + "prompt.example.15": "Проверь, соответствует ли мой код рекомендациям", + "prompt.example.16": "Добавить обработку ошибок в эту функцию", + "prompt.example.17": "Объясни это регулярное выражение", + "prompt.example.18": "Преобразовать это в TypeScript", + "prompt.example.19": "Добавить логирование по всему проекту", + "prompt.example.20": "Какие зависимости устарели?", + "prompt.example.21": "Помоги написать скрипт миграции", + "prompt.example.22": "Реализовать кеширование для этой конечной точки", + "prompt.example.23": "Добавить пагинацию в этот список", + "prompt.example.24": "Создать CLI-команду для...", + "prompt.example.25": "Как здесь работают переменные окружения?", + + "prompt.popover.emptyResults": "Нет совпадений", + "prompt.popover.emptyCommands": "Нет совпадающих команд", + "prompt.dropzone.label": "Перетащите сюда изображения, PDF или текстовые файлы", + "prompt.dropzone.file.label": "Отпустите, чтобы упомянуть файл через @", + "prompt.slash.badge.custom": "своё", + "prompt.slash.badge.skill": "навык", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "активно", + "prompt.context.includeActiveFile": "Включить активный файл", + "prompt.context.removeActiveFile": "Удалить активный файл из контекста", + "prompt.context.removeFile": "Удалить файл из контекста", + "prompt.action.attachFile": "Прикрепить файл", + "prompt.menu.addImagesAndFiles": "Добавить файлы и другое", + "prompt.menu.imagesAndFiles": "Изображения и файлы", + "prompt.menu.commands": "Команды", + "prompt.menu.context": "Контекст", + "prompt.menu.shellCommand": "Команда оболочки", + "prompt.attachment.remove": "Удалить вложение", + "prompt.action.send": "Отправить", + "prompt.action.stop": "Остановить", + + "prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение", + "prompt.toast.attachmentDuplicate.title": "Этот файл уже загружен", + "prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.", + "prompt.toast.modelAgentRequired.title": "Выберите агента и модель", + "prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.", + "prompt.toast.worktreeCreateFailed.title": "Не удалось создать worktree", + "prompt.toast.sessionCreateFailed.title": "Не удалось создать сессию", + "prompt.toast.shellSendFailed.title": "Не удалось отправить команду оболочки", + "prompt.toast.commandSendFailed.title": "Не удалось отправить команду", + "prompt.toast.promptSendFailed.title": "Не удалось отправить запрос", + "prompt.toast.promptSendFailed.description": "Не удалось получить сессию", + + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{enabled}} из {{total}} включено", + "dialog.mcp.empty": "MCP не настроены", + + "dialog.lsp.empty": "LSP автоматически обнаружены по типам файлов", + "dialog.plugins.empty": "Плагины настроены в opencode.json", + + "mcp.status.connected": "подключено", + "mcp.status.failed": "ошибка", + "mcp.status.needs_auth": "требуется авторизация", + "mcp.auth.clickToAuthenticate": "Нажмите, чтобы авторизоваться", + "mcp.status.disabled": "отключено", + + "dialog.fork.empty": "Нет сообщений для ответвления", + + "dialog.directory.search.placeholder": "Поиск папок", + "dialog.directory.empty": "Папки не найдены", + "dialog.directory.action.selectFile": "Выбрать файл", + "dialog.directory.action.selectFolder": "Выбрать папку", + "dialog.directory.root": "Корень", + "dialog.directory.parent": "Родительская папка", + "dialog.directory.readError": "Не удалось прочитать эту папку", + + "dialog.server.title": "Серверы", + "dialog.server.description": "Выберите сервер OpenCode, к которому подключается приложение.", + "dialog.server.search.placeholder": "Поиск серверов", + "dialog.server.empty": "Серверов пока нет", + "dialog.server.add.title": "Добавить сервер", + "dialog.server.add.url": "URL сервера", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Не удалось подключиться к серверу", + "dialog.server.add.checking": "Проверка...", + "dialog.server.add.button": "Добавить сервер", + "dialog.server.add.name": "Имя сервера (необязательно)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Имя пользователя (необязательно)", + "dialog.server.add.password": "Пароль (необязательно)", + "dialog.server.edit.title": "Редактировать сервер", + "dialog.server.default.title": "Сервер по умолчанию", + "dialog.server.default.description": + "Подключаться к этому серверу при запуске приложения вместо запуска локального сервера. Требуется перезапуск.", + "dialog.server.default.none": "Сервер не выбран", + "dialog.server.default.set": "Установить текущий сервер по умолчанию", + "dialog.server.default.clear": "Очистить", + "dialog.server.action.remove": "Удалить сервер", + + "dialog.server.menu.edit": "Редактировать", + "dialog.server.menu.default": "Сделать по умолчанию", + "dialog.server.menu.defaultRemove": "Отменить выбор по умолчанию", + "dialog.server.menu.delete": "Удалить", + "dialog.server.current": "Текущий сервер", + "dialog.server.status.default": "По умолч.", + "wsl.server.add": "Добавить сервер WSL", + "wsl.server.addShort": "Добавить WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "Сервер WSL", + "wsl.server.retryStart": "Повторить запуск", + "wsl.server.updating": "Обновление...", + "wsl.onboarding.step.distro": "Выбрать дистрибутив", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Проверка WSL...", + "wsl.onboarding.restartRequired": "Чтобы завершить установку WSL, перезапустите Windows.", + "wsl.onboarding.ready": "WSL готова к работе.", + "wsl.onboarding.required": "Для продолжения требуется WSL.", + "wsl.onboarding.checkingDistros": "Проверка дистрибутивов...", + "wsl.onboarding.installingDistro": "Установка {{distro}}...", + "wsl.onboarding.checkingDistro": "Проверка {{distro}}...", + "wsl.onboarding.listingDistros": "Получение списка дистрибутивов...", + "wsl.onboarding.distroReady": "Дистрибутив {{distro}} готов к работе.", + "wsl.onboarding.distroNotInstalled": "Дистрибутив {{distro}} пока не установлен.", + "wsl.onboarding.openDistroOnce": "Откройте {{distro}} один раз, чтобы завершить настройку.", + "wsl.onboarding.finishingDistro": "Завершение настройки {{distro}}.", + "wsl.onboarding.pickDistro": "Выберите дистрибутив или установите его ниже.", + "wsl.onboarding.checkingOpencode": "Проверка OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Проверка OpenCode в {{distro}}...", + "wsl.onboarding.updatingOpencode": "Обновление OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Обновление OpenCode в {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Обновите OpenCode в {{distro}}.", + "wsl.onboarding.updateOpencode": "Обновить OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode в {{distro}} готов к работе.", + "wsl.onboarding.opencodeReady": "OpenCode готов к работе.", + "wsl.onboarding.installOpencodeIn": "Установите OpenCode в {{distro}}.", + "wsl.onboarding.installOpencode": "Установить OpenCode", + "wsl.onboarding.chooseDistroFirst": "Сначала выберите дистрибутив.", + "wsl.onboarding.loadFailed": "Не удалось загрузить состояние WSL.", + "wsl.onboarding.loading": "Загрузка...", + "wsl.onboarding.installedDistros": "Установленные дистрибутивы", + "wsl.onboarding.checkAgain": "Проверить снова", + "wsl.onboarding.distroStatus.ready": "Готов", + "wsl.onboarding.distroStatus.checking": "Проверка...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode не установлен", + "wsl.onboarding.distroStatus.missingTools": "Отсутствуют bash и curl", + "wsl.onboarding.distroStatus.unsupported": "Не поддерживается · Используйте WSL 2", + "wsl.onboarding.needAnotherDistro": "Нужен другой дистрибутив?", + "wsl.onboarding.needAnotherDistroHint": "Установите дистрибутив Linux из каталога WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL не установлена", + "wsl.onboarding.wslNotInstalled.description": + "Для добавления сервера WSL в OpenCode требуется WSL (Подсистема Windows для Linux)", + "wsl.onboarding.wslUnavailable.title": "WSL недоступна", + "wsl.onboarding.wslUnavailable.description": "OpenCode не смог проверить WSL на этом компьютере.", + "wsl.onboarding.installWsl": "Установить WSL", + "wsl.onboarding.windowsRestartRequired": + "Перезапустите Windows, чтобы завершить установку WSL, затем снова откройте OpenCode.", + "wsl.onboarding.next": "Далее", + "wsl.onboarding.refresh": "Обновить", + "wsl.onboarding.allDistrosAdded": "Все установленные дистрибутивы уже добавлены.", + "wsl.onboarding.noDistros": "Дистрибутивы пока не обнаружены.", + "wsl.onboarding.install": "Установить", + "wsl.onboarding.installing": "Установка...", + "wsl.onboarding.installDistro": "Установить дистрибутив", + "wsl.onboarding.searchDistros": "Поиск дистрибутивов", + "wsl.onboarding.wsl2Required": "Требуется WSL 2.", + "wsl.onboarding.toolsRequired": "Для этого дистрибутива нужны bash и curl.", + "wsl.onboarding.openTerminal": "Открыть терминал", + "wsl.onboarding.path": "Путь: {{path}}", + "wsl.onboarding.notFound": "не найден", + "wsl.onboarding.version": "Версия: {{version}}", + "wsl.onboarding.unknown": "неизвестно", + "wsl.onboarding.desktopVersion": "приложение {{version}}", + "wsl.onboarding.versionMismatch": "Установленная версия не соответствует версии приложения.", + "wsl.onboarding.adding": "Добавление...", + + "help.tabs.toast.ariaLabel": "Представляем вкладки. Организуйте работу и активные сессии с помощью вкладок", + "help.tabs.toast.dismiss": "Закрыть сведения о вкладках", + "help.tabs.title": "Представляем вкладки", + "help.tabs.description": "Организуйте работу и активные сессии с помощью вкладок", + "help.tabs.date": "14 июля", + "help.tabs.introduction": "Теперь работа в OpenCode Desktop организована с помощью вкладок.", + "help.tabs.sessions": + "Начните новую сессию во вкладке или откройте существующую сессию из любого проекта. Открывайте новую вкладку, когда начинаете новую задачу, и закрывайте её по завершении.", + "help.tabs.organize": + "Несколько открытых вкладок упрощают организацию активных сессий. Если вы планируете оставить вкладки открытыми, присвойте им запоминающиеся названия.", + "help.tabs.home": + "Все сессии и проекты находятся на новом экране «Главная». При выборе сессии она открывается во вкладке.", + "help.tabs.persistence": "После повторного открытия приложения вкладки останутся открытыми.", + "help.tabs.worktrees": + "Новый дизайн пока не поддерживает Git Worktrees, но эта возможность скоро появится. Если вы предпочитаете прежний макет, переключиться между макетами можно в разделе «Настройки». Учтите, что через несколько недель новый макет станет постоянным.", + + "dialog.project.edit.title": "Редактировать проект", + "dialog.project.edit.name": "Название", + "dialog.project.edit.icon": "Значок", + "dialog.project.edit.icon.alt": "Значок проекта", + "dialog.project.edit.icon.hint": "Нажмите или перетащите изображение", + "dialog.project.edit.icon.recommended": "Рекомендуется: 128 x 128 пикселей", + "dialog.project.edit.color": "Цвет", + "dialog.project.edit.color.select": "Выбрать цвет {{color}}", + + "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": + 'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.', + "context.breakdown.system": "Система", + "context.breakdown.user": "Пользователь", + "context.breakdown.assistant": "Ассистент", + "context.breakdown.tool": "Вызовы инструментов", + "context.breakdown.other": "Другое", + + "context.systemPrompt.title": "Системный промпт", + "context.rawMessages.title": "Исходные сообщения", + + "context.export.session": "Экспортировать сессию", + + "context.stats.session": "Сессия", + "context.stats.messages": "Сообщения", + "context.stats.provider": "Провайдер", + "context.stats.model": "Модель", + "context.stats.limit": "Лимит контекста", + "context.stats.totalTokens": "Всего токенов", + "context.stats.usage": "Использование", + "context.stats.inputTokens": "Входные токены", + "context.stats.outputTokens": "Выходные токены", + "context.stats.reasoningTokens": "Токены рассуждения", + "context.stats.cacheTokens": "Токены кеша (чтение/запись)", + "context.stats.userMessages": "Сообщения пользователя", + "context.stats.assistantMessages": "Сообщения ассистента", + "context.stats.totalCost": "Общая стоимость", + "context.stats.sessionCreated": "Сессия создана", + "context.stats.lastActivity": "Последняя активность", + + "context.usage.tokens": "Токены", + "context.usage.usage": "Использование", + "context.usage.cost": "Стоимость", + "context.usage.clickToView": "Нажмите для просмотра контекста", + "context.usage.view": "Показать использование контекста", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "Язык", + "toast.language.description": "Переключено на {{language}}", + + "toast.theme.title": "Тема переключена", + "toast.scheme.title": "Цветовая схема", + + "toast.permissions.autoaccept.on.title": "Разрешения принимаются автоматически", + "toast.permissions.autoaccept.on.description": "Запросы на разрешения будут одобряться автоматически", + "toast.permissions.autoaccept.off.title": "Автоматическое принятие разрешений остановлено", + "toast.permissions.autoaccept.off.description": "Запросы на разрешения будут требовать одобрения", + + "toast.workspace.enabled.title": "Рабочие пространства включены", + "toast.workspace.enabled.description": "В боковой панели теперь отображаются несколько рабочих деревьев", + "toast.workspace.disabled.title": "Рабочие пространства отключены", + "toast.workspace.disabled.description": "В боковой панели отображается только главное рабочее дерево", + + "toast.model.none.title": "Модель не выбрана", + "toast.model.none.description": "Подключите провайдера для суммаризации сессии", + + "toast.file.loadFailed.title": "Не удалось загрузить файл", + + "toast.file.listFailed.title": "Не удалось получить список файлов", + "toast.context.noLineSelection.title": "Нет выделения строк", + "toast.context.noLineSelection.description": "Сначала выберите диапазон строк во вкладке файла.", + "toast.session.share.copyFailed.title": "Не удалось скопировать URL в буфер обмена", + "toast.session.share.success.title": "Сессия опубликована", + "toast.session.share.success.description": "URL скопирован в буфер обмена!", + "toast.session.share.failed.title": "Не удалось опубликовать сессию", + "toast.session.share.failed.description": "Произошла ошибка при публикации сессии", + + "toast.session.unshare.success.title": "Публикация отменена", + "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}}) доступна для установки.", + "toast.update.action.installRestart": "Установить и перезапустить", + "toast.update.action.notYet": "Пока нет", + + "error.page.title": "Что-то пошло не так", + "error.page.description": "Произошла ошибка при загрузке приложения.", + "error.page.description.localServerStartup": "Произошла ошибка при запуске локального сервера.", + "error.page.details.label": "Детали ошибки", + "error.page.action.restart": "Перезапустить", + "error.page.action.report": "Сообщить об ошибке", + "error.page.action.reported": "Об ошибке сообщено", + "error.page.action.exportLogs": "Экспортировать журналы", + "error.page.action.checking": "Проверка...", + "error.page.action.checkUpdates": "Проверить обновления", + "error.page.action.updateTo": "Обновить до {{version}}", + "error.page.report.prefix": "Пожалуйста, сообщите об этой ошибке команде OpenCode", + "error.page.report.discord": "в Discord", + "error.page.version": "Версия: {{version}}", + + "error.dev.rootNotFound": + "Корневой элемент не найден. Вы забыли добавить его в index.html? Или, может быть, атрибут id был написан неправильно?", + + "error.serverSync.connectFailed": "Не удалось подключиться к серверу. Запущен ли сервер по адресу `{{url}}`?", + "directory.error.invalidUrl": "Недопустимая директория в URL.", + + "error.chain.unknown": "Неизвестная ошибка", + "error.chain.causedBy": "Причина:", + "error.chain.apiError": "Ошибка API", + "error.chain.status": "Статус: {{status}}", + "error.chain.retryable": "Можно повторить: {{retryable}}", + "error.chain.responseBody": "Тело ответа:\n{{body}}", + "error.chain.didYouMean": "Возможно, вы имели в виду: {{suggestions}}", + "error.chain.modelNotFound": "Модель не найдена: {{provider}}/{{model}}", + "error.chain.checkConfig": "Проверьте названия провайдера и модели в конфигурации (opencode.json)", + "error.chain.mcpFailed": + 'MCP-сервер "{{name}}" завершился с ошибкой. Обратите внимание: OpenCode пока не поддерживает MCP-авторизацию.', + "error.chain.providerAuthFailed": "Ошибка аутентификации провайдера ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Не удалось инициализировать провайдера "{{provider}}". Проверьте учётные данные и конфигурацию.', + "error.chain.configJsonInvalid": "Конфигурационный файл по адресу {{path}} содержит некорректный JSON(C)", + "error.chain.configJsonInvalidWithMessage": + "Конфигурационный файл по адресу {{path}} содержит некорректный JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Папка "{{dir}}" в {{path}} недопустима. Переименуйте папку в "{{suggestion}}" или удалите её. Это распространённая опечатка.', + "error.chain.configFrontmatterError": "Не удалось разобрать frontmatter в {{path}}:\n{{message}}", + "error.chain.configInvalid": "Недопустимый конфигурационный файл по адресу {{path}}", + "error.chain.configInvalidWithMessage": "Недопустимый конфигурационный файл по адресу {{path}}: {{message}}", + + "notification.permission.title": "Требуется разрешение", + "notification.permission.description": "Для {{sessionTitle}} в {{projectName}} требуется разрешение", + "notification.question.title": "Вопрос", + "notification.question.description": "У {{sessionTitle}} в {{projectName}} есть вопрос", + "notification.action.goToSession": "Перейти к сессии", + + "notification.session.responseReady.title": "Ответ готов", + "notification.session.error.title": "Ошибка сессии", + "notification.session.error.fallbackDescription": "Произошла ошибка", + + "home.recentProjects": "Недавние проекты", + "home.empty.title": "Нет недавних проектов", + "home.empty.description": "Начните с открытия локального проекта", + "home.title": "Главная", + "home.projects": "Проекты", + "home.project.add": "Добавить проект", + "home.recentlyClosed": "Недавно закрытые", + "home.server.collapse": "Свернуть проекты сервера", + "home.server.expand": "Развернуть проекты сервера", + "home.sessions.search.placeholder": "Поиск сессий", + "home.sessions.search.placeholder.scoped": "Поиск сессий в {{scope}}", + "home.sessions.search.sessions": "Сессии", + "home.sessions.search.noResults": "Сессии по запросу {{query}} не найдены", + "home.sessions.empty": "Здесь пока ничего нет", + "home.sessions.empty.description": "Создайте сессию, чтобы начать", + "home.sessions.group.today": "Сегодня", + "home.sessions.group.yesterday": "Вчера", + "home.sessions.group.older": "Ранее", + "home.providerTip": + "Подключитесь к более чем 75 провайдерам, чтобы использовать другие модели, включая Claude, GPT, Gemini и другие", + + "session.tab.session": "Сессия", + "session.tab.review": "Обзор", + "session.tab.context": "Контекст", + "session.tab.unknown": "Неизвестная сессия", + "session.panel.reviewAndFiles": "Обзор и файлы", + "session.error.notFound": "Эта сессия не найдена", + "session.error.notFound.description": "Эта вкладка указывает на сессию, которой больше нет на этом сервере.", + "session.error.notFound.closeTab": "Закрыть вкладку", + "session.error.serverConnection": "Не удаётся подключиться к этому серверу", + "session.review.filesChanged": "Изменено файлов: {{count}}", + "session.review.change.one": "Изменение", + "session.review.change.other": "Изменения", + "session.review.loadingChanges": "Загрузка изменений...", + "session.review.empty": "Изменений в этой сессии пока нет", + "session.review.noVcs": "Система контроля версий Git не обнаружена, изменения не отображаются", + "session.review.noSnapshot": "Отслеживание снимков отключено в настройках, поэтому изменения сессии недоступны", + "session.review.noChanges": "Нет изменений", + "session.review.noUncommittedChanges": "Пока нет незафиксированных изменений", + "session.review.noBranchChanges": "Пока нет изменений в ветке", + "session.files.selectToOpen": "Выберите файл, чтобы открыть", + "session.files.all": "Все файлы", + "session.files.empty": "Нет файлов", + "session.files.binaryContent": "Двоичный файл (содержимое не может быть отображено)", + "session.messages.renderEarlier": "Показать предыдущие сообщения", + "session.messages.loadingEarlier": "Загрузка предыдущих сообщений...", + "session.messages.loadEarlier": "Загрузить предыдущие сообщения", + "session.messages.loading": "Загрузка сообщений...", + "session.messages.jumpToLatest": "Перейти к последнему", + + "session.context.addToContext": "Добавить {{selection}} в контекст", + "session.todo.title": "Задачи", + "session.todo.collapse": "Свернуть", + "session.todo.expand": "Развернуть", + "session.question.minimize": "Свернуть вопрос", + "session.question.restore": "Восстановить вопрос", + "session.question.pending.one": "{{count}} вопрос без ответа", + "session.question.pending.few": "{{count}} вопроса без ответа", + "session.question.pending.many": "{{count}} вопросов без ответа", + "session.question.pending.other": "Вопросы без ответа: {{count}}", + "session.followupDock.summary.one": "{{count}} сообщение в очереди", + "session.followupDock.summary.few": "{{count}} сообщения в очереди", + "session.followupDock.summary.many": "{{count}} сообщений в очереди", + "session.followupDock.summary.other": "Сообщений в очереди: {{count}}", + "session.followupDock.sendNow": "Отправить сейчас", + "session.followupDock.edit": "Редактировать", + "session.followupDock.collapse": "Свернуть сообщения в очереди", + "session.followupDock.expand": "Развернуть сообщения в очереди", + "session.revertDock.summary.one": "{{count}} сообщение возвращено", + "session.revertDock.summary.few": "{{count}} сообщения возвращены", + "session.revertDock.summary.many": "{{count}} сообщений возвращено", + "session.revertDock.summary.other": "Возвращено сообщений: {{count}}", + "session.revertDock.collapse": "Свернуть возвращённые сообщения", + "session.revertDock.expand": "Развернуть возвращённые сообщения", + "session.revertDock.restore": "Восстановить сообщение", + + "session.new.title": "Создавайте что угодно", + "session.new.project.new": "Новый проект", + "session.new.project.search": "Поиск проектов", + "session.new.project.add": "Добавить проект", + "session.new.worktree.main": "Основная ветка", + "session.new.worktree.mainWithBranch": "Основная ветка ({{branch}})", + "session.new.worktree.create": "Создать новый worktree", + "session.new.workspace.runIn": "Запустить сессию в", + "session.new.workspace.triggerLocal": "Локально", + "session.new.workspace.local": "Локальный репозиторий", + "session.new.workspace.existing": "Рабочее пространство…", + "session.new.git.none": "Нет Git", + "session.new.lastModified": "Последнее изменение", + + "session.header.search.placeholder": "Поиск {{project}}", + "session.header.searchFiles": "Поиск файлов", + "session.header.openIn": "Открыть в", + "session.header.open.action": "Открыть {{app}}", + "session.header.open.ariaLabel": "Открыть в {{app}}", + "session.header.open.menu": "Варианты открытия", + "session.header.open.copyPath": "Копировать путь", + + "status.popover.trigger": "Статус", + "status.popover.ariaLabel": "Настройки серверов", + "status.popover.tab.servers": "Серверы", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Плагины", + "status.popover.action.manageServers": "Управлять серверами", + + "session.share.popover.title": "Опубликовать в интернете", + "session.share.popover.description.shared": + "Эта сессия общедоступна. Доступ к ней может получить любой, у кого есть ссылка.", + "session.share.popover.description.unshared": + "Опубликуйте сессию в интернете. Доступ к ней сможет получить любой, у кого есть ссылка.", + "session.share.action.share": "Поделиться", + "session.share.action.publish": "Опубликовать", + "session.share.action.publishing": "Публикация...", + "session.share.action.unpublish": "Отменить публикацию", + "session.share.action.unpublishing": "Отмена публикации...", + "session.share.action.view": "Посмотреть", + "session.share.copy.copied": "Скопировано", + "session.share.copy.copyLink": "Копировать ссылку", + + "lsp.tooltip.none": "Нет LSP-серверов", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Загрузка запроса...", + "terminal.loading": "Загрузка терминала...", + "terminal.title": "Терминал", + "terminal.title.numbered": "Терминал {{number}}", + "terminal.close": "Закрыть терминал", + "terminal.connectionLost.title": "Соединение потеряно", + "terminal.connectionLost.description": + "Соединение с терминалом прервано. Это может произойти при перезапуске сервера.", + "terminal.connectTicket.csrfError": + "Билет подключения PTY отклонён при проверке источника или CSRF. Проверьте конфигурацию CORS сервера.", + "terminal.connectTicket.statusError": "Не удалось получить билет подключения PTY, статус: {{status}}", + + "titlebar.update": "Обновить", + "titlebar.updateVersion": "Обновить {{version}}", + + "common.closeTab": "Закрыть вкладку", + "common.dismiss": "Закрыть", + "common.requestFailed": "Запрос не выполнен", + "common.moreOptions": "Дополнительные опции", + "common.learnMore": "Подробнее", + "common.rename": "Переименовать", + "common.export": "Экспортировать", + "common.reset": "Сбросить", + "common.archive": "Архивировать", + "common.delete": "Удалить", + "common.close": "Закрыть", + "common.edit": "Редактировать", + "common.loadMore": "Загрузить ещё", + "common.key.esc": "ESC", + + "sidebar.menu.toggle": "Переключить меню", + "sidebar.nav.projectsAndSessions": "Проекты и сессии", + "sidebar.settings": "Настройки", + "sidebar.help": "Помощь", + "sidebar.workspaces.enable": "Включить рабочие пространства", + "sidebar.workspaces.disable": "Отключить рабочие пространства", + "sidebar.gettingStarted.title": "Начало работы", + "sidebar.gettingStarted.line1": "OpenCode включает бесплатные модели, чтобы вы могли начать сразу.", + "sidebar.gettingStarted.line2": + "Подключите любого провайдера для использования моделей, включая Claude, GPT, Gemini и др.", + "sidebar.project.recentSessions": "Недавние сессии", + "sidebar.project.viewAllSessions": "Посмотреть все сессии", + "sidebar.project.clearNotifications": "Очистить уведомления", + "sidebar.empty.title": "Нет открытых проектов", + "sidebar.empty.description": "Откройте проект, чтобы начать", + + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Приложение", + "settings.section.server": "Сервер", + "settings.tab.general": "Основные", + "settings.tab.shortcuts": "Горячие клавиши", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Интеграция с WSL", + "settings.desktop.wsl.description": "Запускать сервер OpenCode внутри WSL на Windows.", + + "settings.general.section.appearance": "Внешний вид", + "settings.general.section.advanced": "Дополнительно", + "settings.general.section.notifications": "Системные уведомления", + "settings.general.section.updates": "Обновления", + "settings.general.section.sounds": "Звуковые эффекты", + "settings.general.section.feed": "Лента", + "settings.general.section.display": "Экран", + + "settings.general.row.language.title": "Язык", + "settings.general.row.language.description": "Изменить язык отображения OpenCode", + "settings.general.row.shell.title": "Оболочка терминала", + "settings.general.row.shell.description": + "Выберите оболочку для терминала. Совместимые оболочки также используются агентом при вызове инструментов.", + "settings.general.row.shell.autoDefault": "Авто (по умолчанию)", + "settings.general.row.shell.terminalOnly": "только терминал", + "settings.general.row.appearance.title": "Внешний вид", + "settings.general.row.appearance.description": "Настройте внешний вид OpenCode на вашем устройстве", + "settings.general.row.colorScheme.title": "Цветовая схема", + "settings.general.row.colorScheme.description": "Выберите, следует ли OpenCode системной, светлой или тёмной теме", + "settings.general.row.theme.title": "Тема", + "settings.general.row.theme.description": "Настройте оформление OpenCode.", + "settings.general.row.font.title": "Шрифт кода", + "settings.general.row.font.description": "Настройте шрифт, используемый в блоках кода", + "settings.general.row.terminalFont.title": "Шрифт терминала", + "settings.general.row.terminalFont.description": "Настройте шрифт, используемый в терминале", + "settings.general.row.uiFont.title": "Шрифт интерфейса", + "settings.general.row.uiFont.description": "Настройте шрифт, используемый во всём интерфейсе", + "settings.general.row.followup.title": "Поведение уточняющих вопросов", + "settings.general.row.followup.description": + "Выберите, отправлять ли уточняющие вопросы сразу или помещать их в очередь", + "settings.general.row.followup.option.queue": "Очередь", + "settings.general.row.followup.option.steer": "Направлять", + "settings.general.row.showFileTree.title": "Дерево файлов", + "settings.general.row.showFileTree.description": "Показывать панель дерева файлов в сессиях", + "settings.general.row.showNavigation.title": "Элементы навигации", + "settings.general.row.showNavigation.description": + "Показывать кнопки «Назад» и «Вперёд» в строке заголовка приложения", + "settings.general.row.showSearch.title": "Палитра команд", + "settings.general.row.showSearch.description": "Показывать кнопку поиска и палитры команд в строке заголовка", + "settings.general.row.showTerminal.title": "Терминал", + "settings.general.row.showTerminal.description": "Показывать кнопку терминала в строке заголовка приложения", + "settings.general.row.showStatus.title": "Состояние сервера", + "settings.general.row.showStatus.description": "Показывать кнопку состояния сервера в строке заголовка", + "settings.general.row.mobileTitlebarBottom.title": "Нижняя навигация", + "settings.general.row.mobileTitlebarBottom.description": + "На мобильных устройствах размещать строку заголовка и вкладки сессии внизу экрана", + "settings.general.row.showCustomAgents.title": "Выбор агента", + "settings.general.row.showCustomAgents.description": + "Показывать переключатель агентов в редакторе запросов. Если он скрыт, по умолчанию используется агент Build.", + "settings.general.row.reasoningSummaries.title": "Показывать сводки рассуждений", + "settings.general.row.reasoningSummaries.description": "Отображать сводки рассуждений модели в ленте", + + "settings.general.row.shellToolPartsExpanded.title": "Разворачивать элементы инструмента shell", + "settings.general.row.shellToolPartsExpanded.description": + "Показывать элементы инструмента shell в ленте развернутыми по умолчанию", + "settings.general.row.editToolPartsExpanded.title": "Разворачивать элементы инструмента edit", + "settings.general.row.editToolPartsExpanded.description": + "Показывать элементы инструментов edit, write и patch в ленте развернутыми по умолчанию", + "settings.general.row.newInterface.title": "Новая компоновка", + "settings.general.row.newInterface.badge": "Новое", + "settings.general.row.newInterface.description": + "Используйте новые вкладки и компоновку главной страницы. В течение ограниченного времени можно переключаться между вариантами компоновки.", + "settings.general.row.newInterfaceNotice.title": "Теперь вы используете новую компоновку", + "settings.general.row.newInterfaceNotice.description": "Прежняя компоновка больше недоступна", + "settings.general.row.newInterfaceNotice.dismiss": "Закрыть", + "settings.general.row.pinchZoom.title": "Масштабирование сведением пальцев", + "settings.general.row.pinchZoom.description": + "Разрешить масштабирование сведением пальцев на трекпаде и прокруткой с нажатой клавишей Ctrl", + "settings.general.row.wayland.title": "Использовать нативный Wayland", + "settings.general.row.wayland.description": "Отключить резервный режим X11 в Wayland. Требуется перезапуск.", + "settings.general.row.wayland.tooltip": + "На Linux с мониторами разной частоты обновления нативный Wayland может быть стабильнее.", + + "settings.general.row.releaseNotes.title": "Примечания к выпуску", + "settings.general.row.releaseNotes.description": 'Показывать всплывающие окна "Что нового" после обновлений', + + "settings.updates.row.startup.title": "Проверять обновления при запуске", + "settings.updates.row.startup.description": "Автоматически проверять обновления при запуске OpenCode", + "settings.updates.row.check.title": "Проверить обновления", + "settings.updates.row.check.description": "Проверить обновления вручную и установить, если доступны", + "settings.updates.action.checkNow": "Проверить сейчас", + "settings.updates.action.checking": "Проверка...", + "settings.updates.action.downloading": "Загрузка...", + "settings.updates.action.installing": "Установка...", + "settings.updates.toast.latest.title": "У вас последняя версия", + "settings.updates.toast.latest.description": "Вы используете последнюю версию OpenCode.", + "sound.option.none": "Нет", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + + "settings.general.notifications.agent.title": "Агент", + "settings.general.notifications.agent.description": + "Показывать системное уведомление, когда агент завершил работу или требует внимания", + "settings.general.notifications.permissions.title": "Разрешения", + "settings.general.notifications.permissions.description": + "Показывать системное уведомление, когда требуется разрешение", + "settings.general.notifications.errors.title": "Ошибки", + "settings.general.notifications.errors.description": "Показывать системное уведомление, когда происходит ошибка", + + "settings.general.sounds.agent.title": "Агент", + "settings.general.sounds.agent.description": "Воспроизводить звук, когда агент завершил работу или требует внимания", + "settings.general.sounds.permissions.title": "Разрешения", + "settings.general.sounds.permissions.description": "Воспроизводить звук, когда требуется разрешение", + "settings.general.sounds.errors.title": "Ошибки", + "settings.general.sounds.errors.description": "Воспроизводить звук, когда происходит ошибка", + + "settings.shortcuts.title": "Горячие клавиши", + "settings.shortcuts.reset.button": "Восстановить настройки по умолчанию", + "settings.shortcuts.reset.toast.title": "Горячие клавиши сброшены", + "settings.shortcuts.reset.toast.description": "Горячие клавиши были сброшены к значениям по умолчанию.", + "settings.shortcuts.conflict.title": "Сочетание уже используется", + "settings.shortcuts.conflict.description": "{{keybind}} уже назначено для {{titles}}.", + "settings.shortcuts.unassigned": "Не назначено", + "settings.shortcuts.pressKeys": "Нажмите клавиши", + "settings.shortcuts.search.placeholder": "Поиск горячих клавиш", + "settings.shortcuts.search.empty": "Горячие клавиши не найдены", + + "settings.shortcuts.group.general": "Основные", + "settings.shortcuts.group.session": "Сессия", + "settings.shortcuts.group.navigation": "Навигация", + "settings.shortcuts.group.modelAndAgent": "Модель и агент", + "settings.shortcuts.group.terminal": "Терминал", + "settings.shortcuts.group.prompt": "Запрос", + + "settings.providers.title": "Провайдеры", + "settings.providers.description": "Настройки провайдеров будут доступны здесь.", + "settings.providers.section.connected": "Подключённые провайдеры", + "settings.providers.connected.empty": "Нет подключённых провайдеров", + "settings.providers.section.popular": "Популярные провайдеры", + "settings.providers.tag.environment": "Среда", + "settings.providers.tag.config": "Конфигурация", + "settings.providers.tag.custom": "Пользовательский", + "settings.providers.tag.other": "Другое", + "settings.models.title": "Модели", + "settings.models.description": "Настройки моделей будут доступны здесь.", + "settings.agents.title": "Агенты", + "settings.agents.description": "Настройки агентов будут доступны здесь.", + "settings.commands.title": "Команды", + "settings.commands.description": "Настройки команд будут доступны здесь.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Настройки MCP будут доступны здесь.", + + "settings.permissions.title": "Разрешения", + "settings.permissions.description": "Укажите, какие инструменты сервер может использовать по умолчанию.", + "settings.permissions.section.tools": "Инструменты", + "settings.permissions.toast.updateFailed.title": "Не удалось обновить разрешения", + + "settings.permissions.action.allow": "Разрешить", + "settings.permissions.action.ask": "Спрашивать", + "settings.permissions.action.deny": "Запретить", + + "settings.permissions.tool.read.title": "Чтение", + "settings.permissions.tool.read.description": "Чтение файла (по совпадению пути)", + "settings.permissions.tool.edit.title": "Редактирование", + "settings.permissions.tool.edit.description": + "Изменение файлов, включая редактирование, запись, патчи и множественное редактирование", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Сопоставление файлов по паттернам glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Поиск по содержимому файлов с использованием регулярных выражений", + "settings.permissions.tool.list.title": "List", + "settings.permissions.tool.list.description": "Список файлов в директории", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Запуск команд оболочки", + "settings.permissions.tool.task.title": "Task", + "settings.permissions.tool.task.description": "Запуск субагентов", + "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.description": "Загрузка навыка по имени", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Запросы к языковому серверу", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Обновление списка задач", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Получение контента по URL", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "Поиск в интернете", + "settings.permissions.tool.external_directory.title": "Внешняя директория", + "settings.permissions.tool.external_directory.description": "Доступ к файлам вне директории проекта", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": + "Обнаружение повторяющихся вызовов инструментов с одинаковыми входными данными", + + "session.delete.failed.title": "Не удалось удалить сессию", + "session.delete.title": "Удалить сессию", + "session.delete.confirm": 'Удалить сессию "{{name}}"?', + "session.delete.button": "Удалить сессию", + + "workspace.new": "Новое рабочее пространство", + "workspace.type.local": "локальное", + "workspace.type.sandbox": "песочница", + "workspace.create.failed.title": "Не удалось создать рабочее пространство", + "workspace.delete.failed.title": "Не удалось удалить рабочее пространство", + "workspace.resetting.title": "Сброс рабочего пространства", + "workspace.resetting.description": "Это может занять минуту.", + "workspace.reset.failed.title": "Не удалось сбросить рабочее пространство", + "workspace.reset.success.title": "Рабочее пространство сброшено", + "workspace.reset.success.description": "Рабочее пространство теперь соответствует ветке по умолчанию.", + "workspace.error.stillPreparing": "Рабочее пространство всё ещё подготавливается", + "workspace.status.checking": "Проверка незафиксированных изменений...", + "workspace.status.error": "Не удалось проверить состояние Git.", + "workspace.status.clean": "Незафиксированных изменений не обнаружено.", + "workspace.status.dirty": "В этом рабочем пространстве обнаружены незафиксированные изменения.", + "workspace.delete.title": "Удалить рабочее пространство", + "workspace.delete.confirm": 'Удалить рабочее пространство "{{name}}"?', + "workspace.delete.button": "Удалить рабочее пространство", + "workspace.reset.title": "Сбросить рабочее пространство", + "workspace.reset.confirm": 'Сбросить рабочее пространство "{{name}}"?', + "workspace.reset.button": "Сбросить рабочее пространство", + "workspace.reset.archived.none": "Активные сессии не будут архивированы.", + "workspace.reset.archived.one": "1 сессия будет архивирована.", + "workspace.reset.archived.many": "Будут архивированы сессии: {{count}}.", + "workspace.reset.note": "Это сбросит рабочее пространство до соответствия ветке по умолчанию.", + "common.open": "Открыть", + "dialog.releaseNotes.action.getStarted": "Начать", + "dialog.releaseNotes.action.next": "Далее", + "dialog.releaseNotes.action.hideFuture": "Больше не показывать", + "dialog.releaseNotes.media.alt": "Предварительный просмотр выпуска", + "toast.project.reloadFailed.title": "Не удалось перезагрузить {{project}}", + "error.server.invalidConfiguration": "Недопустимая конфигурация", + "common.moreCountSuffix": " (ещё {{count}})", + "common.time.justNow": "Только что", + "common.time.minutesAgo.short": "{{count}} мин назад", + "common.time.hoursAgo.short": "{{count}} ч назад", + "common.time.daysAgo.short": "{{count}} д назад", + "settings.providers.connected.environmentDescription": "Подключено из ваших переменных окружения", + "settings.providers.custom.description": "Добавить провайдера, совместимого с OpenAI, по базовому URL.", + + "app.server.unreachable": "Не удалось связаться с {{server}}", + "app.server.retrying": "Автоматическая повторная попытка...", + "app.server.otherServers": "Другие серверы", + "dialog.server.add.usernamePlaceholder": "имя пользователя", + "dialog.server.add.passwordPlaceholder": "пароль", + "server.row.noUsername": "нет имени пользователя", + "session.review.noVcs.createGit.title": "Создать репозиторий Git", + "session.review.noVcs.createGit.description": "Отслеживайте, просматривайте и отменяйте изменения в этом проекте", + "session.review.noVcs.createGit.actionLoading": "Создание репозитория Git...", + "session.review.noVcs.createGit.action": "Создать репозиторий Git", + "session.todo.progress": "Выполнено {{done}} из {{total}} задач", + "session.question.progress": "{{current}} из {{total}} вопросов", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Проводник", + "session.header.reveal.finder": "Показать в Finder", + "session.header.reveal.fileExplorer": "Показать в Проводнике", + "session.header.reveal.containingFolder": "Открыть папку расположения", + "session.header.open.fileManager": "Файловый менеджер", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Терминал", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Диагностика производительности разработки", + "debugBar.na": "н/д", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Последний завершённый переход по маршруту, затрагивающий страницу сессии: от запуска маршрутизатора до первой отрисовки после стабилизации.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Скользящая частота кадров в секунду за последние 5 секунд.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Худшее время кадра за последние 5 секунд.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Кадры более 32 мс за последние 5 секунд.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Заблокированное время и количество длинных задач за последние 5 секунд. Макс. задача: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Худшая наблюдаемая задержка ввода за последние 5 секунд.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Приблизительная продолжительность взаимодействия за последние 5 секунд. Это похоже на INP, а не официальный Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Совокупное смещение макета за время работы приложения.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Используемая куча JS по сравнению с лимитом кучи. Только Chromium.", + "debugBar.mem.tip": "Используемая куча JS по сравнению с лимитом кучи. {{used}} из {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Принудительно применить стили фокуса ко всем интерактивным элементам", + "debugBar.focus.on": "ВКЛ.", + "debugBar.focus.off": "ВЫКЛ.", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Принудительно задать направление макета всего приложения, не меняя выбранный язык", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Пробел", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "неизвестно", + "error.page.circular": "[Циклично]", + "error.serverSDK.noServerAvailable": "Нет доступного сервера", + "error.serverSDK.serverNotAvailable": "Сервер недоступен", + "error.childStore.persistedCacheCreateFailed": "Не удалось создать постоянный кеш", + "error.childStore.persistedProjectMetadataCreateFailed": "Не удалось создать постоянные метаданные проекта", + "error.childStore.persistedProjectIconCreateFailed": "Не удалось создать постоянный значок проекта", + "error.childStore.storeCreateFailed": "Не удалось создать хранилище", + "terminal.connectionLost.abnormalClose": "WebSocket закрыт аварийно: {{code}}", +} diff --git a/packages/app/src/i18n/si.ts b/packages/app/src/i18n/si.ts new file mode 100644 index 0000000000000000000000000000000000000000..00ca188b15811c27888ada5834493de1eaf74376 --- /dev/null +++ b/packages/app/src/i18n/si.ts @@ -0,0 +1,1153 @@ +export const dict: Record = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ගොනුව", + "desktop.menu.edit": "සංස්කරණය කරන්න", + "desktop.menu.view": "බලන්න", + "desktop.menu.go": "යන්න", + "desktop.menu.window": "කවුළුව", + "desktop.menu.help": "උදව්", + "desktop.menu.checkForUpdates": "යාවත්කාලීන සඳහා පරීක්ෂා කරන්න...", + "desktop.menu.settings": "සැකසීම්", + "desktop.menu.reloadWebview": "Webview නැවත පූරණය කරන්න", + "desktop.menu.restart": "යළි අරඹන්න", + "desktop.menu.exportLogs": "ලොග අපනයනය කරන්න...", + "desktop.menu.newSession": "නව සැසිය", + "desktop.menu.openProject": "විවෘත ව්‍යාපෘතිය...", + "desktop.menu.newWindow": "නව කවුළුව", + "desktop.menu.closeWindow": "කවුළුව වසන්න", + "desktop.menu.undo": "අහෝසි කරන්න", + "desktop.menu.redo": "නැවත කරන්න", + "desktop.menu.cut": "කපනවා", + "desktop.menu.copy": "පිටපත් කරන්න", + "desktop.menu.paste": "අලවන්න", + "desktop.menu.delete": "මකන්න", + "desktop.menu.selectAll": "සියල්ල තෝරන්න", + "desktop.menu.toggleSidebar": "පැති තීරුව ටොගල් කරන්න", + "desktop.menu.toggleTerminal": "පර්යන්තය ටොගල් කරන්න", + "desktop.menu.toggleFileTree": "ගොනු ගස ටොගල් කරන්න", + "desktop.menu.reload": "නැවත පූරණය කරන්න", + "desktop.menu.toggleDeveloperTools": "සංවර්ධක මෙවලම් ටොගල් කරන්න", + "desktop.menu.actualSize": "සැබෑ ප්රමාණය", + "desktop.menu.zoomIn": "විශාලනය කරන්න", + "desktop.menu.zoomOut": "විශාලනය කරන්න", + "desktop.menu.toggleFullScreen": "සම්පූර්ණ තිරය ටොගල් කරන්න", + "desktop.menu.back": "ආපසු", + "desktop.menu.forward": "ඉදිරියට", + "desktop.menu.previousSession": "පෙර සැසිය", + "desktop.menu.nextSession": "ඊළඟ සැසිය", + "desktop.menu.previousProject": "පෙර ව්යාපෘතිය", + "desktop.menu.nextProject": "ඊළඟ ව්යාපෘතිය", + "desktop.menu.minimize": "අවම කරන්න", + "desktop.menu.maximize": "උපරිම කරන්න", + "desktop.menu.documentation": "OpenCode ලේඛනගත කිරීම", + "desktop.menu.supportForum": "ආධාරක සංසදය", + "desktop.menu.shareFeedback": "ප්‍රතිපෝෂණ බෙදා ගන්න", + "desktop.menu.reportBug": "දෝෂයක් වාර්තා කරන්න", + "desktop.menu.ariaLabel": "OpenCode මෙනුව", + "desktop.updater.dialog.checkFailed.message": "යාවත්කාලීන පරීක්ෂාව අසාර්ථක විය.", + "desktop.updater.dialog.checkFailed.title": "යාවත්කාලීන දෝෂයකි", + "desktop.updater.dialog.upToDate.message": "ඔබ යාවත්කාලීනයි.", + "desktop.updater.dialog.upToDate.title": "යාවත්කාලීන නැත", + "desktop.updater.dialog.ready.message": "යාවත්කාලීන {{version}} බාගන්නා ලදී. දැන් නැවත ආරම්භ කරන්නද?", + "desktop.updater.dialog.ready.title": "යාවත්කාලීනය සූදානම්", + "desktop.updater.dialog.restart": "යළි අරඹන්න", + "desktop.updater.dialog.later": "පසුව", + "desktop.recovery.action.relaunch": "නැවත දියත් කරන්න", + "desktop.recovery.action.exportLogs": "ලොග් අපනයනය කරන්න", + "desktop.recovery.action.keepWaiting": "දිගටම රැඳී සිටින්න", + "desktop.recovery.action.quit": "ඉවත් වන්න", + "desktop.recovery.loadFailed": "OpenCode පූරණය කිරීමට අසමත් විය", + "desktop.recovery.terminated": "OpenCode කවුළුව අනපේක්ෂිත ලෙස අවසන් විය", + "desktop.recovery.unresponsive": "OpenCode ප්‍රතිචාර නොදක්වයි", + "desktop.recovery.unresponsive.detail": "ඔබට යෙදුම නැවත දියත් කිරීමට, ලඝු-සටහන් විවෘත කිරීමට හෝ රැඳී සිටීමට හැකිය.", + "desktop.recovery.loadFailed.detail": "කවුළුව: {{window}}\nURL: {{url}}\nදෝෂය: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "කවුළුව: {{window}}\nහේතුව: {{reason}}\nකේතය: {{code}}", + "desktop.recovery.unknown": "<නොදන්නා>", + "desktop.dialog.chooseFolder": "ෆෝල්ඩරයක් තෝරන්න", + "desktop.dialog.chooseFile": "ගොනුවක් තෝරන්න", + "desktop.dialog.saveFile": "ගොනුව සුරකින්න", + "desktop.dialog.files": "ගොනු", + "desktop.server.local": "දේශීය සේවාදායකය", + "desktop.wsl.error.windowsOnly": "WSL ඇත්තේ Windows හි පමණි", + "desktop.wsl.error.unavailable": "WSL නොමැත", + "desktop.wsl.error.listInstalled": "ස්ථාපිත WSL බෙදාහැරීම් ලැයිස්තුගත කිරීමට අසමත් විය", + "desktop.wsl.error.listOnline": "සබැඳි WSL බෙදාහැරීම් ලැයිස්තුගත කිරීමට අසමත් විය", + "desktop.wsl.error.executeDistro": "distro හි විධාන ක්‍රියාත්මක කළ නොහැක", + "desktop.wsl.error.installWsl": "WSL ස්ථාපනය අසාර්ථක විය", + "desktop.wsl.error.installDistro": "distro ස්ථාපනය කිරීමට අසමත් විය: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode ස්ථාපනය අසාර්ථක විය", + "desktop.wsl.error.alreadyAdded": "{{distro}} දැනටමත් එක් කර ඇත", + "desktop.wsl.error.opencodeMissing": "මෙම distro හි opencode ස්ථාපනය කර නොමැත", + "desktop.wsl.error.opencodeCannotRun": "opencode ස්ථාපනය කර ඇති නමුත් ධාවනය කල නොහැක", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode {{distro}} හි ස්ථාපනය කර නැත", + "desktop.wsl.error.updateVersion": + "OpenCode යාවත්කාලීන කිරීම අවසන් නමුත් {{distro}} තවමත් වාර්තා කරන්නේ {{installed}}; අපේක්ෂිත {{expected}}", + "desktop.wsl.error.noVersion": "අනුවාදයක් නැත", + "desktop.wsl.error.serverExited": "WSL සේවාදායකය ආරම්භයෙන් පසු ඉවත් විය (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL සේවාදායකය නිරෝගී වීමට පෙර ඉවත් විය (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} සෞඛ්‍ය පරීක්‍ෂාව සඳහා සයිඩ්කාර් {{timeout}}ms ට පසුව කල් ඉකුත් විය", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} {{timeout}}msට පසුව කාලය අවසන් විය", + "desktop.wsl.error.failedPort": "වරාය ලබා ගැනීමට අසමත් විය", + "desktop.picker.error.notSelected": "ගොනුව තෝරන්නා විසින් තෝරාගෙන නොමැත", + "desktop.picker.error.sizeLimit": "තෝරාගත් ඇමුණුම් {{limit}} MB සීමාව ඉක්මවයි", + "command.category.suggested": "යෝජනා කර ඇත", + "command.category.view": "බලන්න", + "command.category.project": "ව්යාපෘතිය", + "command.category.provider": "සපයන්නා", + "command.category.server": "සේවාදායකය", + "command.category.session": "සැසිය", + "command.category.theme": "තේමාව", + "command.category.language": "භාෂාව", + "command.category.file": "ගොනුව", + "command.category.context": "සන්දර්භය", + "command.category.terminal": "පර්යන්තය", + "command.category.model": "ආකෘතිය", + "command.category.mcp": "MCP", + "command.category.agent": "නියෝජිතයා", + "command.category.permissions": "අවසර", + "command.category.workspace": "වැඩබිම", + "command.category.settings": "සැකසීම්", + "command.logs.export": "ලොග් අපනයනය කරන්න", + "theme.scheme.system": "පද්ධතිය", + "theme.scheme.light": "ආලෝකය", + "theme.scheme.dark": "අඳුරු", + "command.sidebar.toggle": "පැති තීරුව ටොගල් කරන්න", + "command.project.open": "විවෘත ව්යාපෘතිය", + "command.project.previous": "පෙර ව්යාපෘතිය", + "command.project.next": "ඊළඟ ව්යාපෘතිය", + "command.project.index": "{{index}} ව්‍යාපෘතියට මාරු වන්න", + "command.provider.connect": "සපයන්නා සම්බන්ධ කරන්න", + "command.server.switch": "සේවාදායකය මාරු කරන්න", + "command.settings.open": "සැකසුම් විවෘත කරන්න", + "command.session.previous": "පෙර සැසිය", + "command.session.next": "ඊළඟ සැසිය", + "command.session.previous.unseen": "පෙර නොකියවූ සැසිය", + "command.session.next.unseen": "ඊළඟ නොකියවූ සැසිය", + "command.session.archive": "සංරක්ෂිත සැසිය", + "command.palette": "විධාන පුවරුව", + "command.theme.cycle": "සයිකල් තේමාව", + "command.theme.set": "තේමාව භාවිතා කරන්න: {{theme}}", + "command.theme.scheme.cycle": "චක්‍ර වර්ණ පටිපාටිය", + "command.theme.scheme.set": "වර්ණ පටිපාටිය භාවිතා කරන්න: {{scheme}}", + "command.language.cycle": "චක්‍ර භාෂාව", + "command.language.set": "භාෂාව භාවිතා කරන්න: {{language}}", + "command.session.new": "නව සැසිය", + "command.file.open": "ගොනුව විවෘත කරන්න", + "command.tab.close": "ටැබ් එක වසන්න", + "command.tab.reopenClosed": "වසා දැමූ ටැබය නැවත විවෘත කරන්න", + "command.context.addSelection": "සන්දර්භයට තේරීම එක් කරන්න", + "command.context.addSelection.description": "වත්මන් ගොනුවෙන් තෝරාගත් රේඛා එක් කරන්න", + "command.input.focus": "අවධානය යොමු කරන්න", + "command.terminal.toggle": "පර්යන්තය ටොගල් කරන්න", + "command.fileTree.toggle": "ගොනු ගස ටොගල් කරන්න", + "command.review.toggle": "සමාලෝචනය ටොගල් කරන්න", + "command.terminal.new": "නව පර්යන්තය", + "command.terminal.new.description": "නව පර්යන්ත පටිත්තක් සාදන්න", + "command.steps.toggle": "පියවර ටොගල් කරන්න", + "command.steps.toggle.description": "වත්මන් පණිවිඩය සඳහා පියවර පෙන්වන්න හෝ සඟවන්න", + "command.message.previous": "පෙර පණිවිඩය", + "command.message.previous.description": "පෙර පරිශීලක පණිවිඩයට යන්න", + "command.message.next": "ඊළඟ පණිවිඩය", + "command.message.next.description": "ඊළඟ පරිශීලක පණිවිඩයට යන්න", + "command.model.choose": "ආකෘතිය තෝරන්න", + "command.model.choose.description": "වෙනස් ආකෘතියක් තෝරන්න", + "command.mcp.toggle": "MCPs ටොගල් කරන්න", + "command.mcp.toggle.description": "MCPs ටොගල් කරන්න", + "command.agent.cycle": "සයිකල් නියෝජිතයා", + "command.agent.cycle.description": "ඊළඟ නියෝජිතයා වෙත මාරු වන්න", + "command.agent.cycle.reverse": "සයිකල් නියෝජිතයා පසුපසට", + "command.agent.cycle.reverse.description": "පෙර නියෝජිතයා වෙත මාරු වන්න", + "command.model.variant.cycle": "චක්‍රීය චින්තන උත්සාහය", + "command.model.variant.cycle.description": "ඊළඟ උත්සාහ මට්ටමට මාරු වන්න", + "command.prompt.mode.shell": "ෂෙල්", + "command.prompt.mode.normal": "ප්‍රොම්ප්ට්", + "command.permissions.autoaccept.enable": "අවසර ස්වයංක්‍රීයව පිළිගන්න", + "command.permissions.autoaccept.disable": "අවසර ස්වයංක්‍රීයව පිළිගැනීම නවත්වන්න", + "command.workspace.toggle": "වැඩබිම් ටොගල් කරන්න", + "command.workspace.toggle.description": "පැති තීරුවේ වැඩබිම් කිහිපයක් සබල හෝ අබල කරන්න", + "command.session.undo": "අහෝසි කරන්න", + "command.session.undo.description": "අවසාන පණිවිඩය අහෝසි කරන්න", + "command.session.redo": "නැවත කරන්න", + "command.session.redo.description": "අවසන් අවලංගු කළ පණිවිඩය නැවත කරන්න", + "command.session.compact": "සංයුක්ත සැසිය", + "command.session.compact.description": "සන්දර්භය ප්‍රමාණය අඩු කිරීමට සැසිය සාරාංශ කරන්න", + "command.session.fork": "පණිවිඩයෙන් දෙබලක", + "command.session.fork.description": "පෙර පණිවිඩයකින් නව සැසියක් සාදන්න", + "command.session.share": "සැසිය බෙදා ගන්න", + "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": "ප්‍රතිඵල හමු නොවිණි", + "palette.group.commands": "විධාන", + "palette.group.files": "ගොනු", + "dialog.provider.search.placeholder": "සෙවුම් සපයන්නන්", + "dialog.provider.empty": "සපයන්නන් හමු නොවීය", + "dialog.provider.group.popular": "ජනප්‍රියයි", + "dialog.provider.group.other": "වෙනත්", + "dialog.provider.custom.label": "අභිරුචි OpenAI-අනුකූල සැපයුම්කරු", + "dialog.provider.tag.recommended": "නිර්දේශ කර ඇත", + "dialog.provider.opencode.note": "Claude, GPT, Gemini සහ තවත් දේ ඇතුළුව සංවෘත ආකෘති", + "dialog.provider.opencode.tagline": "විශ්වසනීය ප්රශස්ත මාදිලි", + "dialog.provider.opencodeGo.tagline": "සෑම කෙනෙකුටම අඩු වියදම් දායකත්වයක්", + "dialog.provider.anthropic.note": "Pro සහ Max ඇතුළුව Claude මාදිලි වෙත සෘජු ප්‍රවේශය", + "dialog.provider.copilot.note": "GitHub Copilot හරහා කේතීකරණ සහාය සඳහා AI ආකෘති", + "dialog.provider.openai.note": "වේගවත්, හැකියාව ඇති සාමාන්‍ය AI කාර්යයන් සඳහා GPT මාදිලි", + "dialog.provider.google.note": "වේගවත්, ව්‍යුහගත ප්‍රතිචාර සඳහා Gemini ආකෘති", + "dialog.provider.openrouter.note": "එක් සැපයුම්කරුවෙකුගෙන් සියලුම සහාය දක්වන මාදිලි වෙත ප්‍රවේශ වන්න", + "dialog.provider.vercel.note": "ස්මාර්ට් රවුටින් සමඟ AI මාදිලි සඳහා ඒකාබද්ධ ප්‍රවේශය", + "dialog.model.select.title": "ආකෘතිය තෝරන්න", + "dialog.model.search.placeholder": "සෙවුම් ආකෘති", + "dialog.model.empty": "ආදර්ශ ප්‍රතිඵල නැත", + "dialog.model.manage": "ආකෘති කළමනාකරණය කරන්න", + "dialog.model.manage.description": "ආකෘති තේරීම්කාරකයේ දිස්වන මාදිලි අභිරුචිකරණය කරන්න.", + "dialog.model.manage.provider.toggle": "සියලුම {{provider}} මාදිලි ටොගල් කරන්න", + "dialog.model.unpaid.freeModels.title": "OpenCode මඟින් සපයනු ලබන නිදහස් මාදිලි", + "dialog.model.unpaid.addMore.title": "ජනප්‍රිය සැපයුම්කරුවන්ගෙන් තවත් ආකෘති එක් කරන්න", + "dialog.model.unpaid.viewMoreProviders": "තවත් සපයන්නන් 70+ බලන්න", + "dialog.provider.viewAll": "තවත් සපයන්නන් පෙන්වන්න", + "provider.connect.title": "{{provider}} සම්බන්ධ කරන්න", + "provider.connect.title.anthropicProMax": "Claude Pro/Max සමඟින් පුරනය වන්න", + "provider.connect.selectMethod": "{{provider}} සඳහා පිවිසුම් ක්‍රමය තෝරන්න.", + "provider.connect.method.apiKey": "API යතුර", + "provider.connect.method.browser": "බ්රවුසරය", + "provider.connect.method.headless": "හිස නැති", + "provider.connect.status.inProgress": "අවසරය ක්‍රියාත්මක වෙමින් පවතී...", + "provider.connect.status.waiting": "අවසරය සඳහා රැඳී සිටිමින්...", + "provider.connect.status.failed": "අවසරය අසාර්ථක විය: {{error}}", + "provider.connect.apiKey.description": + "ඔබගේ ගිණුම සම්බන්ධ කිරීමට ඔබගේ {{provider}} API යතුර ඇතුලත් කරන්න සහ OpenCode හි {{provider}} මාදිලි භාවිතා කරන්න.", + "provider.connect.apiKey.label": "{{provider}} API යතුර", + "provider.connect.apiKey.placeholder": "API යතුර", + "provider.connect.apiKey.required": "API යතුර අවශ්‍යයි", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ඔබට කේතීකරණ නියෝජිතයන් සඳහා විශ්වාසදායක ප්‍රශස්ත මාදිලි සමූහයකට ප්‍රවේශය ලබා දෙයි.", + "provider.connect.opencodeZen.line2": + "තනි API යතුරක් සමඟින් ඔබට Claude, GPT, Gemini, GLM සහ තවත් ආකෘති වෙත ප්‍රවේශය ලැබෙනු ඇත.", + "provider.connect.opencodeZen.visit.prefix": "පිවිසෙන්න", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "ඔබගේ API යතුර එකතු කිරීමට.", + "provider.connect.oauth.code.visit.prefix": "පිවිසෙන්න", + "provider.connect.oauth.code.visit.link": "මෙම සබැඳිය", + "provider.connect.oauth.code.visit.suffix": + "ඔබගේ ගිණුම සම්බන්ධ කිරීමට සහ OpenCode හි {{provider}} මාදිලි භාවිතා කිරීමට ඔබගේ අවසර කේතය එකතු කිරීමට.", + "provider.connect.oauth.code.label": "{{method}} අවසර කේතය", + "provider.connect.oauth.code.placeholder": "අවසර කේතය", + "provider.connect.oauth.code.required": "අවසර කේතය අවශ්ය වේ", + "provider.connect.oauth.code.invalid": "වලංගු නොවන අවසර කේතය", + "provider.connect.oauth.auto.visit.prefix": "පිවිසෙන්න", + "provider.connect.oauth.auto.visit.link": "මෙම සබැඳිය", + "provider.connect.oauth.auto.visit.suffix": + "සහ ඔබගේ ගිණුම සම්බන්ධ කිරීමට පහත කේතය ඇතුළු කර {{provider}} මාදිලි OpenCode තුළ භාවිතා කරන්න.", + "provider.connect.oauth.auto.confirmationCode": "තහවුරු කිරීමේ කේතය", + "provider.connect.toast.connected.title": "{{provider}} සම්බන්ධයි", + "provider.connect.toast.connected.description": "{{provider}} මාදිලි දැන් භාවිතා කිරීමට තිබේ.", + "provider.custom.title": "අභිරුචි සපයන්නා", + "provider.custom.unavailable": "අභිරුචි සපයන්නන් මෙම සේවාදායකයේ නොමැත", + "provider.custom.description.prefix": "OpenAI-අනුකූල සැපයුම්කරුවෙකු වින්‍යාස කරන්න. බලන්න", + "provider.custom.description.link": "සපයන්නාගේ වින්‍යාස ලේඛන", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "සපයන්නාගේ හැඳුනුම්පත", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "කුඩා අකුරු, ඉලක්කම්, ඉරි, හෝ යටි ඉරි", + "provider.custom.field.name.label": "ප්රදර්ශන නාමය", + "provider.custom.field.name.placeholder": "මගේ AI සපයන්නා", + "provider.custom.field.baseURL.label": "පාදක URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API යතුර", + "provider.custom.field.apiKey.placeholder": "API යතුර", + "provider.custom.field.apiKey.description": "විකල්ප. ඔබ ශීර්ෂ හරහා සත්‍යාපනය කළමනාකරණය කරන්නේ නම් හිස්ව තබන්න.", + "provider.custom.models.label": "ආකෘති", + "provider.custom.models.id.label": "හැඳුනුම්පත", + "provider.custom.models.id.placeholder": "ආදර්ශ-id", + "provider.custom.models.name.label": "නම", + "provider.custom.models.name.placeholder": "ප්රදර්ශන නාමය", + "provider.custom.models.remove": "ආකෘතිය ඉවත් කරන්න", + "provider.custom.models.add": "ආකෘතිය එකතු කරන්න", + "provider.custom.headers.label": "ශීර්ෂ (විකල්ප)", + "provider.custom.headers.key.label": "ශීර්ෂකය", + "provider.custom.headers.key.placeholder": "ශීර්ෂය-නම", + "provider.custom.headers.value.label": "වටිනාකම", + "provider.custom.headers.value.placeholder": "වටිනාකම", + "provider.custom.headers.remove": "ශීර්ෂකය ඉවත් කරන්න", + "provider.custom.headers.add": "ශීර්ෂකය එක් කරන්න", + "provider.custom.error.providerID.required": "සපයන්නාගේ හැඳුනුම්පත අවශ්‍යයි", + "provider.custom.error.providerID.format": "කුඩා අකුරු, ඉලක්කම්, ඉරි, හෝ යටි ඉරි භාවිතා කරන්න", + "provider.custom.error.providerID.exists": "එම සැපයුම්කරු හැඳුනුම්පත දැනටමත් පවතී", + "provider.custom.error.name.required": "සංදර්ශක නම අවශ්ය වේ", + "provider.custom.error.baseURL.required": "පදනම URL අවශ්‍යයි", + "provider.custom.error.baseURL.format": "http:// හෝ https:// සමඟින් ආරම්භ කළ යුතුය", + "provider.custom.error.required": "අවශ්යයි", + "provider.custom.error.duplicate": "අනුපිටපත් කරන්න", + "provider.disconnect.toast.disconnected.title": "{{provider}} විසන්ධි විය", + "provider.disconnect.toast.disconnected.description": "{{provider}} මාදිලි තවදුරටත් නොමැත.", + "model.tag.free": "නොමිලේ", + "model.tag.latest": "නවතම", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "පෙළ", + "model.input.image": "රූපය", + "model.input.audio": "ශ්රව්ය", + "model.input.video": "වීඩියෝ", + "model.input.pdf": "pdf", + "model.tooltip.allows": "ඉඩ දෙයි: {{inputs}}", + "model.tooltip.context.label": "සන්දර්භය", + "model.tooltip.inputs": "යෙදවුම්", + "model.tooltip.model": "ආකෘතිය", + "model.tooltip.provider": "සපයන්නා", + "model.tooltip.reasoning": "තර්ක කිරීම", + "model.tooltip.reasoning.allowed": "තර්ක කිරීමට ඉඩ දෙයි", + "model.tooltip.reasoning.none": "තර්කයක් නැත", + "model.tooltip.context": "සන්දර්භය සීමාව {{limit}}", + "common.search.placeholder": "සොයන්න", + "common.goBack": "ආපසු සංචාලනය කරන්න", + "common.goForward": "ඉදිරියට ගමන් කරන්න", + "common.loading": "පැටවීම", + "common.loading.ellipsis": "...", + "common.cancel": "අවලංගු කරන්න", + "common.clear": "පැහැදිලියි", + "common.open": "විවෘත කරන්න", + "common.connect": "සම්බන්ධ කරන්න", + "common.disconnect": "විසන්ධි කරන්න", + "common.continue": "දිගටම කරගෙන යන්න", + "common.submit": "ඉදිරිපත් කරන්න", + "common.save": "සුරකින්න", + "common.saving": "සුරකිමින්...", + "common.default": "පෙරනිමිය", + "common.attachment": "ඇමුණුම", + "prompt.placeholder.shell": "ෂෙල් විධානය ඇතුළත් කරන්න... {{example}}", + "prompt.placeholder.normal": 'ඕනෑම දෙයක් අසන්න... "{{example}}"', + "prompt.placeholder.simple": "ඕන දෙයක් අහන්න...", + "prompt.placeholder.summarizeComments": "අදහස් සාරාංශ කරන්න...", + "prompt.placeholder.summarizeComment": "අදහස් සාරාංශ කරන්න...", + "prompt.mode.shell": "ෂෙල්", + "prompt.mode.normal": "ප්‍රොම්ප්ට්", + "prompt.mode.shell.exit": "පිටවීමට esc", + "session.child.promptDisabled": "උපකාරක සැසි ඉල්ලා සිටිය නොහැක.", + "session.child.backToParent": "ප්‍රධාන සැසිය වෙත ආපසු.", + "prompt.example.1": "කේත පදනමේ TODO එකක් සවි කරන්න", + "prompt.example.2": "මෙම ව්‍යාපෘතියේ තාක්ෂණික තොගය කුමක්ද?", + "prompt.example.3": "කැඩුණු පරීක්ෂණ නිවැරදි කරන්න", + "prompt.example.4": "සත්‍යාපනය ක්‍රියාත්මක වන ආකාරය පැහැදිලි කරන්න", + "prompt.example.5": "ආරක්ෂක දුර්වලතා සොයාගෙන නිවැරදි කරන්න", + "prompt.example.6": "පරිශීලක සේවාව සඳහා ඒකක පරීක්ෂණ එක් කරන්න", + "prompt.example.7": "මෙම ශ්‍රිතය වඩාත් කියවිය හැකි ලෙස නැවත සකස් කරන්න", + "prompt.example.8": "මෙම දෝෂය අදහස් කරන්නේ කුමක්ද?", + "prompt.example.9": "මෙම ගැටලුව නිදොස් කිරීමට මට උදවු කරන්න", + "prompt.example.10": "API ලේඛන උත්පාදනය කරන්න", + "prompt.example.11": "දත්ත සමුදා විමසුම් ප්‍රශස්ත කරන්න", + "prompt.example.12": "ආදාන වලංගුකරණය එක් කරන්න", + "prompt.example.13": "සඳහා නව සංරචකයක් සාදන්න...", + "prompt.example.14": "මම මෙම ව්‍යාපෘතිය යොදවන්නේ කෙසේද?", + "prompt.example.15": "හොඳම භාවිතයන් සඳහා මගේ කේතය සමාලෝචනය කරන්න", + "prompt.example.16": "මෙම කාර්යයට දෝෂ හැසිරවීම එක් කරන්න", + "prompt.example.17": "මෙම regex රටාව පැහැදිලි කරන්න", + "prompt.example.18": "මෙය TypeScript වෙත පරිවර්තනය කරන්න", + "prompt.example.19": "කේත පදනම පුරා ලොග් වීම එක් කරන්න", + "prompt.example.20": "යල් පැන ගිය පරායත්තතා මොනවාද?", + "prompt.example.21": "සංක්‍රමණ පිටපතක් ලිවීමට මට උදවු කරන්න", + "prompt.example.22": "මෙම අන්ත ලක්ෂ්‍යය සඳහා හැඹිලිගත කිරීම ක්‍රියාත්මක කරන්න", + "prompt.example.23": "මෙම ලැයිස්තුවට පිටුව එකතු කරන්න", + "prompt.example.24": "සඳහා CLI විධානයක් සාදන්න...", + "prompt.example.25": "පරිසර විචල්‍යයන් මෙහි ක්‍රියා කරන්නේ කෙසේද?", + "prompt.popover.emptyResults": "ගැළපෙන ප්‍රතිඵල නැත", + "prompt.popover.emptyCommands": "ගැළපෙන විධාන නොමැත", + "prompt.dropzone.label": "පින්තූර, PDF හෝ පෙළ ගොනු මෙහි දමන්න", + "prompt.dropzone.file.label": "@mention ගොනුව වෙත ඇද දමන්න", + "prompt.slash.badge.custom": "චාරිත්රය", + "prompt.slash.badge.skill": "දක්ෂතාවය", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "ක්රියාකාරී", + "prompt.context.includeActiveFile": "සක්රිය ගොනුව ඇතුළත් කරන්න", + "prompt.context.removeActiveFile": "සක්‍රිය ගොනුව සන්දර්භයෙන් ඉවත් කරන්න", + "prompt.context.removeFile": "සන්දර්භය වෙතින් ගොනුව ඉවත් කරන්න", + "prompt.action.attachFile": "ගොනු එකතු කරන්න", + "prompt.menu.addImagesAndFiles": "ගොනු සහ තවත් දේ එක් කරන්න", + "prompt.menu.imagesAndFiles": "පින්තූර සහ ගොනු", + "prompt.menu.commands": "විධාන", + "prompt.menu.context": "සන්දර්භය", + "prompt.menu.shellCommand": "ෂෙල් විධානය", + "prompt.attachment.remove": "ඇමුණුම ඉවත් කරන්න", + "prompt.action.send": "යවන්න", + "prompt.action.stop": "නවත්වන්න", + "prompt.toast.pasteUnsupported.title": "සහාය නොදක්වන ඇමුණුම", + "prompt.toast.pasteUnsupported.description": "පින්තූර, PDF හෝ පෙළ ගොනු පමණක් මෙහි ඇමිණිය හැක.", + "prompt.toast.attachmentDuplicate.title": "මෙම ගොනුව දැනටමත් උඩුගත කර ඇත", + "prompt.toast.modelAgentRequired.title": "නියෝජිතයෙකු සහ ආකෘතියක් තෝරන්න", + "prompt.toast.modelAgentRequired.description": "ප්‍රොම්ප්ට් එකක් යැවීමට පෙර නියෝජිතයෙකු සහ ආකෘතියක් තෝරන්න.", + "prompt.toast.worktreeCreateFailed.title": "Git වර්ක්ට්‍රී එකක් සෑදීමට අසමත් විය", + "prompt.toast.sessionCreateFailed.title": "සැසිය සෑදීමට අසමත් විය", + "prompt.toast.shellSendFailed.title": "ෂෙල් විධානය යැවීමට අසමත් විය", + "prompt.toast.commandSendFailed.title": "විධානය යැවීමට අසමත් විය", + "prompt.toast.promptSendFailed.title": "ප්‍රොම්ප්ට් එක යැවීමට අසමත් විය", + "prompt.toast.promptSendFailed.description": "සැසිය ලබා ගැනීමට නොහැකි විය", + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{total}} න් {{enabled}} සබලයි", + "dialog.mcp.empty": "MCPs වින්‍යාස කර නැත", + "dialog.lsp.empty": "LSPs ගොනු වර්ග වලින් ස්වයංක්‍රීයව අනාවරණය කර ඇත", + "dialog.plugins.empty": "ප්ලගීන opencode.json හි වින්‍යාස කර ඇත", + "mcp.status.connected": "සම්බන්ධයි", + "mcp.status.failed": "අසාර්ථක විය", + "mcp.status.needs_auth": "auth අවශ්‍යයි", + "mcp.status.disabled": "ආබාධිතයි", + "mcp.auth.clickToAuthenticate": "සත්‍යාපනය කිරීමට ක්ලික් කරන්න", + "dialog.fork.empty": "ෆෝක් කිරීමට පණිවිඩ නැත", + "dialog.directory.search.placeholder": "ෆෝල්ඩර සොයන්න", + "dialog.directory.empty": "ෆෝල්ඩර හමු නොවීය", + "dialog.directory.action.selectFile": "ගොනුව තෝරන්න", + "dialog.directory.action.selectFolder": "ෆෝල්ඩරය තෝරන්න", + "dialog.directory.root": "මූල", + "dialog.directory.parent": "දෙමාපිය", + "dialog.directory.readError": "මෙම ෆෝල්ඩරය කියවීමට නොහැක", + "app.server.unreachable": "{{server}} වෙත ළඟා විය නොහැක", + "app.server.retrying": "ස්වයංක්‍රීයව නැවත උත්සාහ කරමින්...", + "app.server.otherServers": "වෙනත් සේවාදායකයන්", + "dialog.server.title": "සේවාදායකයන්", + "dialog.server.description": "මෙම යෙදුම සම්බන්ධ වන්නේ කුමන OpenCode සේවාදායකයටද යන්න මාරු කරන්න.", + "dialog.server.search.placeholder": "සර්වර් සොයන්න", + "dialog.server.empty": "තවමත් සේවාදායකයන් නොමැත", + "dialog.server.add.title": "සේවාදායකය එක් කරන්න", + "dialog.server.add.url": "සේවාදායක ලිපිනය", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "සේවාදායකයට සම්බන්ධ වීමට නොහැකි විය", + "dialog.server.add.checking": "පරීක්ෂා කරමින්...", + "dialog.server.add.button": "සේවාදායකය එක් කරන්න", + "dialog.server.add.name": "සේවාදායකයේ නම (විකල්ප)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "පරිශීලක නාමය (විකල්ප)", + "dialog.server.add.usernamePlaceholder": "පරිශීලක නාමය", + "dialog.server.add.password": "මුරපදය (විකල්ප)", + "dialog.server.add.passwordPlaceholder": "මුරපදය", + "dialog.server.edit.title": "සේවාදායකය සංස්කරණය කරන්න", + "dialog.server.default.title": "පෙරනිමි සේවාදායකය", + "dialog.server.default.description": + "දේශීය සේවාදායකයක් ආරම්භ කිරීම වෙනුවට යෙදුම් දියත් කිරීමේදී මෙම සේවාදායකයට සම්බන්ධ කරන්න. නැවත ආරම්භ කිරීම අවශ්‍ය වේ.", + "dialog.server.default.none": "සේවාදායකයක් තෝරා නැත", + "dialog.server.default.set": "වත්මන් සේවාදායකය පෙරනිමිය ලෙස සකසන්න", + "dialog.server.default.clear": "පැහැදිලියි", + "dialog.server.action.remove": "සේවාදායකය ඉවත් කරන්න", + "dialog.server.menu.edit": "සංස්කරණය කරන්න", + "dialog.server.menu.default": "පෙරනිමිය ලෙස සකසන්න", + "dialog.server.menu.defaultRemove": "පෙරනිමිය ඉවත් කරන්න", + "dialog.server.menu.delete": "මකන්න", + "dialog.server.current": "වත්මන් සේවාදායකය", + "dialog.server.status.default": "පෙරනිමිය", + "wsl.server.add": "WSL සේවාදායකය එක් කරන්න", + "wsl.server.addShort": "WSL එක් කරන්න", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL සේවාදායකය", + "wsl.server.retryStart": "නැවත ආරම්භ කිරීමට උත්සාහ කරන්න", + "wsl.server.updating": "යාවත්කාලීන කරමින්...", + "wsl.onboarding.step.distro": "ඩිස්ට්‍රෝ තෝරන්න", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL පරීක්ෂා කරමින්...", + "wsl.onboarding.restartRequired": "WSL ස්ථාපනය අවසන් කිරීමට Windows හට නැවත ආරම්භයක් අවශ්‍ය වේ.", + "wsl.onboarding.ready": "WSL සූදානම්.", + "wsl.onboarding.required": "දිගටම කරගෙන යාමට WSL අවශ්‍යයි.", + "wsl.onboarding.checkingDistros": "බෙදාහැරීම් පරීක්ෂා කරමින්...", + "wsl.onboarding.installingDistro": "{{distro}} ස්ථාපනය කරමින්...", + "wsl.onboarding.checkingDistro": "{{distro}} පරීක්ෂා කරමින්...", + "wsl.onboarding.listingDistros": "බෙදාහැරීම් ලැයිස්තුගත කරමින්...", + "wsl.onboarding.distroReady": "{{distro}} සූදානම්.", + "wsl.onboarding.distroNotInstalled": "{{distro}} තවම ස්ථාපනය කර නැත.", + "wsl.onboarding.openDistroOnce": "පිහිටුවීම අවසන් කිරීමට {{distro}} වරක් විවෘත කරන්න.", + "wsl.onboarding.finishingDistro": "{{distro}} සඳහා පිහිටුවීම අවසන් කරමින්.", + "wsl.onboarding.pickDistro": "ඩිස්ට්‍රෝ එකක් තෝරන්න හෝ පහත එකක් ස්ථාපනය කරන්න.", + "wsl.onboarding.checkingOpencode": "OpenCode පරීක්ෂා කරමින්...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}} හි OpenCode පරීක්ෂා කරමින්...", + "wsl.onboarding.updatingOpencode": "OpenCode යාවත්කාලීන කරමින්...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}} හි OpenCode යාවත්කාලීන කරමින්...", + "wsl.onboarding.updateOpencodeIn": "{{distro}} හි OpenCode යාවත්කාලීන කරන්න.", + "wsl.onboarding.updateOpencode": "OpenCode යාවත්කාලීන කරන්න", + "wsl.onboarding.opencodeReadyIn": "OpenCode {{distro}} හි සූදානම්ය.", + "wsl.onboarding.opencodeReady": "OpenCode සූදානම්.", + "wsl.onboarding.installOpencodeIn": "{{distro}} හි OpenCode ස්ථාපනය කරන්න.", + "wsl.onboarding.installOpencode": "OpenCode ස්ථාපනය කරන්න", + "wsl.onboarding.chooseDistroFirst": "මුලින්ම ඩිස්ට්‍රෝ එකක් තෝරන්න.", + "wsl.onboarding.loadFailed": "WSL තත්ත්වය පූරණය කිරීමට අසමත් විය.", + "wsl.onboarding.loading": "පූරණය වෙමින්...", + "wsl.onboarding.installedDistros": "ස්ථාපිත distros", + "wsl.onboarding.checkAgain": "නැවත පරීක්ෂා කරන්න", + "wsl.onboarding.distroStatus.ready": "සූදානම්", + "wsl.onboarding.distroStatus.checking": "පරීක්ෂා කරමින්...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode ස්ථාපනය කර නැත", + "wsl.onboarding.distroStatus.missingTools": "නැති bash, curl", + "wsl.onboarding.distroStatus.unsupported": "සහාය නොදක්වයි · WSL 2 භාවිතා කරන්න", + "wsl.onboarding.needAnotherDistro": "තවත් බෙදාහැරීමක් අවශ්‍යද?", + "wsl.onboarding.needAnotherDistroHint": "WSL නාමාවලියෙන් Linux බෙදාහැරීමක් ස්ථාපනය කරන්න", + "wsl.onboarding.wslNotInstalled.title": "WSL ස්ථාපනය කර නැත", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Linux සඳහා උප පද්ධතිය) OpenCode WSL සේවාදායකයක් එක් කිරීමට පෙර අවශ්‍ය වේ", + "wsl.onboarding.wslUnavailable.title": "WSL නොමැත", + "wsl.onboarding.wslUnavailable.description": "OpenCode හට මෙම යන්ත්‍රයේ WSL සත්‍යාපනය කළ නොහැක.", + "wsl.onboarding.installWsl": "WSL ස්ථාපනය කරන්න", + "wsl.onboarding.windowsRestartRequired": + "WSL ස්ථාපනය අවසන් කිරීමට Windows නැවත අරඹන්න, පසුව OpenCode නැවත විවෘත කරන්න.", + "wsl.onboarding.next": "ඊළඟ", + "wsl.onboarding.refresh": "නැවුම් කරන්න", + "wsl.onboarding.allDistrosAdded": "ස්ථාපිත සියලුම බෙදාහැරීම් දැනටමත් එකතු කර ඇත.", + "wsl.onboarding.noDistros": "තවමත් distros අනාවරණය කර ගෙන නොමැත.", + "wsl.onboarding.install": "ස්ථාපනය කරන්න", + "wsl.onboarding.installing": "ස්ථාපනය කරමින්...", + "wsl.onboarding.installDistro": "distro ස්ථාපනය කරන්න", + "wsl.onboarding.searchDistros": "බෙදාහැරීම් සොයන්න", + "wsl.onboarding.wsl2Required": "WSL 2 අවශ්‍යයි.", + "wsl.onboarding.toolsRequired": "මේ distro එකට bash සහ curl අවශ්‍යයි.", + "wsl.onboarding.openTerminal": "ටර්මිනලය විවෘත කරන්න", + "wsl.onboarding.path": "මාර්ගය: {{path}}", + "wsl.onboarding.notFound": "හමු වුණේ නැහැ", + "wsl.onboarding.version": "අනුවාදය: {{version}}", + "wsl.onboarding.unknown": "නොදන්නා", + "wsl.onboarding.desktopVersion": "ඩෙස්ක්ටොප් {{version}}", + "wsl.onboarding.versionMismatch": "ස්ථාපිත අනුවාදය ඩෙස්ක්ටොප් යෙදුම් අනුවාදයට නොගැලපේ.", + "wsl.onboarding.adding": "එකතු කරමින්...", + "help.tabs.toast.ariaLabel": "ටැබ් හඳුන්වා දීම. ටැබ් සමඟ ඔබේ වැඩ සහ සක්‍රිය සැසි සංවිධානය කරන්න", + "help.tabs.toast.dismiss": "ටැබ් තොරතුරු ඉවතලන්න", + "help.tabs.title": "ටැබ් හඳුන්වා දීම", + "help.tabs.description": "ටැබ් සමඟ ඔබේ වැඩ සහ සක්‍රිය සැසි සංවිධානය කරන්න", + "help.tabs.date": "ජූලි 14", + "help.tabs.introduction": "OpenCode ඩෙස්ක්ටොප් එක දැන් ටැබ් වටා ගොඩනගා ඇත.", + "help.tabs.sessions": + "ටැබ් එකක නව සැසියක් ආරම්භ කරන්න, නැතහොත් ඔබේ ඕනෑම ව්‍යාපෘතියකින් පවතින සැසියක් විවෘත කරන්න. ඔබ අලුත් දෙයක් ආරම්භ කරන විට නව ටැබ් එකක් විවෘත කරන්න, ඔබ අවසන් වූ පසු එය වසා දමන්න.", + "help.tabs.organize": + "ටැබ් කිහිපයක් විවෘතව තබා ගැනීම ඔබගේ සක්‍රිය සැසි සංවිධානය කිරීම පහසු කරයි. ඔබ ඒවා අවට තබා ගැනීමට අදහස් කරන්නේ නම් අමතක නොවන දෙයකට ටැබ් නැවත නම් කරන්න.", + "help.tabs.home": + "ඔබ ඔබගේ සියලුම සැසි සහ ව්‍යාපෘති නව මුල් තිරය මත සොයා ගනු ඇත. සැසියක් තේරීමෙන් එය ටැබ් එකකින් විවෘත වේ.", + "help.tabs.persistence": "ඔබ යෙදුම නැවත විවෘත කරන විට, ඔබේ ටැබ් තවමත් විවෘතයි.", + "help.tabs.worktrees": + "නව සැලසුම තවමත් Git Worktrees සඳහා සහය නොදක්වයි, එය ඉක්මනින් පැමිණේ. එබැවින් ඔබ පෙර පිරිසැලසුම දිගටම භාවිතා කිරීමට කැමති නම්, ඔබට සැකසීම් තුළ පිරිසැලසුම් අතර මාරු විය හැක. නව පිරිසැලසුම සති කිහිපයකින් ස්ථිර වනු ඇති බව මතක තබා ගන්න.", + "server.row.noUsername": "පරිශීලක නාමයක් නැත", + "dialog.project.edit.title": "ව්‍යාපෘතිය සංස්කරණය කරන්න", + "dialog.project.edit.name": "නම", + "dialog.project.edit.icon": "නිරූපකය", + "dialog.project.edit.icon.alt": "ව්යාපෘති නිරූපකය", + "dialog.project.edit.icon.hint": "පින්තූරයක් ක්ලික් කරන්න හෝ අදින්න", + "dialog.project.edit.icon.recommended": "නිර්දේශිත: 128x128px", + "dialog.project.edit.color": "වර්ණය", + "dialog.project.edit.color.select": "{{color}} වර්ණය තෝරන්න", + "dialog.project.edit.worktree.startup": "වැඩබිම් ආරම්භක ස්ක්‍රිප්ට්", + "dialog.project.edit.worktree.startup.description": "නව වැඩබිමක් (Git වර්ක්ට්‍රී) සෑදීමෙන් පසු ධාවනය වේ.", + "dialog.project.edit.worktree.startup.placeholder": "උදා. බන් ස්ථාපනය", + "dialog.releaseNotes.action.getStarted": "පටන් ගන්න", + "dialog.releaseNotes.action.next": "ඊළඟ", + "dialog.releaseNotes.action.hideFuture": "මේවා ඉස්සරහට පෙන්නන්න එපා", + "dialog.releaseNotes.media.alt": "පෙරදසුන නිකුත් කරන්න", + "context.breakdown.title": "සන්දර්භය බිඳවැටීම", + "context.breakdown.note": 'ආදාන ටෝකනවල ආසන්න බිඳවැටීම. "වෙනත්" යන්නෙහි මෙවලම් අර්ථ දැක්වීම් සහ පොදු කාර්ය ඇතුළත් වේ.', + "context.breakdown.system": "පද්ධතිය", + "context.breakdown.user": "පරිශීලක", + "context.breakdown.assistant": "සහකාර", + "context.breakdown.tool": "මෙවලම් ඇමතුම්", + "context.breakdown.other": "වෙනත්", + "context.systemPrompt.title": "පද්ධති ප්‍රොම්ප්ට්", + "context.rawMessages.title": "අමු පණිවිඩ", + "context.export.session": "අපනයන සැසිය", + "context.stats.session": "සැසිය", + "context.stats.messages": "පණිවිඩ", + "context.stats.provider": "සපයන්නා", + "context.stats.model": "ආකෘතිය", + "context.stats.limit": "සන්දර්භය සීමාව", + "context.stats.totalTokens": "මුළු ටෝකන", + "context.stats.usage": "භාවිතය", + "context.stats.inputTokens": "ආදාන ටෝකන", + "context.stats.outputTokens": "ප්රතිදාන ටෝකන", + "context.stats.reasoningTokens": "තර්ක ටෝකන", + "context.stats.cacheTokens": "හැඹිලි ටෝකන (කියවීමට/ලිවීමට)", + "context.stats.userMessages": "පරිශීලක පණිවිඩ", + "context.stats.assistantMessages": "සහකාර පණිවිඩ", + "context.stats.totalCost": "මුළු පිරිවැය", + "context.stats.sessionCreated": "සැසිය නිර්මාණය කරන ලදී", + "context.stats.lastActivity": "අවසාන ක්‍රියාකාරකම", + "context.usage.tokens": "ටෝකන", + "context.usage.usage": "භාවිතය", + "context.usage.cost": "පිරිවැය", + "context.usage.clickToView": "සන්දර්භය බැලීමට ක්ලික් කරන්න", + "context.usage.view": "සන්දර්භය භාවිතය බලන්න", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "භාෂාව", + "toast.language.description": "{{language}} වෙත මාරු විය", + "toast.theme.title": "තේමාව මාරු විය", + "toast.scheme.title": "වර්ණ පටිපාටිය", + "toast.workspace.enabled.title": "වැඩබිම් සබල කර ඇත", + "toast.workspace.enabled.description": "Git වර්ක්ට්‍රී කිහිපයක් දැන් පැති තීරුවේ පෙන්වයි", + "toast.workspace.disabled.title": "වැඩබිම් අබල කර ඇත", + "toast.workspace.disabled.description": "පැති තීරුවේ ප්‍රධාන Git වර්ක්ට්‍රී එක පමණක් පෙන්වයි", + "toast.permissions.autoaccept.on.title": "ස්වයං-පිළිගැනීමේ අවසර", + "toast.permissions.autoaccept.on.description": "අවසර ඉල්ලීම් ස්වයංක්‍රීයව අනුමත වේ", + "toast.permissions.autoaccept.off.title": "අවසර ස්වයංක්‍රීයව පිළිගැනීම නතර විය", + "toast.permissions.autoaccept.off.description": "අවසර ඉල්ලීම් සඳහා අනුමැතිය අවශ්‍ය වනු ඇත", + "toast.model.none.title": "ආකෘතියක් තෝරා නැත", + "toast.model.none.description": "මෙම සැසිය සාරාංශ කිරීමට සැපයුම්කරුවෙකු සම්බන්ධ කරන්න", + "toast.file.loadFailed.title": "ගොනුව පූරණය කිරීමට අසමත් විය", + "toast.file.listFailed.title": "ගොනු ලැයිස්තුගත කිරීමට අසමත් විය", + "toast.context.noLineSelection.title": "රේඛා තේරීමක් නැත", + "toast.context.noLineSelection.description": "පළමුව ගොනු පටිත්තක රේඛා පරාසයක් තෝරන්න.", + "toast.session.share.copyFailed.title": "පසුරු පුවරුවට URL පිටපත් කිරීමට අසමත් විය", + "toast.session.share.success.title": "සැසිය බෙදා ගත්තා", + "toast.session.share.success.description": "බෙදාගන්න URL පසුරු පුවරුවට පිටපත් කර ඇත!", + "toast.session.share.failed.title": "සැසිය බෙදා ගැනීමට අසමත් විය", + "toast.session.share.failed.description": "සැසිය බෙදා ගැනීමේදී දෝෂයක් ඇති විය", + "toast.session.unshare.success.title": "සැසිය බෙදා නොගන්නා ලදී", + "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": "යාවත්කාලීන ලබා ගත හැක", + "toast.update.description": "OpenCode හි නව අනුවාදයක් ({{version}}) ස්ථාපනය කිරීමට දැන් තිබේ.", + "toast.update.action.installRestart": "ස්ථාපනය කර නැවත ආරම්භ කරන්න", + "toast.update.action.notYet": "තවමත් නෑ", + "error.page.title": "යමක් වැරදී ඇත", + "error.page.description": "යෙදුම පූරණය කිරීමේදී දෝෂයක් ඇති විය.", + "error.page.description.localServerStartup": "දේශීය සේවාදායකය ආරම්භ කිරීමේදී දෝෂයක් ඇති විය.", + "error.page.details.label": "දෝෂ විස්තර", + "error.page.action.restart": "යළි අරඹන්න", + "error.page.action.report": "දෝෂය වාර්තා කරන්න", + "error.page.action.reported": "දෝෂය වාර්තා විය", + "error.page.action.exportLogs": "ලොග් අපනයනය කරන්න", + "error.page.action.checking": "පරීක්ෂා කරමින්...", + "error.page.action.checkUpdates": "යාවත්කාලීන සඳහා පරීක්ෂා කරන්න", + "error.page.action.updateTo": "{{version}} වෙත යාවත්කාලීන කරන්න", + "error.page.circular": "[චක්‍රලේඛය]", + "error.page.report.prefix": "කරුණාකර මෙම දෝෂය OpenCode කණ්ඩායමට වාර්තා කරන්න", + "error.page.report.discord": "Discord මත", + "error.page.version": "අනුවාදය: {{version}}", + "error.dev.rootNotFound": + "මූල මූලද්රව්යය හමු නොවීය. ඔබට එය ඔබගේ index.html වෙත එක් කිරීමට අමතකද? එසේත් නැතිනම් හැඳුනුම්පත වැරදි ලෙස සටහන් වී තිබේද?", + "error.serverSync.connectFailed": "සේවාදායකයට සම්බන්ධ වීමට නොහැකි විය. `{{url}}` හි ධාවනය වන සේවාදායකයක් තිබේද?", + "error.serverSDK.noServerAvailable": "සේවාදායකයක් නොමැත", + "error.serverSDK.serverNotAvailable": "සේවාදායකය නොමැත", + "error.childStore.persistedCacheCreateFailed": "නොනැසී පවතින හැඹිලිය සෑදීමට අසමත් විය", + "error.childStore.persistedProjectMetadataCreateFailed": "අඛණ්ඩ ව්‍යාපෘති පාර-දත්ත නිර්මාණය කිරීමට අසමත් විය", + "error.childStore.persistedProjectIconCreateFailed": "නොනැසී පවතින ව්‍යාපෘති නිරූපකය සෑදීමට අසමත් විය", + "error.childStore.storeCreateFailed": "ගබඩාව සෑදීමට අසමත් විය", + "directory.error.invalidUrl": "URL හි වලංගු නොවන නාමාවලිය.", + "error.chain.unknown": "නොදන්නා දෝෂයකි", + "error.server.invalidConfiguration": "වලංගු නොවන වින්‍යාසය", + "error.chain.causedBy": "හේතුව:", + "error.chain.apiError": "API දෝෂයකි", + "error.chain.status": "තත්ත්වය: {{status}}", + "error.chain.retryable": "නැවත උත්සාහ කළ හැකි: {{retryable}}", + "error.chain.responseBody": "ප්‍රතිචාර අන්තර්ගතය:\n{{body}}", + "error.chain.didYouMean": "ඔබ අදහස් කළේ: {{suggestions}}", + "error.chain.modelNotFound": "ආකෘතිය හමු නොවීය: {{provider}}/{{model}}", + "error.chain.checkConfig": "ඔබගේ වින්‍යාසය (opencode.json) සපයන්නා/ආදර්ශ නම් පරීක්ෂා කරන්න", + "error.chain.mcpFailed": 'MCP සේවාදායකය "{{name}}" අසාර්ථක විය. සටහන, OpenCode තවමත් MCP සත්‍යාපනයට සහය නොදක්වයි.', + "error.chain.providerAuthFailed": "සැපයුම්කරු සත්‍යාපනය අසාර්ථක විය ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + '"{{provider}}" සපයන්නා ආරම්භ කිරීමට අසමත් විය. අක්තපත්ර සහ වින්යාසය පරීක්ෂා කරන්න.', + "error.chain.configJsonInvalid": "{{path}} හි වින්‍යාස ගොනුව වලංගු නොවේ JSON(C)", + "error.chain.configJsonInvalidWithMessage": "{{path}} හි වින්‍යාස ගොනුව වලංගු නොවේ JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} හි "{{dir}}" නාමාවලිය වලංගු නොවේ. නාමාවලිය "{{suggestion}}" ලෙස නැවත නම් කරන්න හෝ එය ඉවත් කරන්න. මෙය පොදු අක්ෂර වින්‍යාසයකි.', + "error.chain.configFrontmatterError": "{{path}}:\n{{message}} හි මූලික කරුණු විග්‍රහ කිරීමට අසමත් විය", + "error.chain.configInvalid": "{{path}} හි වින්‍යාස ගොනුව වලංගු නොවේ", + "error.chain.configInvalidWithMessage": "{{path}} හි වින්‍යාස ගොනුව වලංගු නොවේ: {{message}}", + "notification.permission.title": "අවසරය අවශ්‍යයි", + "notification.permission.description": "{{projectName}} හි {{sessionTitle}} හට අවසර අවශ්‍යයි", + "notification.question.title": "ප්රශ්නය", + "notification.question.description": "{{projectName}} හි {{sessionTitle}} හට ප්‍රශ්නයක් ඇත", + "notification.action.goToSession": "සැසිය වෙත යන්න", + "notification.session.responseReady.title": "ප්‍රතිචාරය සූදානම්", + "notification.session.error.title": "සැසි දෝෂය", + "notification.session.error.fallbackDescription": "දෝෂයක් සිදු විය", + "home.recentProjects": "මෑත ව්යාපෘති", + "home.empty.title": "මෑත ව්යාපෘති නැත", + "home.empty.description": "දේශීය ව්‍යාපෘතියක් විවෘත කිරීමෙන් ආරම්භ කරන්න", + "home.title": "නිවස", + "home.projects": "ව්යාපෘති", + "home.project.add": "ව්යාපෘතිය එකතු කරන්න", + "home.recentlyClosed": "මෑතකදී වසා ඇත", + "home.server.collapse": "සේවාදායක ව්‍යාපෘති හකුළන්න", + "home.server.expand": "සේවාදායක ව්‍යාපෘති පුළුල් කරන්න", + "home.sessions.search.placeholder": "සෙවුම් සැසි", + "home.sessions.search.placeholder.scoped": "{{scope}} හි සැසි සොයන්න", + "home.sessions.search.sessions": "සැසි", + "home.sessions.search.noResults": "{{query}} සඳහා සැසි හමු නොවීය", + "home.sessions.empty": "තවමත් මෙහි කිසිවක් නැත", + "home.sessions.empty.description": "ආරම්භ කිරීමට සැසියක් සාදන්න", + "home.sessions.group.today": "අද", + "home.sessions.group.yesterday": "ඊයේ", + "home.sessions.group.older": "වැඩිහිටි", + "home.providerTip": "Claude, GPT, Gemini, ආදිය ඇතුළුව අනෙකුත් මාදිලි භාවිතා කිරීමට 75+ සපයන්නන් වෙත සම්බන්ධ වන්න", + "session.tab.session": "සැසිය", + "session.tab.review": "සමාලෝචනය කරන්න", + "session.tab.context": "සන්දර්භය", + "session.tab.unknown": "නොදන්නා සැසිය", + "session.panel.reviewAndFiles": "සමාලෝචනය සහ ගොනු", + "session.error.notFound": "මෙම සැසිය සොයාගත නොහැක", + "session.error.notFound.description": "මෙම පටිත්ත මෙම සේවාදායකයේ තවදුරටත් නොපවතින සැසියක් වෙත යොමු කරයි.", + "session.error.notFound.closeTab": "ටැබය වසන්න", + "session.error.serverConnection": "මෙම සේවාදායකයට සම්බන්ධ විය නොහැක", + "session.review.filesChanged": "ගොනු {{count}} වෙනස් කරන ලදී", + "session.review.change.one": "වෙනස් කරන්න", + "session.review.change.other": "වෙනස්කම්", + "session.review.loadingChanges": "වෙනස්කම් පූරණය කරමින්...", + "session.review.empty": "මෙම සැසියේ තවමත් වෙනස්කම් නොමැත", + "session.review.noVcs": "Git අනුවාද පාලන පද්ධතියක් අනාවරණය වී නැත, වෙනස්කම් දර්ශනය නොවේ", + "session.review.noVcs.createGit.title": "Git ගබඩාවක් සාදන්න", + "session.review.noVcs.createGit.description": "මෙම ව්‍යාපෘතියේ වෙනස්කම් හඹා යන්න, සමාලෝචනය කරන්න, සහ පසුගමනය කරන්න", + "session.review.noVcs.createGit.actionLoading": "Git ගබඩාව නිර්මාණය කරමින්...", + "session.review.noVcs.createGit.action": "Git ගබඩාවක් සාදන්න", + "session.review.noSnapshot": "Snapshot ලුහුබැඳීම වින්‍යාසය තුළ අබල කර ඇත, එබැවින් සැසි වෙනස්කම් නොමැත", + "session.review.noChanges": "වෙනස්කම් නොමැත", + "session.review.noUncommittedChanges": "තවමත් කැප නොකළ වෙනස්කම් නොමැත", + "session.review.noBranchChanges": "තවම ශාඛාවේ වෙනසක් නැත", + "session.files.selectToOpen": "විවෘත කිරීමට ගොනුවක් තෝරන්න", + "session.files.all": "සියලුම ගොනු", + "session.files.empty": "ගොනු නැත", + "session.files.binaryContent": "ද්විමය ගොනුව (අන්තර්ගතය පෙන්විය නොහැක)", + "session.messages.renderEarlier": "පෙර පණිවිඩ ලබා දෙන්න", + "session.messages.loadingEarlier": "පෙර පණිවිඩ පූරණය කරමින්...", + "session.messages.loadEarlier": "කලින් පණිවිඩ පූරණය කරන්න", + "session.messages.loading": "පණිවිඩ පූරණය කරමින්...", + "session.messages.jumpToLatest": "නවතම වෙත පනින්න", + "session.context.addToContext": "සන්දර්භය වෙත {{selection}} එක් කරන්න", + "session.todo.title": "ටෝඩෝස්", + "session.todo.collapse": "හකුළන්න", + "session.todo.expand": "පුළුල් කරන්න", + "session.todo.progress": "{{total}} න් {{done}} සම්පූර්ණයි", + "session.question.progress": "{{current}} {{total}} ප්‍රශ්න", + "session.question.minimize": "ප්‍රශ්නය අවම කරන්න", + "session.question.restore": "ප්‍රශ්නය ප්‍රතිසාධනය කරන්න", + "session.question.pending.one": "{{count}} පොරොත්තු ප්‍රශ්නය", + "session.question.pending.other": "{{count}} පොරොත්තු ප්‍රශ්න", + "session.followupDock.summary.one": "{{count}} පෝලිම් පණිවිඩය", + "session.followupDock.summary.other": "{{count}} පෝලිම්ගත පණිවිඩ", + "session.followupDock.sendNow": "දැන් එවන්න", + "session.followupDock.edit": "සංස්කරණය කරන්න", + "session.followupDock.collapse": "පෝලිම්ගත පණිවිඩ හකුළන්න", + "session.followupDock.expand": "පෝලිම් පණිවිඩ දිග හරින්න", + "session.revertDock.summary.one": "{{count}} පෙරළන ලද පණිවිඩය", + "session.revertDock.summary.other": "{{count}} පණිවිඩ ආපසු හරවන ලදී", + "session.revertDock.collapse": "පෙරළන ලද පණිවිඩ හකුළන්න", + "session.revertDock.expand": "පෙරළන ලද පණිවිඩ පුළුල් කරන්න", + "session.revertDock.restore": "පණිවිඩය ප්‍රතිසාධනය කරන්න", + "session.new.title": "ඕනෑම දෙයක් ගොඩනඟන්න", + "session.new.project.new": "නව ව්යාපෘතිය", + "session.new.project.search": "ව්යාපෘති සොයන්න", + "session.new.project.add": "ව්යාපෘතිය එකතු කරන්න", + "session.new.worktree.main": "ප්රධාන ශාඛාව", + "session.new.worktree.mainWithBranch": "ප්‍රධාන ශාඛාව ({{branch}})", + "session.new.worktree.create": "නව Git වර්ක්ට්‍රී එකක් සාදන්න", + "session.new.workspace.runIn": "සැසිය ධාවනය කරන්න", + "session.new.workspace.triggerLocal": "දේශීය", + "session.new.workspace.local": "දේශීය ගබඩාව", + "session.new.workspace.existing": "වැඩබිම…", + "session.new.git.none": "Git නැත", + "session.new.lastModified": "අවසන් වරට වෙනස් කරන ලදී", + "session.header.search.placeholder": "{{project}} සොයන්න", + "session.header.searchFiles": "ගොනු සොයන්න", + "session.header.openIn": "විවෘත කරන්න", + "session.header.open.action": "{{app}} විවෘත කරන්න", + "session.header.open.ariaLabel": "{{app}} හි විවෘත කරන්න", + "session.header.open.menu": "විකල්ප විවෘත කරන්න", + "session.header.open.copyPath": "මාර්ගය පිටපත් කරන්න", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "File Manager", + "session.header.reveal.finder": "Finder තුළ හෙළි කරන්න", + "session.header.reveal.fileExplorer": "File Explorer හි හෙළි කරන්න", + "session.header.reveal.containingFolder": "අඩංගු ෆෝල්ඩරය විවෘත කරන්න", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "පර්යන්තය", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "තත්ත්වය", + "status.popover.ariaLabel": "සේවාදායක සැකසුම්", + "status.popover.tab.servers": "සේවාදායකයන්", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "ප්ලගීන", + "status.popover.action.manageServers": "සේවාදායකයන් කළමනාකරණය කරන්න", + "session.share.popover.title": "වෙබයේ පළ කරන්න", + "session.share.popover.description.shared": "මෙම සැසිය වෙබයේ පොදු වේ. සබැඳිය ඇති ඕනෑම කෙනෙකුට එය ප්‍රවේශ විය හැකිය.", + "session.share.popover.description.unshared": + "වෙබයේ ප්‍රසිද්ධියේ සැසිය බෙදා ගන්න. සබැඳිය ඇති ඕනෑම කෙනෙකුට එය ප්‍රවේශ විය හැකිය.", + "session.share.action.share": "බෙදාගන්න", + "session.share.action.publish": "ප්‍රකාශ කරන්න", + "session.share.action.publishing": "ප්‍රකාශනය කරමින්...", + "session.share.action.unpublish": "ප්‍රකාශනය ඉවත් කරන්න", + "session.share.action.unpublishing": "ප්‍රකාශනය ඉවත් කරමින්...", + "session.share.action.view": "බලන්න", + "session.share.copy.copied": "පිටපත් කර ඇත", + "session.share.copy.copyLink": "සබැඳිය පිටපත් කරන්න", + "lsp.tooltip.none": "LSP සේවාදායකයන් නොමැත", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "ප්‍රොම්ප්ට් එක පූරණය වෙමින්...", + "terminal.loading": "පර්යන්තය පූරණය කරමින්...", + "terminal.title": "පර්යන්තය", + "terminal.title.numbered": "පර්යන්තය {{number}}", + "terminal.close": "පර්යන්තය වසන්න", + "terminal.connectionLost.title": "සම්බන්ධතාවය නැති විය", + "terminal.connectionLost.abnormalClose": "WebSocket අසාමාන්‍ය ලෙස වසා ඇත: {{code}}", + "terminal.connectionLost.description": + "පර්යන්ත සම්බන්ධතාවයට බාධා ඇති විය. සේවාදායකය නැවත ආරම්භ වන විට මෙය සිදු විය හැක.", + "terminal.connectTicket.csrfError": + "PTY සම්බන්ධක ටිකට් පත සම්භවය හෝ CSRF චෙක්පත් මගින් ප්‍රතික්ෂේප විය. සේවාදායකය CORS වින්‍යාසය පරීක්ෂා කරන්න.", + "terminal.connectTicket.statusError": "{{status}} සමඟ PTY සම්බන්ධක ප්‍රවේශපත්‍රය අසාර්ථක විය", + "titlebar.update": "යාවත්කාලීන කරන්න", + "titlebar.updateVersion": "{{version}} යාවත්කාලීන කරන්න", + "common.closeTab": "ටැබ් එක වසන්න", + "common.dismiss": "අස් කරන්න", + "common.moreCountSuffix": "(+{{count}} තව)", + "common.requestFailed": "ඉල්ලීම අසාර්ථක විය", + "common.moreOptions": "තවත් විකල්ප", + "common.learnMore": "තවත් හදාරන්න", + "common.rename": "නැවත නම් කරන්න", + "common.export": "අපනයනය කරන්න", + "common.reset": "යළි පිහිටුවන්න", + "common.archive": "සංරක්ෂිතය", + "common.delete": "මකන්න", + "common.close": "වසන්න", + "common.edit": "සංස්කරණය කරන්න", + "common.loadMore": "වැඩිපුර පටවන්න", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "නොදන්නා", + "common.time.justNow": "මේ දැන්", + "common.time.minutesAgo.short": "{{count}}m පෙර", + "common.time.hoursAgo.short": "{{count}}පැයට පෙර", + "common.time.daysAgo.short": "{{count}}d පෙර", + "sidebar.menu.toggle": "මෙනුව ටොගල් කරන්න", + "sidebar.nav.projectsAndSessions": "ව්යාපෘති සහ සැසි", + "sidebar.settings": "සැකසීම්", + "sidebar.help": "උදව්", + "sidebar.workspaces.enable": "වැඩබිම් සබල කරන්න", + "sidebar.workspaces.disable": "වැඩබිම් අබල කරන්න", + "sidebar.gettingStarted.title": "ඇරඹේ", + "sidebar.gettingStarted.line1": "OpenCode නොමිලේ ආකෘති ඇතුළත් වන නිසා ඔබට වහාම ආරම්භ කළ හැක.", + "sidebar.gettingStarted.line2": + "ආකෘති භාවිතා කිරීමට ඕනෑම සැපයුම්කරුවෙකු සම්බන්ධ කරන්න, inc. Claude, GPT, Gemini ආදිය.", + "sidebar.project.recentSessions": "මෑත සැසි", + "sidebar.project.viewAllSessions": "සියලුම සැසි බලන්න", + "sidebar.project.clearNotifications": "දැනුම්දීම් හිස් කරන්න", + "sidebar.empty.title": "ව්‍යාපෘති විවෘත නැත", + "sidebar.empty.description": "ආරම්භ කිරීමට ව්‍යාපෘතියක් විවෘත කරන්න", + "debugBar.ariaLabel": "සංවර්ධන කාර්ය සාධන රෝග විනිශ්චය", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "සැසි පිටුවක් ස්පර්ශ කරමින් අවසන් වරට සම්පුර්ණ කරන ලද මාර්ග සංක්‍රාන්තිය, රවුටරය ආරම්භයේ සිට එය නිරවුල් වූ පසු පළමු තීන්ත දක්වා මනිනු ලැබේ.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "පසුගිය තත්පර 5 තුළ තත්පරයට රාමු පෙරළීම.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "පසුගිය තත්පර 5 තුළ නරකම රාමු කාලය.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "අවසන් තත්පර 5 තුළ 32ms ට වැඩි රාමු.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "අවසාන තත්පර 5 තුළ අවහිර කළ කාලය සහ දිගු කාර්ය ගණන. උපරිම කාර්යය: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "පසුගිය තත්පර 5 තුළ නරකම නිරීක්ෂණය කළ ආදාන ප්‍රමාදය.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "පසුගිය තත්පර 5 තුළ ආසන්න අන්තර්ක්‍රියා කාලය. මෙය INP-වැනි, නිල Web Vitals INP නොවේ.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "වත්මන් යෙදුම් ආයු කාලය සඳහා සමුච්චිත පිරිසැලසුම් මාරුව.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "භාවිතා කරන ලද JS heap එදිරිව ගොඩ සීමාව. Chromium පමණි.", + "debugBar.mem.tip": "භාවිතා කරන ලද JS heap එදිරිව ගොඩ සීමාව. {{limit}} හි {{used}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "සියලු අන්තර්ක්‍රියාකාරී මූලද්‍රව්‍ය කෙරෙහි අවධානය යොමු කිරීමේ ශෛලීන් බල කරන්න", + "debugBar.focus.on": "ක්‍රියාත්මකයි", + "debugBar.focus.off": "අක්රියයි", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "තෝරාගත් භාෂාව වෙනස් නොකර සම්පූර්ණ යෙදුම් පිරිසැලසුම් දිශාව බල කරන්න", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode ඩෙස්ක්ටොප්", + "settings.section.desktop": "ඩෙස්ක්ටොප්", + "settings.section.server": "සේවාදායකය", + "settings.tab.general": "ජෙනරාල්", + "settings.tab.shortcuts": "කෙටිමං", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL ඒකාබද්ධ කිරීම", + "settings.desktop.wsl.description": "Windows හි WSL තුළ OpenCode සේවාදායකය ධාවනය කරන්න.", + "settings.general.section.appearance": "පෙනුම", + "settings.general.section.advanced": "උසස්", + "settings.general.section.notifications": "පද්ධති දැනුම්දීම්", + "settings.general.section.updates": "යාවත්කාලීන", + "settings.general.section.sounds": "ශබ්ද ප්රයෝග", + "settings.general.section.feed": "පෝෂණය කරන්න", + "settings.general.section.display": "ප්රදර්ශනය කරන්න", + "settings.general.row.language.title": "භාෂාව", + "settings.general.row.language.description": "OpenCode සඳහා සංදර්ශක භාෂාව වෙනස් කරන්න", + "settings.general.row.shell.title": "පර්යන්ත ෂෙල්", + "settings.general.row.shell.description": "පර්යන්තය සහ නියෝජිත මෙවලම් භාවිතා කරන ෂෙල්", + "settings.general.row.shell.autoDefault": "ස්වයංක්‍රීය (පෙරනිමි)", + "settings.general.row.shell.terminalOnly": "පර්යන්තය පමණි", + "settings.general.row.appearance.title": "පෙනුම", + "settings.general.row.appearance.description": "ඔබගේ උපාංගයේ OpenCode පෙනෙන ආකාරය අභිරුචිකරණය කරන්න", + "settings.general.row.colorScheme.title": "වර්ණ පටිපාටිය", + "settings.general.row.colorScheme.description": "OpenCode පද්ධතිය, ආලෝකය හෝ අඳුරු තේමාව අනුගමනය කරන්නේද යන්න තෝරන්න", + "settings.general.row.theme.title": "තේමාව", + "settings.general.row.theme.description": "OpenCode තේමා කර ඇති ආකාරය අභිරුචිකරණය කරන්න.", + "settings.general.row.font.title": "කේත අකුරු", + "settings.general.row.font.description": "කේත බ්ලොක් වල භාවිතා කරන අකුරු අභිරුචිකරණය කරන්න", + "settings.general.row.terminalFont.title": "ටර්මිනල් අකුරු", + "settings.general.row.terminalFont.description": "ටර්මිනලයේ භාවිතා කරන අකුරු අභිරුචිකරණය කරන්න", + "settings.general.row.uiFont.title": "UI අකුරු", + "settings.general.row.uiFont.description": "අතුරු මුහුණත පුරා භාවිතා කරන අකුරු අභිරුචිකරණය කරන්න", + "settings.general.row.followup.title": "පසු විපරම් හැසිරීම", + "settings.general.row.followup.description": + "පසු විපරම් ප්‍රේරක ක්ෂණිකව ගමන් කරනවාද නැතිනම් පෝලිමක රැඳී සිටිනවාද යන්න තෝරන්න", + "settings.general.row.followup.option.queue": "පෝලිමේ", + "settings.general.row.followup.option.steer": "මෙහෙයවන්න", + "settings.general.row.showFileTree.title": "ගොනු ගස", + "settings.general.row.showFileTree.description": "සැසි තුළ ගොනු ගස් පැනලය පෙන්වන්න", + "settings.general.row.showNavigation.title": "සංචාලන පාලන", + "settings.general.row.showNavigation.description": "ඩෙස්ක්ටොප් මාතෘකා තීරුවේ පසුපස සහ ඉදිරි බොත්තම් පෙන්වන්න", + "settings.general.row.showSearch.title": "විධාන පුවරුව", + "settings.general.row.showSearch.description": "මාතෘකා තීරුවේ සෙවුම් සහ විධාන පුවරුවේ බොත්තම පෙන්වන්න", + "settings.general.row.showTerminal.title": "පර්යන්තය", + "settings.general.row.showTerminal.description": "ඩෙස්ක්ටොප් මාතෘකා තීරුවේ ටර්මිනල් බොත්තම පෙන්වන්න", + "settings.general.row.showStatus.title": "සේවාදායක තත්ත්වය", + "settings.general.row.showStatus.description": "මාතෘකා තීරුවේ සේවාදායක තත්ව බොත්තම පෙන්වන්න", + "settings.general.row.mobileTitlebarBottom.title": "පහළ සංචලනය", + "settings.general.row.mobileTitlebarBottom.description": "මාතෘකා තීරුව සහ සැසි ටැබ් ජංගම දුරකථනයේ තිරයේ පතුලේ තබන්න", + "settings.general.row.showCustomAgents.title": "නියෝජිතයා පෙන්වන්න", + "settings.general.row.showCustomAgents.description": + "නිර්මාපකයේ නියෝජිතයන් අතර මාරු වන්න. සැඟවුණු විට, බිල්ඩ් ඒජන්ත වෙත පෙරනිමි වේ.", + "settings.general.row.reasoningSummaries.title": "තර්ක සාරාංශ පෙන්වන්න", + "settings.general.row.reasoningSummaries.description": "කාලරේඛාව තුළ ආකෘති තර්ක සාරාංශ සංදර්ශන කරන්න", + "settings.general.row.shellToolPartsExpanded.title": "ෂෙල් මෙවලම් කොටස් පුළුල් කරන්න", + "settings.general.row.shellToolPartsExpanded.description": + "කාලරේඛාව තුළ පෙරනිමියෙන් පුළුල් කළ ෂෙල් මෙවලම් කොටස් පෙන්වන්න", + "settings.general.row.editToolPartsExpanded.title": "සංස්කරණ මෙවලම් කොටස් පුළුල් කරන්න", + "settings.general.row.editToolPartsExpanded.description": + "කාලරේඛාව තුළ පෙරනිමියෙන් පුළුල් කරන ලද සංස්කරණය, ලිවීම සහ පැච් මෙවලම් කොටස් පෙන්වන්න", + "settings.general.row.newInterface.title": "නව පිරිසැලසුම", + "settings.general.row.newInterface.badge": "අලුත්", + "settings.general.row.newInterface.description": + "නව ටැබ් සහ නිවසේ පිරිසැලසුම භාවිතා කරන්න. සීමිත කාලයක් සඳහා පිරිසැලසුම් අතර මාරු වන්න.", + "settings.general.row.newInterfaceNotice.title": "ඔබ දැන් නව පිරිසැලසුමක් භාවිතා කරයි", + "settings.general.row.newInterfaceNotice.description": "පෙර පිරිසැලසුම තවදුරටත් නොමැත", + "settings.general.row.newInterfaceNotice.dismiss": "අස් කරන්න", + "settings.general.row.pinchZoom.title": "විශාලනය කිරීමට පින්ච් කරන්න", + "settings.general.row.pinchZoom.description": + "විශාලනය කිරීමට ට්‍රැක්පෑඩ් පින්ච් කිරීමට සහ Ctrl-අනුචලන අභිනයන්ට ඉඩ දෙන්න", + "settings.general.row.wayland.title": "දේශීය Wayland භාවිත කරන්න", + "settings.general.row.wayland.description": "Wayland හි X11 ආපසු හැරීම අක්‍රීය කරන්න. නැවත ආරම්භ කිරීම අවශ්‍ය වේ.", + "settings.general.row.wayland.tooltip": + "මිශ්‍ර නැවුම් අනුපාත මොනිටර සහිත Linux මත, ස්වදේශික Wayland වඩා ස්ථායී විය හැක.", + "settings.general.row.releaseNotes.title": "නිකුත් කිරීමේ සටහන්", + "settings.general.row.releaseNotes.description": "යාවත්කාලීන කිරීමෙන් පසු නව උත්පතන මොනවාදැයි පෙන්වන්න", + "settings.updates.row.startup.title": "ආරම්භය පිළිබඳ යාවත්කාලීන සඳහා පරීක්ෂා කරන්න", + "settings.updates.row.startup.description": "OpenCode දියත් කරන විට යාවත්කාලීන සඳහා ස්වයංක්‍රීයව පරීක්ෂා කරන්න", + "settings.updates.row.check.title": "යාවත්කාලීන සඳහා පරීක්ෂා කරන්න", + "settings.updates.row.check.description": "යාවත්කාලීන සඳහා හස්තීයව පරීක්ෂා කර තිබේ නම් ස්ථාපනය කරන්න", + "settings.updates.action.checkNow": "දැන් පරීක්ෂා කරන්න", + "settings.updates.action.checking": "පරීක්ෂා කරමින්...", + "settings.updates.action.downloading": "බාගනිමින්...", + "settings.updates.action.installing": "ස්ථාපනය කරමින්...", + "settings.updates.toast.latest.title": "ඔබ යාවත්කාලීනයි", + "settings.updates.toast.latest.description": "ඔබ OpenCode හි නවතම අනුවාදය ධාවනය කරයි.", + "sound.option.none": "කිසිවක් නැත", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "නියෝජිතයා", + "settings.general.notifications.agent.description": + "නියෝජිතයා සම්පූර්ණ වූ විට හෝ අවධානය අවශ්‍ය වූ විට පද්ධති දැනුම්දීම පෙන්වන්න", + "settings.general.notifications.permissions.title": "අවසර", + "settings.general.notifications.permissions.description": "අවසරයක් අවශ්‍ය වූ විට පද්ධති දැනුම්දීම පෙන්වන්න", + "settings.general.notifications.errors.title": "දෝෂ", + "settings.general.notifications.errors.description": "දෝෂයක් ඇති වූ විට පද්ධති දැනුම්දීම පෙන්වන්න", + "settings.general.sounds.agent.title": "නියෝජිතයා", + "settings.general.sounds.agent.description": "නියෝජිතයා සම්පූර්ණ වූ විට හෝ අවධානය අවශ්‍ය වූ විට ශබ්දය වාදනය කරන්න", + "settings.general.sounds.permissions.title": "අවසර", + "settings.general.sounds.permissions.description": "අවසරයක් අවශ්‍ය විට ශබ්දය වාදනය කරන්න", + "settings.general.sounds.errors.title": "දෝෂ", + "settings.general.sounds.errors.description": "දෝෂයක් ඇති වූ විට ශබ්දය වාදනය කරන්න", + "settings.shortcuts.title": "යතුරුපුවරු කෙටිමං", + "settings.shortcuts.reset.button": "පෙරනිමියට යළි පිහිටුවන්න", + "settings.shortcuts.reset.toast.title": "කෙටිමං යළි පිහිටුවීම", + "settings.shortcuts.reset.toast.description": "යතුරුපුවරු කෙටිමං පෙරනිමියට යළි සකසා ඇත.", + "settings.shortcuts.conflict.title": "කෙටිමඟ දැනටමත් භාවිතයේ ඇත", + "settings.shortcuts.conflict.description": "{{keybind}} දැනටමත් {{titles}} වෙත පවරා ඇත.", + "settings.shortcuts.unassigned": "පැවරීම නොකළ", + "settings.shortcuts.pressKeys": "යතුරු ඔබන්න", + "settings.shortcuts.search.placeholder": "කෙටිමං සොයන්න", + "settings.shortcuts.search.empty": "කෙටිමං හමු නොවීය", + "settings.shortcuts.group.general": "ජෙනරාල්", + "settings.shortcuts.group.session": "සැසිය", + "settings.shortcuts.group.navigation": "සංචලනය", + "settings.shortcuts.group.modelAndAgent": "ආකෘතිය සහ නියෝජිතයා", + "settings.shortcuts.group.terminal": "පර්යන්තය", + "settings.shortcuts.group.prompt": "ප්‍රොම්ප්ට්", + "settings.providers.title": "සපයන්නන්", + "settings.providers.description": "සපයන්නාගේ සිටුවම් මෙහි වින්‍යාස කළ හැක.", + "settings.providers.section.connected": "සම්බන්ධිත සැපයුම්කරුවන්", + "settings.providers.connected.empty": "සම්බන්ධිත සපයන්නන් නැත", + "settings.providers.connected.environmentDescription": "ඔබගේ පරිසර විචල්‍ය වලින් සම්බන්ධ කර ඇත", + "settings.providers.section.popular": "ජනප්‍රිය සැපයුම්කරුවන්", + "settings.providers.custom.description": "URL පදනම අනුව OpenAI-අනුකූල සැපයුම්කරුවෙකු එක් කරන්න.", + "settings.providers.tag.environment": "පරිසරය", + "settings.providers.tag.config": "වින්යාසය", + "settings.providers.tag.custom": "අභිරුචි", + "settings.providers.tag.other": "වෙනත්", + "settings.models.title": "ආකෘති", + "settings.models.description": "ආදර්ශ සැකසුම් මෙහි වින්‍යාසගත වනු ඇත.", + "settings.agents.title": "නියෝජිතයන්", + "settings.agents.description": "නියෝජිත සැකසුම් මෙහි වින්‍යාස කළ හැක.", + "settings.commands.title": "විධාන", + "settings.commands.description": "විධාන සැකසුම් මෙහි වින්‍යාසගත කළ හැක.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP සැකසීම් මෙහි වින්‍යාස කළ හැක.", + "settings.permissions.title": "අවසර", + "settings.permissions.description": "පෙරනිමියෙන් සේවාදායකයට භාවිතා කළ හැකි මෙවලම් පාලනය කරන්න.", + "settings.permissions.section.tools": "මෙවලම්", + "settings.permissions.toast.updateFailed.title": "අවසර යාවත්කාලීන කිරීමට අසමත් විය", + "settings.permissions.action.allow": "ඉඩ දෙන්න", + "settings.permissions.action.ask": "අහන්න", + "settings.permissions.action.deny": "ප්‍රතික්ෂේප කරන්න", + "settings.permissions.tool.read.title": "කියවන්න", + "settings.permissions.tool.read.description": "ගොනුවක් කියවීම (ගොනු මාර්ගයට ගැලපේ)", + "settings.permissions.tool.edit.title": "සංස්කරණය කරන්න", + "settings.permissions.tool.edit.description": "සංස්කරණය කිරීම්, ලිවීම් සහ පැච් ඇතුළු ගොනු වෙනස් කරන්න", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "ග්ලෝබ් රටා භාවිතයෙන් ගොනු ගලපන්න", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "සාමාන්‍ය ප්‍රකාශන භාවිතයෙන් ගොනු අන්තර්ගතය සොයන්න", + "settings.permissions.tool.list.title": "ලැයිස්තුව", + "settings.permissions.tool.list.description": "නාමාවලියක් තුළ ගොනු ලැයිස්තුගත කරන්න", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "shell විධාන ක්‍රියාත්මක කරන්න", + "settings.permissions.tool.task.title": "කාර්යය", + "settings.permissions.tool.task.description": "උප නියෝජිතයන් දියත් කරන්න", + "settings.permissions.tool.skill.title": "දක්ෂතාවය", + "settings.permissions.tool.skill.description": "නමින් කුසලතාවයක් පූරණය කරන්න", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "භාෂා සේවාදායක විමසුම් ධාවනය කරන්න", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Todo ලැයිස්තුව යාවත්කාලීන කරන්න", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "URL වෙතින් අන්තර්ගතය ලබා ගන්න", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "වෙබයේ සොයන්න", + "settings.permissions.tool.external_directory.title": "බාහිර නාමාවලිය", + "settings.permissions.tool.external_directory.description": "ව්‍යාපෘති නාමාවලියෙන් පිටත ගොනු වෙත ප්‍රවේශ වන්න", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "සමාන ආදානයක් සමඟ නැවත නැවතත් මෙවලම් ඇමතුම් හඳුනා ගන්න", + "session.delete.failed.title": "සැසිය මැකීමට අසමත් විය", + "session.delete.title": "සැසිය මකන්න", + "session.delete.confirm": '"{{name}}" සැසිය මකන්නද?', + "session.delete.button": "සැසිය මකන්න", + "workspace.new": "නව වැඩබිම", + "workspace.type.local": "දේශීය", + "workspace.type.sandbox": "වැලිපිල්ල", + "workspace.create.failed.title": "වැඩබිම සෑදීමට අසමත් විය", + "workspace.delete.failed.title": "වැඩබිම මැකීමට අසමත් විය", + "workspace.resetting.title": "වැඩබිම නැවත සැකසීම", + "workspace.resetting.description": "මෙය විනාඩියක් ගත විය හැක.", + "workspace.reset.failed.title": "වැඩබිම යළි පිහිටුවීමට අසමත් විය", + "workspace.reset.success.title": "වැඩබිම යළි පිහිටුවීම", + "workspace.reset.success.description": "වැඩබිම දැන් පෙරනිමි ශාඛාවට ගැලපේ.", + "workspace.error.stillPreparing": "වැඩබිම තවමත් සූදානම් වෙමින් පවතී", + "workspace.status.checking": "ඒකාබද්ධ නොකළ වෙනස්කම් සඳහා පරීක්ෂා කරමින්...", + "workspace.status.error": "git තත්ත්වය සත්‍යාපනය කළ නොහැක.", + "workspace.status.clean": "ඒකාබද්ධ නොකළ වෙනස්කම් කිසිවක් අනාවරණය කර නොමැත.", + "workspace.status.dirty": "මෙම වැඩබිමේ ඒකාබද්ධ නොකළ වෙනස්කම් හමු විය.", + "workspace.delete.title": "වැඩබිම මකන්න", + "workspace.delete.confirm": '"{{name}}" වැඩබිම මකන්නද?', + "workspace.delete.button": "වැඩබිම මකන්න", + "workspace.reset.title": "වැඩබිම නැවත සකසන්න", + "workspace.reset.confirm": '"{{name}}" වැඩබිම යළි පිහිටුවන්නද?', + "workspace.reset.button": "වැඩබිම නැවත සකසන්න", + "workspace.reset.archived.none": "සක්‍රිය සැසි කිසිවක් සංරක්ෂණය නොකෙරේ.", + "workspace.reset.archived.one": "1 සැසියක් සංරක්ෂණය කෙරේ.", + "workspace.reset.archived.many": "සැසි {{count}} සංරක්ෂිත වනු ඇත.", + "workspace.reset.note": "මෙය පෙරනිමි ශාඛාවට ගැලපෙන පරිදි වැඩබිම නැවත සකසනු ඇත.", + "dialog.usageExceeded.dontShowAgain": "නැවත නොපෙන්වන්න", +} diff --git a/packages/app/src/i18n/sl.ts b/packages/app/src/i18n/sl.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c23fcd7f708c3be5d214812a3da8677d93d41e3 --- /dev/null +++ b/packages/app/src/i18n/sl.ts @@ -0,0 +1,1163 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Datoteka", + "desktop.menu.edit": "Uredi", + "desktop.menu.view": "Pogled", + "desktop.menu.go": "pojdi", + "desktop.menu.window": "Okno", + "desktop.menu.help": "pomoč", + "desktop.menu.checkForUpdates": "Preverite posodobitve ...", + "desktop.menu.settings": "nastavitve", + "desktop.menu.reloadWebview": "Ponovno naloži Webview", + "desktop.menu.restart": "Znova zaženite", + "desktop.menu.exportLogs": "Izvozi dnevnike ...", + "desktop.menu.newSession": "Nova seja", + "desktop.menu.openProject": "Odpri projekt ...", + "desktop.menu.newWindow": "Novo okno", + "desktop.menu.closeWindow": "Zapri okno", + "desktop.menu.undo": "Razveljavi", + "desktop.menu.redo": "Ponovi", + "desktop.menu.cut": "Izreži", + "desktop.menu.copy": "Kopiraj", + "desktop.menu.paste": "Prilepi", + "desktop.menu.delete": "Izbriši", + "desktop.menu.selectAll": "Izberite Vse", + "desktop.menu.toggleSidebar": "Preklopi stransko vrstico", + "desktop.menu.toggleTerminal": "Preklopi terminal", + "desktop.menu.toggleFileTree": "Preklopi drevo datotek", + "desktop.menu.reload": "Ponovno naloži", + "desktop.menu.toggleDeveloperTools": "Preklop orodij za razvijalce", + "desktop.menu.actualSize": "Dejanska velikost", + "desktop.menu.zoomIn": "Povečaj", + "desktop.menu.zoomOut": "Pomanjšaj", + "desktop.menu.toggleFullScreen": "Preklop na celoten zaslon", + "desktop.menu.back": "Nazaj", + "desktop.menu.forward": "Naprej", + "desktop.menu.previousSession": "Prejšnja seja", + "desktop.menu.nextSession": "Naslednja seja", + "desktop.menu.previousProject": "Prejšnji projekt", + "desktop.menu.nextProject": "Naslednji projekt", + "desktop.menu.minimize": "Zmanjšaj", + "desktop.menu.maximize": "Povečaj", + "desktop.menu.documentation": "OpenCode Dokumentacija", + "desktop.menu.supportForum": "Forum za podporo", + "desktop.menu.shareFeedback": "Delite povratne informacije", + "desktop.menu.reportBug": "Prijavi napako", + "desktop.menu.ariaLabel": "OpenCode meni", + "desktop.updater.dialog.checkFailed.message": "Preverjanje posodobitve ni uspelo.", + "desktop.updater.dialog.checkFailed.title": "Napaka posodobitve", + "desktop.updater.dialog.upToDate.message": "Ste na tekočem.", + "desktop.updater.dialog.upToDate.title": "Brez posodobitev", + "desktop.updater.dialog.ready.message": "Prenesena posodobitev {{version}}. Ponovno zagnati zdaj?", + "desktop.updater.dialog.ready.title": "Posodobitev pripravljena", + "desktop.updater.dialog.restart": "Znova zaženite", + "desktop.updater.dialog.later": "Kasneje", + "desktop.recovery.action.relaunch": "Znova zaženi", + "desktop.recovery.action.exportLogs": "Izvozi dnevnike", + "desktop.recovery.action.keepWaiting": "Čakaj še naprej", + "desktop.recovery.action.quit": "Končaj", + "desktop.recovery.loadFailed": "OpenCode ni uspelo naložiti", + "desktop.recovery.terminated": "Okno OpenCode se je nepričakovano končalo", + "desktop.recovery.unresponsive": "OpenCode se ne odziva", + "desktop.recovery.unresponsive.detail": "Lahko znova zaženete aplikacijo, odprete dnevnike ali počakate.", + "desktop.recovery.loadFailed.detail": "Okno: {{window}}\nURL: {{url}}\nNapaka: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Okno: {{window}}\nRazlog: {{reason}}\nKoda: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Izberite mapo", + "desktop.dialog.chooseFile": "Izberite datoteko", + "desktop.dialog.saveFile": "Shrani datoteko", + "desktop.dialog.files": "Datoteke", + "desktop.server.local": "Lokalni strežnik", + "desktop.wsl.error.windowsOnly": "WSL je na voljo samo na Windows", + "desktop.wsl.error.unavailable": "WSL ni na voljo", + "desktop.wsl.error.listInstalled": "Seznam nameščenih distribucij WSL ni uspel", + "desktop.wsl.error.listOnline": "Seznam spletnih distribucij WSL ni uspel", + "desktop.wsl.error.executeDistro": "Ni mogoče izvajati ukazov v distribuciji", + "desktop.wsl.error.installWsl": "Namestitev WSL ni uspela", + "desktop.wsl.error.installDistro": "Namestitev distribucije ni uspela: {{distro}}", + "desktop.wsl.error.installOpencode": "Namestitev OpenCode ni uspela", + "desktop.wsl.error.alreadyAdded": "{{distro}} je že dodan", + "desktop.wsl.error.opencodeMissing": "opencode ni nameščen v tej distribuciji", + "desktop.wsl.error.opencodeCannotRun": "opencode je nameščen, vendar se ne more zagnati", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode ni nameščen v {{distro}}", + "desktop.wsl.error.updateVersion": + "Posodobitev OpenCode je končana, vendar {{distro}} še vedno poroča {{installed}}; pričakovano {{expected}}", + "desktop.wsl.error.noVersion": "brez različice", + "desktop.wsl.error.serverExited": "Strežnik WSL je po zagonu zapustil (koda={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Strežnik WSL je zapustil, preden je postal zdrav (koda={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Stranska prikolica za zdravstveni pregled {{distro}} je potekla po {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "Časovna omejitev {{command}} {{args}} je potekla po {{timeout}}ms", + "desktop.wsl.error.failedPort": "Vrat ni bilo mogoče pridobiti", + "desktop.picker.error.notSelected": "Izbirnik ni izbral datoteke", + "desktop.picker.error.sizeLimit": "Izbrane priloge presegajo omejitev {{limit}} MB", + "command.category.suggested": "Predlagano", + "command.category.view": "Pogled", + "command.category.project": "Projekt", + "command.category.provider": "Ponudnik", + "command.category.server": "Strežnik", + "command.category.session": "Seja", + "command.category.theme": "Tema", + "command.category.language": "Jezik", + "command.category.file": "Datoteka", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Dovoljenja", + "command.category.workspace": "Delovni prostor", + "command.category.settings": "nastavitve", + "command.logs.export": "Izvoz dnevnikov", + "theme.scheme.system": "Sistem", + "theme.scheme.light": "Svetloba", + "theme.scheme.dark": "Temno", + "command.sidebar.toggle": "Preklop stranske vrstice", + "command.project.open": "Odprt projekt", + "command.project.previous": "Prejšnji projekt", + "command.project.next": "Naslednji projekt", + "command.project.index": "Preklopite na projekt {{index}}", + "command.provider.connect": "Poveži ponudnika", + "command.server.switch": "Zamenjaj strežnik", + "command.settings.open": "Odprite nastavitve", + "command.session.previous": "Prejšnja seja", + "command.session.next": "Naslednja seja", + "command.session.previous.unseen": "Prejšnja neprebrana seja", + "command.session.next.unseen": "Naslednja neprebrana seja", + "command.session.archive": "Arhiv seje", + "command.palette": "Paleta ukazov", + "command.theme.cycle": "Tema cikla", + "command.theme.set": "Uporabite temo: {{theme}}", + "command.theme.scheme.cycle": "Cikel barvne sheme", + "command.theme.scheme.set": "Uporabite barvno shemo: {{scheme}}", + "command.language.cycle": "Jezik cikla", + "command.language.set": "Uporabi jezik: {{language}}", + "command.session.new": "Nova seja", + "command.file.open": "Odpri datoteko", + "command.tab.close": "Zapri zavihek", + "command.tab.reopenClosed": "Ponovno odpri zaprt zavihek", + "command.context.addSelection": "Dodaj izbor v kontekst", + "command.context.addSelection.description": "Dodaj izbrane vrstice iz trenutne datoteke", + "command.input.focus": "Vnos fokusa", + "command.terminal.toggle": "Preklop terminala", + "command.fileTree.toggle": "Preklop drevesa datotek", + "command.review.toggle": "Preklopi pregled", + "command.terminal.new": "Nov terminal", + "command.terminal.new.description": "Ustvari nov zavihek terminala", + "command.steps.toggle": "Preklapljanje korakov", + "command.steps.toggle.description": "Prikaži ali skrij korake za trenutno sporočilo", + "command.message.previous": "Prejšnje sporočilo", + "command.message.previous.description": "Pojdite na prejšnje uporabniško sporočilo", + "command.message.next": "Naslednje sporočilo", + "command.message.next.description": "Pojdite na naslednje uporabniško sporočilo", + "command.model.choose": "Izberite model", + "command.model.choose.description": "Izberite drug model", + "command.mcp.toggle": "Preklop MCP-jev", + "command.mcp.toggle.description": "Preklop MCP-jev", + "command.agent.cycle": "Naslednji agent", + "command.agent.cycle.description": "Preklopite na naslednjega agenta", + "command.agent.cycle.reverse": "Prejšnji agent", + "command.agent.cycle.reverse.description": "Preklopite na prejšnjega agenta", + "command.model.variant.cycle": "Naslednja raven razmišljanja", + "command.model.variant.cycle.description": "Preklopite na naslednjo stopnjo napora", + "command.prompt.mode.shell": "školjka", + "command.prompt.mode.normal": "Poziv", + "command.permissions.autoaccept.enable": "Samodejno sprejemanje dovoljenj", + "command.permissions.autoaccept.disable": "Ustavi samodejno sprejemanje dovoljenj", + "command.workspace.toggle": "Preklopi delovne prostore", + "command.workspace.toggle.description": "Omogočite ali onemogočite več delovnih prostorov v stranski vrstici", + "command.session.undo": "Razveljavi", + "command.session.undo.description": "Razveljavi zadnje sporočilo", + "command.session.redo": "Ponovi", + "command.session.redo.description": "Ponovite zadnje razveljavljeno sporočilo", + "command.session.compact": "Kompaktna seja", + "command.session.compact.description": "Povzemite sejo, da zmanjšate velikost konteksta", + "command.session.fork": "Razcep iz sporočila", + "command.session.fork.description": "Ustvari novo sejo iz prejšnjega sporočila", + "command.session.share": "Delite sejo", + "command.session.share.description": "Delite to sejo in kopirajte URL v odložišče", + "command.session.unshare": "Prekliči skupno rabo seje", + "command.session.unshare.description": "Nehaj deliti to sejo", + "command.session.export": "Izvozna seja", + "command.session.export.description": "Izvoz celotnega zapisa seje kot JSON", + "palette.search.placeholder": "Iskanje datotek, ukazov in sej", + "palette.search.placeholder.home": "Iskalni ukazi in seje", + "palette.empty": "Ni rezultatov", + "palette.group.commands": "Ukazi", + "palette.group.files": "Datoteke", + "dialog.provider.search.placeholder": "Ponudniki iskanja", + "dialog.provider.empty": "Ni ponudnikov", + "dialog.provider.group.popular": "Priljubljeno", + "dialog.provider.group.other": "drugo", + "dialog.provider.custom.label": "Ponudnik po meri, združljiv z OpenAI", + "dialog.provider.tag.recommended": "Priporočeno", + "dialog.provider.opencode.note": "Izbrani modeli, vključno z Claude, GPT, Gemini in drugimi", + "dialog.provider.opencode.tagline": "Zanesljivi optimizirani modeli", + "dialog.provider.opencodeGo.tagline": "Poceni naročnina za vsakogar", + "dialog.provider.anthropic.note": "Neposreden dostop do modelov Claude, vključno s Pro in Max", + "dialog.provider.copilot.note": "Modeli AI za pomoč pri kodiranju prek GitHub Copilot", + "dialog.provider.openai.note": "Modeli GPT za hitre in zmogljive splošne naloge z umetno inteligenco", + "dialog.provider.google.note": "Modeli Gemini za hitre, strukturirane odzive", + "dialog.provider.openrouter.note": "Dostop do vseh podprtih modelov pri enem ponudniku", + "dialog.provider.vercel.note": "Poenoten dostop do modelov AI s pametnim usmerjanjem", + "dialog.model.select.title": "Izberite model", + "dialog.model.search.placeholder": "Iskanje modelov", + "dialog.model.empty": "Ni rezultatov modela", + "dialog.model.manage": "Upravljanje modelov", + "dialog.model.manage.description": "Prilagodite, kateri modeli se prikažejo v izbirniku modelov.", + "dialog.model.manage.provider.toggle": "Preklopi vse modele {{provider}}", + "dialog.model.unpaid.freeModels.title": "Brezplačne modele ponuja OpenCode", + "dialog.model.unpaid.addMore.title": "Dodajte več modelov priljubljenih ponudnikov", + "dialog.model.unpaid.viewMoreProviders": "Oglejte si več kot 70 ponudnikov", + "dialog.provider.viewAll": "Pokaži več ponudnikov", + "provider.connect.title": "Povežite {{provider}}", + "provider.connect.title.anthropicProMax": "Prijavite se z Claude Pro/Max", + "provider.connect.selectMethod": "Izberite način prijave za {{provider}}.", + "provider.connect.method.apiKey": "Ključ API", + "provider.connect.method.browser": "Brskalnik", + "provider.connect.method.headless": "Brez glave", + "provider.connect.status.inProgress": "Avtorizacija v teku...", + "provider.connect.status.waiting": "Čakanje na avtorizacijo ...", + "provider.connect.status.failed": "Avtorizacija ni uspela: {{error}}", + "provider.connect.apiKey.description": + "Vnesite ključ API ponudnika {{provider}}, da povežete račun in uporabljate modele {{provider}} v OpenCode.", + "provider.connect.apiKey.label": "Ključ {{provider}} API", + "provider.connect.apiKey.placeholder": "Ključ API", + "provider.connect.apiKey.required": "Potreben je ključ API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen vam omogoča dostop do izbranega nabora zanesljivih optimiziranih modelov za agente za kodiranje.", + "provider.connect.opencodeZen.line2": + "Z enim samim ključem API boste dobili dostop do modelov, kot so Claude, GPT, Gemini, GLM in več.", + "provider.connect.opencodeZen.visit.prefix": "Obisk ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " za prevzem ključa API.", + "provider.connect.oauth.code.visit.prefix": "Obisk ", + "provider.connect.oauth.code.visit.link": "ta povezava", + "provider.connect.oauth.code.visit.suffix": + " za zbiranje avtorizacijske kode za povezavo vašega računa in uporabo modelov {{provider}} v OpenCode.", + "provider.connect.oauth.code.label": "{{method}} avtorizacijska koda", + "provider.connect.oauth.code.placeholder": "Avtorizacijska koda", + "provider.connect.oauth.code.required": "Zahtevana je avtorizacijska koda", + "provider.connect.oauth.code.invalid": "Neveljavna avtorizacijska koda", + "provider.connect.oauth.auto.visit.prefix": "Obisk ", + "provider.connect.oauth.auto.visit.link": "ta povezava", + "provider.connect.oauth.auto.visit.suffix": + " in vnesite spodnjo kodo, da povežete svoj račun in uporabite modele {{provider}} v OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Potrditvena koda", + "provider.connect.toast.connected.title": "{{provider}} je povezan", + "provider.connect.toast.connected.description": "Modeli {{provider}} so zdaj na voljo za uporabo.", + "provider.custom.title": "Ponudnik po meri", + "provider.custom.unavailable": "Ponudniki po meri na tem strežniku niso na voljo", + "provider.custom.description.prefix": "Konfigurirajte ponudnika, združljivega z OpenAI. Glej ", + "provider.custom.description.link": "konfiguracijski dokumenti ponudnika", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID ponudnika", + "provider.custom.field.providerID.placeholder": "moj ponudnik", + "provider.custom.field.providerID.description": "Male črke, številke, vezaji ali podčrtaji", + "provider.custom.field.name.label": "Prikazno ime", + "provider.custom.field.name.placeholder": "Moj ponudnik AI", + "provider.custom.field.baseURL.label": "Osnova URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "Ključ API", + "provider.custom.field.apiKey.placeholder": "Ključ API", + "provider.custom.field.apiKey.description": "Neobvezno. Pustite prazno, če upravljate avtorizacijo prek glav.", + "provider.custom.models.label": "Modeli", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Ime", + "provider.custom.models.name.placeholder": "Prikazno ime", + "provider.custom.models.remove": "Odstrani model", + "provider.custom.models.add": "Dodaj model", + "provider.custom.headers.label": "Glave (neobvezno)", + "provider.custom.headers.key.label": "Glava", + "provider.custom.headers.key.placeholder": "Glava-Ime", + "provider.custom.headers.value.label": "Vrednost", + "provider.custom.headers.value.placeholder": "vrednost", + "provider.custom.headers.remove": "Odstrani glavo", + "provider.custom.headers.add": "Dodaj glavo", + "provider.custom.error.providerID.required": "Potreben je ID ponudnika", + "provider.custom.error.providerID.format": "Uporabljajte male črke, številke, vezaje ali podčrtaje", + "provider.custom.error.providerID.exists": "Ta ID ponudnika že obstaja", + "provider.custom.error.name.required": "Prikazno ime je obvezno", + "provider.custom.error.baseURL.required": "Zahtevana je osnova URL", + "provider.custom.error.baseURL.format": "Začeti se mora s http:// ali https://", + "provider.custom.error.required": "Obvezno", + "provider.custom.error.duplicate": "Dvojnik", + "provider.disconnect.toast.disconnected.title": "{{provider}} prekinjen", + "provider.disconnect.toast.disconnected.description": "Modeli {{provider}} niso več na voljo.", + "model.tag.free": "Brezplačno", + "model.tag.latest": "Zadnje", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "besedilo", + "model.input.image": "slika", + "model.input.audio": "zvok", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Omogoča: {{inputs}}", + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Vložki", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Ponudnik", + "model.tooltip.reasoning": "Utemeljitev", + "model.tooltip.reasoning.allowed": "Omogoča sklepanje", + "model.tooltip.reasoning.none": "Brez obrazložitve", + "model.tooltip.context": "Omejitev konteksta {{limit}}", + "common.search.placeholder": "Iskanje", + "common.goBack": "Navigacija nazaj", + "common.goForward": "Navigacija naprej", + "common.loading": "Nalaganje", + "common.loading.ellipsis": "...", + "common.cancel": "Prekliči", + "common.clear": "jasno", + "common.open": "Odpri", + "common.connect": "Povežite se", + "common.disconnect": "Prekini povezavo", + "common.continue": "Nadaljuj", + "common.submit": "Predloži", + "common.save": "Shrani", + "common.saving": "Shranjevanje ...", + "common.default": "Privzeto", + "common.attachment": "priloga", + "prompt.placeholder.shell": "Vnesite ukaz lupine ... {{example}}", + "prompt.placeholder.normal": 'Vprašajte karkoli ... "{{example}}"', + "prompt.placeholder.simple": "Vprašajte karkoli ...", + "prompt.placeholder.summarizeComments": "Povzemite komentarje …", + "prompt.placeholder.summarizeComment": "Povzemite komentar ...", + "prompt.mode.shell": "školjka", + "prompt.mode.normal": "Poziv", + "prompt.mode.shell.exit": "esc za izhod", + "session.child.promptDisabled": "Sej podagenta ni mogoče pozvati.", + "session.child.backToParent": "Nazaj na glavno sejo.", + "prompt.example.1": "Popravite TODO v zbirki kod", + "prompt.example.2": "Kakšen je tehnološki sklop tega projekta?", + "prompt.example.3": "Popravi pokvarjene teste", + "prompt.example.4": "Pojasnite, kako deluje preverjanje pristnosti", + "prompt.example.5": "Poiščite in odpravite varnostne ranljivosti", + "prompt.example.6": "Dodajte teste enot za uporabniško storitev", + "prompt.example.7": "Preoblikujte to funkcijo, da bo bolj berljiva", + "prompt.example.8": "Kaj pomeni ta napaka?", + "prompt.example.9": "Pomagajte mi odpraviti to težavo", + "prompt.example.10": "Ustvari dokumentacijo API", + "prompt.example.11": "Optimizirajte poizvedbe po bazi podatkov", + "prompt.example.12": "Dodajte preverjanje vnosa", + "prompt.example.13": "Ustvari novo komponento za...", + "prompt.example.14": "Kako uvedem ta projekt?", + "prompt.example.15": "Preglejte mojo kodo za najboljše prakse", + "prompt.example.16": "Tej funkciji dodajte obravnavanje napak", + "prompt.example.17": "Pojasnite ta vzorec regularnega izraza", + "prompt.example.18": "Pretvorite to v TypeScript", + "prompt.example.19": "Dodajte beleženje v celotno kodno zbirko", + "prompt.example.20": "Katere odvisnosti so zastarele?", + "prompt.example.21": "Pomagaj mi napisati skript za selitev", + "prompt.example.22": "Izvedite predpomnjenje za to končno točko", + "prompt.example.23": "Na ta seznam dodajte paginacijo", + "prompt.example.24": "Ustvarite ukaz CLI za...", + "prompt.example.25": "Kako tukaj delujejo spremenljivke okolja?", + "prompt.popover.emptyResults": "Ni ustreznih rezultatov", + "prompt.popover.emptyCommands": "Ni ustreznih ukazov", + "prompt.dropzone.label": "Sem spustite slike, datoteke PDF ali besedilne datoteke", + "prompt.dropzone.file.label": "Spustite v datoteko @mention", + "prompt.slash.badge.custom": "po meri", + "prompt.slash.badge.skill": "spretnost", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktivna", + "prompt.context.includeActiveFile": "Vključi aktivno datoteko", + "prompt.context.removeActiveFile": "Odstrani aktivno datoteko iz konteksta", + "prompt.context.removeFile": "Odstrani datoteko iz konteksta", + "prompt.action.attachFile": "Dodajte datoteke", + "prompt.menu.addImagesAndFiles": "Dodajte datoteke in drugo", + "prompt.menu.imagesAndFiles": "Slike in datoteke", + "prompt.menu.commands": "Ukazi", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Ukaz lupine", + "prompt.attachment.remove": "Odstrani prilogo", + "prompt.action.send": "Pošlji", + "prompt.action.stop": "Ustavi", + "prompt.toast.pasteUnsupported.title": "Nepodprta priloga", + "prompt.toast.pasteUnsupported.description": "Sem lahko priložite samo slike, datoteke PDF ali besedilne datoteke.", + "prompt.toast.attachmentDuplicate.title": "Ta datoteka je že naložena", + "prompt.toast.modelAgentRequired.title": "Izberite agenta in model", + "prompt.toast.modelAgentRequired.description": "Preden pošljete poziv, izberite agenta in model.", + "prompt.toast.worktreeCreateFailed.title": "Delovnega drevesa ni bilo mogoče ustvariti", + "prompt.toast.sessionCreateFailed.title": "Seje ni bilo mogoče ustvariti", + "prompt.toast.shellSendFailed.title": "Ukaza lupine ni bilo mogoče poslati", + "prompt.toast.commandSendFailed.title": "Ukaza ni bilo mogoče poslati", + "prompt.toast.promptSendFailed.title": "Poziva ni bilo mogoče poslati", + "prompt.toast.promptSendFailed.description": "Seje ni mogoče pridobiti", + "dialog.mcp.title": "MCP-ji", + "dialog.mcp.description": "{{enabled}} od {{total}} omogočen", + "dialog.mcp.empty": "MCP-ji niso konfigurirani", + "dialog.lsp.empty": "LSP-ji samodejno zaznani glede na vrste datotek", + "dialog.plugins.empty": "Vtičniki, konfigurirani v opencode.json", + "mcp.status.connected": "povezan", + "mcp.status.failed": "ni uspelo", + "mcp.status.needs_auth": "potrebuje avt", + "mcp.status.disabled": "onemogočen", + "mcp.auth.clickToAuthenticate": "Kliknite za preverjanje pristnosti", + "dialog.fork.empty": "Ni sporočil za odcepitev", + "dialog.directory.search.placeholder": "Iskanje map", + "dialog.directory.empty": "Ni najdenih map", + "dialog.directory.action.selectFile": "Izberite datoteko", + "dialog.directory.action.selectFolder": "Izberite mapo", + "dialog.directory.root": "Koren", + "dialog.directory.parent": "starš", + "dialog.directory.readError": "Te mape ni mogoče prebrati", + "app.server.unreachable": "Ni bilo mogoče doseči {{server}}", + "app.server.retrying": "Samodejni ponovni poskus ...", + "app.server.otherServers": "Drugi strežniki", + "dialog.server.title": "Strežniki", + "dialog.server.description": "Preklopite, s katerim strežnikom OpenCode se povezuje ta aplikacija.", + "dialog.server.search.placeholder": "Iskanje strežnikov", + "dialog.server.empty": "Še ni strežnikov", + "dialog.server.add.title": "Dodaj strežnik", + "dialog.server.add.url": "Naslov strežnika", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Ni bilo mogoče vzpostaviti povezave s strežnikom", + "dialog.server.add.checking": "Preverjanje ...", + "dialog.server.add.button": "Dodaj strežnik", + "dialog.server.add.name": "Ime strežnika (neobvezno)", + "dialog.server.add.namePlaceholder": "Lokalni gostitelj", + "dialog.server.add.username": "uporabniško ime (neobvezno)", + "dialog.server.add.usernamePlaceholder": "uporabniško ime", + "dialog.server.add.password": "Geslo (neobvezno)", + "dialog.server.add.passwordPlaceholder": "geslo", + "dialog.server.edit.title": "Uredi strežnik", + "dialog.server.default.title": "Privzeti strežnik", + "dialog.server.default.description": + "Povežite se s tem strežnikom ob zagonu aplikacije namesto zagona lokalnega strežnika. Zahteva ponovni zagon.", + "dialog.server.default.none": "Izbran ni noben strežnik", + "dialog.server.default.set": "Trenutni strežnik nastavite kot privzetega", + "dialog.server.default.clear": "Počisti", + "dialog.server.action.remove": "Odstrani strežnik", + "dialog.server.menu.edit": "Uredi", + "dialog.server.menu.default": "Nastavi kot privzeto", + "dialog.server.menu.defaultRemove": "Odstrani privzeto", + "dialog.server.menu.delete": "Izbriši", + "dialog.server.current": "Trenutni strežnik", + "dialog.server.status.default": "Privzeto", + "wsl.server.add": "Dodajte strežnik WSL", + "wsl.server.addShort": "Dodajte WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "strežnik WSL", + "wsl.server.retryStart": "Poskusite znova začeti", + "wsl.server.updating": "Posodabljanje ...", + "wsl.onboarding.step.distro": "Izberite distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Preverjanje WSL ...", + "wsl.onboarding.restartRequired": "Windows potrebuje ponovni zagon za dokončanje namestitve WSL.", + "wsl.onboarding.ready": "WSL je pripravljen.", + "wsl.onboarding.required": "Za nadaljevanje je potreben WSL.", + "wsl.onboarding.checkingDistros": "Preverjanje distribucij ...", + "wsl.onboarding.installingDistro": "Namestitev {{distro}} ...", + "wsl.onboarding.checkingDistro": "Preverjanje {{distro}} ...", + "wsl.onboarding.listingDistros": "Seznam distribucij ...", + "wsl.onboarding.distroReady": "{{distro}} je pripravljen.", + "wsl.onboarding.distroNotInstalled": "{{distro}} še ni nameščen.", + "wsl.onboarding.openDistroOnce": "Enkrat odprite {{distro}}, da dokončate namestitev.", + "wsl.onboarding.finishingDistro": "Končna nastavitev za {{distro}}.", + "wsl.onboarding.pickDistro": "Spodaj izberite distribucijo ali jo namestite.", + "wsl.onboarding.checkingOpencode": "Preverjanje OpenCode ...", + "wsl.onboarding.checkingOpencodeIn": "Preverjanje OpenCode v {{distro}} ...", + "wsl.onboarding.updatingOpencode": "Posodabljanje OpenCode ...", + "wsl.onboarding.updatingOpencodeIn": "Posodabljanje OpenCode v {{distro}} ...", + "wsl.onboarding.updateOpencodeIn": "Posodobite OpenCode v {{distro}}.", + "wsl.onboarding.updateOpencode": "Posodobite OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode je pripravljen v {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode je pripravljen.", + "wsl.onboarding.installOpencodeIn": "Namestite OpenCode v {{distro}}.", + "wsl.onboarding.installOpencode": "Namestite OpenCode", + "wsl.onboarding.chooseDistroFirst": "Najprej izberite distribucijo.", + "wsl.onboarding.loadFailed": "Stanja WSL ni bilo mogoče naložiti.", + "wsl.onboarding.loading": "Nalaganje...", + "wsl.onboarding.installedDistros": "Nameščene distribucije", + "wsl.onboarding.checkAgain": "Preverite znova", + "wsl.onboarding.distroStatus.ready": "pripravljena", + "wsl.onboarding.distroStatus.checking": "Preverjanje ...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode ni nameščen", + "wsl.onboarding.distroStatus.missingTools": "Manjka bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Nepodprto · Uporabi WSL 2", + "wsl.onboarding.needAnotherDistro": "Potrebujete drugo distribucijo?", + "wsl.onboarding.needAnotherDistroHint": "Namestite distribucijo Linux iz kataloga WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL ni nameščen", + "wsl.onboarding.wslNotInstalled.description": + "WSL (podsistem Windows za Linux) je potreben, preden lahko OpenCode doda strežnik WSL", + "wsl.onboarding.wslUnavailable.title": "WSL ni na voljo", + "wsl.onboarding.wslUnavailable.description": "OpenCode ni mogel preveriti WSL na tej napravi.", + "wsl.onboarding.installWsl": "Namestite WSL", + "wsl.onboarding.windowsRestartRequired": + "Znova zaženite Windows, da dokončate namestitev WSL, nato znova odprite OpenCode.", + "wsl.onboarding.next": "Naprej", + "wsl.onboarding.refresh": "Osveži", + "wsl.onboarding.allDistrosAdded": "Vse nameščene distribucije so že dodane.", + "wsl.onboarding.noDistros": "Ni zaznanih distribucij.", + "wsl.onboarding.install": "Namestite", + "wsl.onboarding.installing": "Namestitev ...", + "wsl.onboarding.installDistro": "Namestite distro", + "wsl.onboarding.searchDistros": "Iskanje distribucij", + "wsl.onboarding.wsl2Required": "Zahtevan je WSL 2.", + "wsl.onboarding.toolsRequired": "Ta distribucija potrebuje bash in curl.", + "wsl.onboarding.openTerminal": "Odpri terminal", + "wsl.onboarding.path": "Pot: {{path}}", + "wsl.onboarding.notFound": "ni najden", + "wsl.onboarding.version": "Različica: {{version}}", + "wsl.onboarding.unknown": "neznano", + "wsl.onboarding.desktopVersion": "namizje {{version}}", + "wsl.onboarding.versionMismatch": "Nameščena različica se ne ujema z različico namizne aplikacije.", + "wsl.onboarding.adding": "Dodajanje ...", + "help.tabs.toast.ariaLabel": "Predstavljamo zavihke. Organizirajte svoje delo in aktivne seje z zavihki", + "help.tabs.toast.dismiss": "Opusti informacije o zavihkih", + "help.tabs.title": "Predstavljamo zavihke", + "help.tabs.description": "Organizirajte svoje delo in aktivne seje z zavihki", + "help.tabs.date": "14. julij", + "help.tabs.introduction": "OpenCode Desktop je zdaj zgrajen okoli zavihkov.", + "help.tabs.sessions": + "Začnite novo sejo na zavihku ali odprite obstoječo sejo iz katerega koli od svojih projektov. Odprite nov zavihek, ko začnete nekaj novega, in ga zaprite, ko končate.", + "help.tabs.organize": + "Če imate nekaj odprtih zavihkov, lažje organizirate svoje aktivne seje. Preimenujte zavihke v nekaj nepozabnega, če jih nameravate obdržati.", + "help.tabs.home": + "Vse svoje seje in projekte boste našli na novem zaslonu Home. Če izberete sejo, jo odprete v zavihku.", + "help.tabs.persistence": "Ko znova odprete aplikacijo, so vaši zavihki še vedno odprti.", + "help.tabs.worktrees": + "Nova oblika še ne podpira Git Worktrees, kmalu bo na voljo. Če bi torej raje še naprej uporabljali prejšnjo postavitev, lahko preklapljate med postavitvami v nastavitvah. Ne pozabite le, da bo nova postavitev v nekaj tednih postala trajna.", + "server.row.noUsername": "brez uporabniškega imena", + "dialog.project.edit.title": "Uredi projekt", + "dialog.project.edit.name": "Ime", + "dialog.project.edit.icon": "Ikona", + "dialog.project.edit.icon.alt": "Ikona projekta", + "dialog.project.edit.icon.hint": "Kliknite ali povlecite sliko", + "dialog.project.edit.icon.recommended": "Priporočeno: 128x128px", + "dialog.project.edit.color": "barva", + "dialog.project.edit.color.select": "Izberite barvo {{color}}", + "dialog.project.edit.worktree.startup": "Zagonski skript delovnega prostora", + "dialog.project.edit.worktree.startup.description": + "Zažene se po ustvarjanju novega delovnega prostora (delovnega drevesa).", + "dialog.project.edit.worktree.startup.placeholder": "npr. bun install", + "dialog.releaseNotes.action.getStarted": "Začnite", + "dialog.releaseNotes.action.next": "Naprej", + "dialog.releaseNotes.action.hideFuture": "V prihodnje jih ne prikazujte", + "dialog.releaseNotes.media.alt": "Predogled izdaje", + "context.breakdown.title": "Razčlenitev konteksta", + "context.breakdown.note": + 'Približna razčlenitev vhodnih žetonov. "Drugo" vključuje definicije orodij in režijske stroške.', + "context.breakdown.system": "Sistem", + "context.breakdown.user": "Uporabnik", + "context.breakdown.assistant": "Pomočnik", + "context.breakdown.tool": "Klici orodja", + "context.breakdown.other": "drugo", + "context.systemPrompt.title": "Sistemski poziv", + "context.rawMessages.title": "Neobdelana sporočila", + "context.export.session": "Izvozna seja", + "context.stats.session": "Seja", + "context.stats.messages": "Sporočila", + "context.stats.provider": "Ponudnik", + "context.stats.model": "Model", + "context.stats.limit": "Omejitev konteksta", + "context.stats.totalTokens": "Skupaj žetonov", + "context.stats.usage": "Uporaba", + "context.stats.inputTokens": "Vhodni žetoni", + "context.stats.outputTokens": "Izhodni žetoni", + "context.stats.reasoningTokens": "Žetoni razmišljanja", + "context.stats.cacheTokens": "Žetoni predpomnilnika (branje/pisanje)", + "context.stats.userMessages": "Uporabniška sporočila", + "context.stats.assistantMessages": "Sporočila pomočnika", + "context.stats.totalCost": "Skupni stroški", + "context.stats.sessionCreated": "Seja ustvarjena", + "context.stats.lastActivity": "Zadnja dejavnost", + "context.usage.tokens": "Žetoni", + "context.usage.usage": "Uporaba", + "context.usage.cost": "Stroški", + "context.usage.clickToView": "Kliknite za ogled konteksta", + "context.usage.view": "Oglejte si uporabo konteksta", + "language.en": "angleščina", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Russkij", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "português (Brazilija)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Jezik", + "toast.language.description": "Preklopil na {{language}}", + "toast.theme.title": "Tema je zamenjana", + "toast.scheme.title": "Barvna shema", + "toast.workspace.enabled.title": "Delovni prostori omogočeni", + "toast.workspace.enabled.description": "V stranski vrstici je zdaj prikazanih več delovnih dreves", + "toast.workspace.disabled.title": "Delovni prostori so onemogočeni", + "toast.workspace.disabled.description": "V stranski vrstici je prikazano samo glavno delovno drevo", + "toast.permissions.autoaccept.on.title": "Samodejno sprejemanje dovoljenj", + "toast.permissions.autoaccept.on.description": "Zahteve za dovoljenje bodo samodejno odobrene", + "toast.permissions.autoaccept.off.title": "Ustavljeno samodejno sprejemanje dovoljenj", + "toast.permissions.autoaccept.off.description": "Zahteve za dovoljenje bodo zahtevale odobritev", + "toast.model.none.title": "Noben model ni izbran", + "toast.model.none.description": "Povežite ponudnika, da povzamete to sejo", + "toast.file.loadFailed.title": "Datoteke ni bilo mogoče naložiti", + "toast.file.listFailed.title": "Seznam datotek ni uspel", + "toast.context.noLineSelection.title": "Brez izbire vrstice", + "toast.context.noLineSelection.description": "Najprej izberite obseg vrstic na zavihku datoteke.", + "toast.session.share.copyFailed.title": "URL ni bilo mogoče kopirati v odložišče", + "toast.session.share.success.title": "Seja je deljena", + "toast.session.share.success.description": "Skupna raba URL kopirano v odložišče!", + "toast.session.share.failed.title": "Skupna raba seje ni uspela", + "toast.session.share.failed.description": "Med deljenjem seje je prišlo do napake", + "toast.session.unshare.success.title": "Seja ni deljena", + "toast.session.unshare.success.description": "Seja je bila uspešno preklicana!", + "toast.session.unshare.failed.title": "Preklic skupne rabe seje ni uspel", + "toast.session.unshare.failed.description": "Pri preklicu skupne rabe seje je prišlo do napake", + "toast.session.export.success.title": "Seja izvožena", + "toast.session.export.success.description": "Shranjena seja v {{filename}}", + "toast.session.export.failed.title": "Seje ni bilo mogoče izvoziti", + "toast.session.export.failed.description": "Pri izvozu seje je prišlo do napake", + "toast.session.listFailed.title": "Nalaganje sej za {{project}} ni uspelo", + "toast.project.reloadFailed.title": "Ponovno nalaganje {{project}} ni uspelo", + "toast.update.title": "Na voljo je posodobitev", + "toast.update.description": "Nova različica OpenCode ({{version}}) je zdaj na voljo za namestitev.", + "toast.update.action.installRestart": "Namestite in znova zaženite", + "toast.update.action.notYet": "Ne še", + "error.page.title": "Nekaj je šlo narobe", + "error.page.description": "Med nalaganjem aplikacije je prišlo do napake.", + "error.page.description.localServerStartup": "Med zagonom lokalnega strežnika je prišlo do napake.", + "error.page.details.label": "Podrobnosti o napaki", + "error.page.action.restart": "Znova zaženite", + "error.page.action.report": "Prijavi napako", + "error.page.action.reported": "Prijavljena napaka", + "error.page.action.exportLogs": "Izvozi dnevnike", + "error.page.action.checking": "Preverjanje ...", + "error.page.action.checkUpdates": "Preverite posodobitve", + "error.page.action.updateTo": "Posodobite na {{version}}", + "error.page.circular": "[Okrožnica]", + "error.page.report.prefix": "Prosimo, prijavite to napako ekipi OpenCode", + "error.page.report.discord": "na Discordu", + "error.page.version": "Različica: {{version}}", + "error.dev.rootNotFound": + "Korenski element ni bil najden. Ste ga pozabili dodati v svoj index.html? Ali pa je morda atribut id narobe črkovan?", + "error.serverSync.connectFailed": + "Ni bilo mogoče vzpostaviti povezave s strežnikom. Ali na `{{url}}` deluje strežnik?", + "error.serverSDK.noServerAvailable": "Strežnik ni na voljo", + "error.serverSDK.serverNotAvailable": "Strežnik ni na voljo", + "error.childStore.persistedCacheCreateFailed": "Trajnega predpomnilnika ni bilo mogoče ustvariti", + "error.childStore.persistedProjectMetadataCreateFailed": "Trajnih metapodatkov projekta ni bilo mogoče ustvariti", + "error.childStore.persistedProjectIconCreateFailed": "Ikone trajnega projekta ni bilo mogoče ustvariti", + "error.childStore.storeCreateFailed": "Trgovine ni bilo mogoče ustvariti", + "directory.error.invalidUrl": "Neveljaven imenik v URL.", + "error.chain.unknown": "Neznana napaka", + "error.server.invalidConfiguration": "Neveljavna konfiguracija", + "error.chain.causedBy": "Vzrok:", + "error.chain.apiError": "Napaka API", + "error.chain.status": "Stanje: {{status}}", + "error.chain.retryable": "Ponovni poskus: {{retryable}}", + "error.chain.responseBody": "Telo odgovora:\n{{body}}", + "error.chain.didYouMean": "Ste mislili: {{suggestions}}", + "error.chain.modelNotFound": "Modela ni bilo mogoče najti: {{provider}}/{{model}}", + "error.chain.checkConfig": "Preverite imena ponudnikov/modelov konfiguracije (opencode.json).", + "error.chain.mcpFailed": 'MCP strežnik "{{name}}" ni uspel. Opomba, OpenCode še ne podpira avtentikacije MCP.', + "error.chain.providerAuthFailed": "Preverjanje pristnosti ponudnika ni uspelo ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Inicializacija ponudnika "{{provider}}" ni uspela. Preverite poverilnice in konfiguracijo.', + "error.chain.configJsonInvalid": "Konfiguracijska datoteka na {{path}} ni veljavna JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Konfiguracijska datoteka pri {{path}} ni veljavna JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Imenik "{{dir}}" v {{path}} ni veljaven. Preimenujte imenik v "{{suggestion}}" ali ga odstranite. To je običajna tipkarska napaka.', + "error.chain.configFrontmatterError": "Ni bilo mogoče razčleniti sprednje snovi v {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfiguracijska datoteka na {{path}} ni veljavna", + "error.chain.configInvalidWithMessage": "Konfiguracijska datoteka na {{path}} je neveljavna: {{message}}", + "notification.permission.title": "Potrebno je dovoljenje", + "notification.permission.description": "{{sessionTitle}} v {{projectName}} potrebuje dovoljenje", + "notification.question.title": "vprašanje", + "notification.question.description": "{{sessionTitle}} v {{projectName}} ima vprašanje", + "notification.action.goToSession": "Pojdi na sejo", + "notification.session.responseReady.title": "Odziv pripravljen", + "notification.session.error.title": "Napaka seje", + "notification.session.error.fallbackDescription": "Prišlo je do napake", + "home.recentProjects": "Nedavni projekti", + "home.empty.title": "Ni nedavnih projektov", + "home.empty.description": "Začnite z odprtjem lokalnega projekta", + "home.title": "Domov", + "home.projects": "Projekti", + "home.project.add": "Dodajte projekt", + "home.recentlyClosed": "Pred kratkim zaprto", + "home.server.collapse": "Strni strežniške projekte", + "home.server.expand": "Razširite strežniške projekte", + "home.sessions.search.placeholder": "Seje iskanja", + "home.sessions.search.placeholder.scoped": "Iskanje sej v {{scope}}", + "home.sessions.search.sessions": "Seje", + "home.sessions.search.noResults": "Ni najdenih sej za {{query}}", + "home.sessions.empty": "Tukaj še ni ničesar", + "home.sessions.empty.description": "Za začetek ustvarite sejo", + "home.sessions.group.today": "Danes", + "home.sessions.group.yesterday": "včeraj", + "home.sessions.group.older": "Starejši", + "home.providerTip": "Povežite se s 75+ ponudniki za uporabo drugih modelov, vključno z Claude, GPT, Gemini itd.", + "session.tab.session": "Seja", + "session.tab.review": "Pregled", + "session.tab.context": "Kontekst", + "session.tab.unknown": "Neznana seja", + "session.panel.reviewAndFiles": "Pregled in datoteke", + "session.error.notFound": "Te seje ni mogoče najti", + "session.error.notFound.description": "Ta zavihek kaže na sejo, ki ne obstaja več na tem strežniku.", + "session.error.notFound.closeTab": "Zapri zavihek", + "session.error.serverConnection": "Ni mogoče vzpostaviti povezave s tem strežnikom", + "session.review.filesChanged": "Spremenjene datoteke {{count}}", + "session.review.change.one": "spremeniti", + "session.review.change.other": "Spremembe", + "session.review.loadingChanges": "Nalaganje sprememb ...", + "session.review.empty": "V tej seji še ni sprememb", + "session.review.noVcs": "Sistem za nadzor različic Git ni bil zaznan, spremembe niso prikazane", + "session.review.noVcs.createGit.title": "Ustvarite repozitorij Git", + "session.review.noVcs.createGit.description": "Sledite, pregledujte in razveljavite spremembe v tem projektu", + "session.review.noVcs.createGit.actionLoading": "Ustvarjanje skladišča Git ...", + "session.review.noVcs.createGit.action": "Ustvari repozitorij Git", + "session.review.noSnapshot": "Sledenje posnetku je onemogočeno v konfiguraciji, zato spremembe seje niso na voljo", + "session.review.noChanges": "Brez sprememb", + "session.review.noUncommittedChanges": "Ni še nepotrjenih sprememb", + "session.review.noBranchChanges": "Ni še nobenih sprememb veje", + "session.files.selectToOpen": "Izberite datoteko, ki jo želite odpreti", + "session.files.all": "Vse datoteke", + "session.files.empty": "Ni datotek", + "session.files.binaryContent": "Binarna datoteka (vsebine ni mogoče prikazati)", + "session.messages.renderEarlier": "Upodobi prejšnja sporočila", + "session.messages.loadingEarlier": "Nalaganje prejšnjih sporočil ...", + "session.messages.loadEarlier": "Naloži prejšnja sporočila", + "session.messages.loading": "Nalaganje sporočil ...", + "session.messages.jumpToLatest": "Skoči na najnovejšo", + "session.context.addToContext": "Dodajte {{selection}} v kontekst", + "session.todo.title": "Opravila", + "session.todo.collapse": "Strni", + "session.todo.expand": "Razširi", + "session.todo.progress": "{{done}} od {{total}} opravil je končano", + "session.question.progress": "{{current}} od vprašanj {{total}}", + "session.question.minimize": "Zmanjšajte vprašanje", + "session.question.restore": "Obnovi vprašanje", + "session.question.pending.one": "{{count}} čakajoče vprašanje", + "session.question.pending.other": "{{count}} čakajočih vprašanj", + "session.question.pending.two": "{{count}} čakajoči vprašanji", + "session.question.pending.few": "{{count}} čakajoča vprašanja", + "session.followupDock.summary.one": "{{count}} sporočilo v čakalni vrsti", + "session.followupDock.summary.other": "{{count}} sporočil v čakalni vrsti", + "session.followupDock.summary.two": "{{count}} sporočili v čakalni vrsti", + "session.followupDock.summary.few": "{{count}} sporočila v čakalni vrsti", + "session.followupDock.sendNow": "Pošlji zdaj", + "session.followupDock.edit": "Uredi", + "session.followupDock.collapse": "Strni sporočila v čakalni vrsti", + "session.followupDock.expand": "Razširi sporočila v čakalni vrsti", + "session.revertDock.summary.one": "{{count}} razveljavljeno sporočilo", + "session.revertDock.summary.other": "{{count}} razveljavljenih sporočil", + "session.revertDock.summary.two": "{{count}} razveljavljeni sporočili", + "session.revertDock.summary.few": "{{count}} razveljavljena sporočila", + "session.revertDock.collapse": "Strni povratna sporočila", + "session.revertDock.expand": "Razširi povrnjena sporočila", + "session.revertDock.restore": "Obnovi sporočilo", + "session.new.title": "Zgradi karkoli", + "session.new.project.new": "Nov projekt", + "session.new.project.search": "Iskanje projektov", + "session.new.project.add": "Dodajte projekt", + "session.new.worktree.main": "Glavna veja", + "session.new.worktree.mainWithBranch": "Glavna veja ({{branch}})", + "session.new.worktree.create": "Ustvari novo delovno drevo", + "session.new.workspace.runIn": "Zaženi sejo", + "session.new.workspace.triggerLocal": "Lokalno", + "session.new.workspace.local": "Lokalno skladišče", + "session.new.workspace.existing": "Delovni prostor…", + "session.new.git.none": "Ne Git", + "session.new.lastModified": "Nazadnje spremenjeno", + "session.header.search.placeholder": "Išči {{project}}", + "session.header.searchFiles": "Iskanje datotek", + "session.header.openIn": "Odpri v", + "session.header.open.action": "Odprite {{app}}", + "session.header.open.ariaLabel": "Odpri v {{app}}", + "session.header.open.menu": "Odprte možnosti", + "session.header.open.copyPath": "Kopiraj pot", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Raziskovalec datotek", + "session.header.open.fileManager": "Upravitelj datotek", + "session.header.reveal.finder": "Razkrij v Finder", + "session.header.reveal.fileExplorer": "Razkrij v File Explorer", + "session.header.reveal.containingFolder": "Odpri mapo, ki vsebuje", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Stanje", + "status.popover.ariaLabel": "Konfiguracije strežnika", + "status.popover.tab.servers": "Strežniki", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Vtičniki", + "status.popover.action.manageServers": "Upravljanje strežnikov", + "session.share.popover.title": "Objavi na spletu", + "session.share.popover.description.shared": "Ta seja je javna v spletu. Dostopen je vsem s povezavo.", + "session.share.popover.description.unshared": "Delite sejo javno v spletu. Dostopen bo vsem s povezavo.", + "session.share.action.share": "Delite", + "session.share.action.publish": "Objavi", + "session.share.action.publishing": "Objava ...", + "session.share.action.unpublish": "Prekliči objavo", + "session.share.action.unpublishing": "Preklic objave ...", + "session.share.action.view": "Pogled", + "session.share.copy.copied": "Kopirano", + "session.share.copy.copyLink": "Kopiraj povezavo", + "lsp.tooltip.none": "Ni strežnikov LSP", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Nalaganje poziva ...", + "terminal.loading": "Nalaganje terminala ...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Zapri terminal", + "terminal.connectionLost.title": "Povezava je prekinjena", + "terminal.connectionLost.abnormalClose": "WebSocket zaprt nenormalno: {{code}}", + "terminal.connectionLost.description": + "Povezava s terminalom je bila prekinjena. To se lahko zgodi, ko se strežnik znova zažene.", + "terminal.connectTicket.csrfError": + "PTY povezovalna vozovnica zavrnjena s preverjanjem izvora ali CSRF. Preverite konfiguracijo strežnika CORS.", + "terminal.connectTicket.statusError": "PTY povezovalna karta ni uspela z {{status}}", + "titlebar.update": "posodobitev", + "titlebar.updateVersion": "Posodobite {{version}}", + "common.closeTab": "Zapri zavihek", + "common.dismiss": "Odpusti", + "common.moreCountSuffix": " (+{{count}} več)", + "common.requestFailed": "Zahteva ni uspela", + "common.moreOptions": "Več možnosti", + "common.learnMore": "Več o tem", + "common.rename": "Preimenuj", + "common.export": "Izvozi", + "common.reset": "Ponastavi", + "common.archive": "Arhiv", + "common.delete": "Izbriši", + "common.close": "Zapri", + "common.edit": "Uredi", + "common.loadMore": "Naloži več", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "neznano", + "common.time.justNow": "Pravkar", + "common.time.minutesAgo.short": "Pred {{count}}m", + "common.time.hoursAgo.short": "{{count}}h nazaj", + "common.time.daysAgo.short": "Pred {{count}}d", + "sidebar.menu.toggle": "Preklopi meni", + "sidebar.nav.projectsAndSessions": "Projekti in seje", + "sidebar.settings": "nastavitve", + "sidebar.help": "pomoč", + "sidebar.workspaces.enable": "Omogoči delovne prostore", + "sidebar.workspaces.disable": "Onemogoči delovne prostore", + "sidebar.gettingStarted.title": "Začetek", + "sidebar.gettingStarted.line1": "OpenCode vključuje brezplačne modele, tako da lahko začnete takoj.", + "sidebar.gettingStarted.line2": "Povežite katerega koli ponudnika za uporabo modelov, vklj. Claude, GPT, Gemini itd.", + "sidebar.project.recentSessions": "Nedavne seje", + "sidebar.project.viewAllSessions": "Ogled vseh sej", + "sidebar.project.clearNotifications": "Počisti obvestila", + "sidebar.empty.title": "Ni odprtih projektov", + "sidebar.empty.description": "Za začetek odprite projekt", + "debugBar.ariaLabel": "Diagnostika uspešnosti razvoja", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Zadnji zaključen prehod poti, ki se je dotaknil strani seje, merjeno od začetka usmerjevalnika do prve barve po tem, ko se poravna.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Vrtenje sličic na sekundo v zadnjih 5 sekundah.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Najslabši čas sličice v zadnjih 5 sekundah.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Sličice, daljše od 32 ms v zadnjih 5 sekundah.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Štetje blokiranega časa in dolgih opravil v zadnjih 5 sekundah. Največja naloga: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Najslabša opažena zakasnitev vnosa v zadnjih 5 sekundah.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Približno trajanje interakcije v zadnjih 5 sekundah. To je podobno INP, ne uradni spletni kazalniki INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativni premik postavitve za trenutno življenjsko dobo aplikacije.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Uporabljena kopica JS v primerjavi z omejitvijo kopice. Samo Chromium.", + "debugBar.mem.tip": "Uporabljena kopica JS v primerjavi z omejitvijo kopice. {{used}} od {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Vsili sloge fokusa na vse interaktivne elemente", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Vsili celotno smer postavitve aplikacije brez spreminjanja izbranega jezika", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Namizje", + "settings.section.server": "Strežnik", + "settings.tab.general": "Splošno", + "settings.tab.shortcuts": "Bližnjice", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Integracija WSL", + "settings.desktop.wsl.description": "Zaženite strežnik OpenCode znotraj WSL na Windows.", + "settings.general.section.appearance": "Videz", + "settings.general.section.advanced": "Napredno", + "settings.general.section.notifications": "Sistemska obvestila", + "settings.general.section.updates": "Posodobitve", + "settings.general.section.sounds": "Zvočni učinki", + "settings.general.section.feed": "Krma", + "settings.general.section.display": "Zaslon", + "settings.general.row.language.title": "Jezik", + "settings.general.row.language.description": "Spremenite jezik prikaza za OpenCode", + "settings.general.row.shell.title": "Končna lupina", + "settings.general.row.shell.description": "Lupina, ki jo uporabljajo terminalska in agentska orodja", + "settings.general.row.shell.autoDefault": "Samodejno (privzeto)", + "settings.general.row.shell.terminalOnly": "samo terminal", + "settings.general.row.appearance.title": "Videz", + "settings.general.row.appearance.description": "Prilagodite, kako OpenCode izgleda na vaši napravi", + "settings.general.row.colorScheme.title": "Barvna shema", + "settings.general.row.colorScheme.description": "Izberite, ali OpenCode sledi sistemski, svetli ali temni temi", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Prilagodite temo OpenCode.", + "settings.general.row.font.title": "Pisava kode", + "settings.general.row.font.description": "Prilagodite pisavo, uporabljeno v blokih kode", + "settings.general.row.terminalFont.title": "Pisava terminala", + "settings.general.row.terminalFont.description": "Prilagodite pisavo, uporabljeno v terminalu", + "settings.general.row.uiFont.title": "Pisava uporabniškega vmesnika", + "settings.general.row.uiFont.description": "Prilagodite pisavo, ki se uporablja v celotnem vmesniku", + "settings.general.row.followup.title": "Nadaljnje vedenje", + "settings.general.row.followup.description": + "Izberite, ali se nadaljnji pozivi usmerjajo takoj ali čakajo v čakalni vrsti", + "settings.general.row.followup.option.queue": "Čakalna vrsta", + "settings.general.row.followup.option.steer": "Krmariti", + "settings.general.row.showFileTree.title": "Drevo datotek", + "settings.general.row.showFileTree.description": "Pokaži podokno drevesa datotek v sejah", + "settings.general.row.showNavigation.title": "Navigacijske kontrole", + "settings.general.row.showNavigation.description": "Pokaži gumba za nazaj in naprej v naslovni vrstici namizja", + "settings.general.row.showSearch.title": "Paleta ukazov", + "settings.general.row.showSearch.description": "Prikaži gumb za iskanje in ukazno paleto v naslovni vrstici", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Prikažite gumb terminala v naslovni vrstici namizja", + "settings.general.row.showStatus.title": "Stanje strežnika", + "settings.general.row.showStatus.description": "Prikaži gumb stanja strežnika v naslovni vrstici", + "settings.general.row.mobileTitlebarBottom.title": "Spodnja navigacija", + "settings.general.row.mobileTitlebarBottom.description": + "Postavite naslovno vrstico in zavihke seje na dno zaslona mobilne naprave", + "settings.general.row.showCustomAgents.title": "Razstavni agent", + "settings.general.row.showCustomAgents.description": + "Preklapljanje med agenti v skladatelju. Ko je skrit, je privzeto nastavljen na Build agent.", + "settings.general.row.reasoningSummaries.title": "Prikažite povzetke sklepanja", + "settings.general.row.reasoningSummaries.description": "Prikažite povzetke sklepanja modela na časovnici", + "settings.general.row.shellToolPartsExpanded.title": "Razširite dele orodja lupine", + "settings.general.row.shellToolPartsExpanded.description": + "Pokaži privzeto razširjene dele orodja lupine na časovnici", + "settings.general.row.editToolPartsExpanded.title": "Razširite urejanje delov orodja", + "settings.general.row.editToolPartsExpanded.description": + "Prikaži privzeto razširjene dele orodja za urejanje, pisanje in popravljanje na časovnici", + "settings.general.row.newInterface.title": "Nova postavitev", + "settings.general.row.newInterface.badge": "Novo", + "settings.general.row.newInterface.description": + "Uporabite nove zavihke in domačo postavitev. Za omejen čas preklapljajte med postavitvami.", + "settings.general.row.newInterfaceNotice.title": "Zdaj uporabljate novo postavitev", + "settings.general.row.newInterfaceNotice.description": "Prejšnja postavitev ni več na voljo", + "settings.general.row.newInterfaceNotice.dismiss": "Odpusti", + "settings.general.row.pinchZoom.title": "Stisnite za povečavo", + "settings.general.row.pinchZoom.description": + "Omogočite ščipanje sledilne ploščice in Ctrl poteze drsenja za povečavo", + "settings.general.row.wayland.title": "Uporabi izvorni Wayland", + "settings.general.row.wayland.description": "Onemogoči nadomestno X11 na Wayland. Zahteva ponovni zagon.", + "settings.general.row.wayland.tooltip": + "Na Linux z monitorji z mešano hitrostjo osveževanja je izvorni Wayland lahko bolj stabilen.", + "settings.general.row.releaseNotes.title": "Opombe ob izdaji", + "settings.general.row.releaseNotes.description": "Pokaži pojavna okna Kaj je novega po posodobitvah", + "settings.updates.row.startup.title": "Preverite posodobitve ob zagonu", + "settings.updates.row.startup.description": "Samodejno preveri posodobitve, ko se zažene OpenCode", + "settings.updates.row.check.title": "Preverite posodobitve", + "settings.updates.row.check.description": + "Ročno preverite, ali so na voljo posodobitve, in jih namestite, če so na voljo", + "settings.updates.action.checkNow": "Preverite zdaj", + "settings.updates.action.checking": "Preverjanje ...", + "settings.updates.action.downloading": "Prenašanje ...", + "settings.updates.action.installing": "Namestitev ...", + "settings.updates.toast.latest.title": "Ste na tekočem", + "settings.updates.toast.latest.description": "Uporabljate najnovejšo različico OpenCode.", + "sound.option.none": "Noben", + "sound.option.alert01": "Opozorilo 01", + "sound.option.alert02": "Opozorilo 02", + "sound.option.alert03": "Opozorilo 03", + "sound.option.alert04": "Opozorilo 04", + "sound.option.alert05": "Opozorilo 05", + "sound.option.alert06": "Opozorilo 06", + "sound.option.alert07": "Opozorilo 07", + "sound.option.alert08": "Opozorilo 08", + "sound.option.alert09": "Opozorilo 09", + "sound.option.alert10": "Opozorilo 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "ne 01", + "sound.option.nope02": "ne 02", + "sound.option.nope03": "ne 03", + "sound.option.nope04": "ne 04", + "sound.option.nope05": "ne 05", + "sound.option.nope06": "ne 06", + "sound.option.nope07": "ne 07", + "sound.option.nope08": "ne 08", + "sound.option.nope09": "ne 09", + "sound.option.nope10": "Ne 10", + "sound.option.nope11": "Ne 11", + "sound.option.nope12": "Ne 12", + "sound.option.yup01": "Ja 01", + "sound.option.yup02": "Ja 02", + "sound.option.yup03": "Ja 03", + "sound.option.yup04": "Ja 04", + "sound.option.yup05": "Ja 05", + "sound.option.yup06": "Ja 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Pokaži sistemsko obvestilo, ko je agent dokončan ali potrebuje pozornost", + "settings.general.notifications.permissions.title": "Dovoljenja", + "settings.general.notifications.permissions.description": "Pokaži sistemsko obvestilo, ko je potrebno dovoljenje", + "settings.general.notifications.errors.title": "Napake", + "settings.general.notifications.errors.description": "Pokaži sistemsko obvestilo, ko pride do napake", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Predvajaj zvok, ko je agent dokončan ali potrebuje pozornost", + "settings.general.sounds.permissions.title": "Dovoljenja", + "settings.general.sounds.permissions.description": "Predvajaj zvok, ko je potrebno dovoljenje", + "settings.general.sounds.errors.title": "Napake", + "settings.general.sounds.errors.description": "Predvajaj zvok, ko pride do napake", + "settings.shortcuts.title": "Bližnjice na tipkovnici", + "settings.shortcuts.reset.button": "Ponastavi na privzete nastavitve", + "settings.shortcuts.reset.toast.title": "Ponastavitev bližnjic", + "settings.shortcuts.reset.toast.description": "Bližnjice na tipkovnici so bile ponastavljene na privzete.", + "settings.shortcuts.conflict.title": "Bližnjica je že v uporabi", + "settings.shortcuts.conflict.description": "{{keybind}} je že dodeljen {{titles}}.", + "settings.shortcuts.unassigned": "Nedodeljen", + "settings.shortcuts.pressKeys": "Pritisnite tipke", + "settings.shortcuts.search.placeholder": "Bližnjice za iskanje", + "settings.shortcuts.search.empty": "Ni bližnjic", + "settings.shortcuts.group.general": "Splošno", + "settings.shortcuts.group.session": "Seja", + "settings.shortcuts.group.navigation": "Navigacija", + "settings.shortcuts.group.modelAndAgent": "Model in agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Poziv", + "settings.providers.title": "Ponudniki", + "settings.providers.description": "Tukaj je mogoče konfigurirati nastavitve ponudnika.", + "settings.providers.section.connected": "Povezani ponudniki", + "settings.providers.connected.empty": "Ni povezanih ponudnikov", + "settings.providers.connected.environmentDescription": "Povezano iz spremenljivk vašega okolja", + "settings.providers.section.popular": "Priljubljeni ponudniki", + "settings.providers.custom.description": "Dodajte ponudnika, združljivega z OpenAI, po osnovi URL.", + "settings.providers.tag.environment": "okolje", + "settings.providers.tag.config": "Konfiguracija", + "settings.providers.tag.custom": "Po meri", + "settings.providers.tag.other": "drugo", + "settings.models.title": "Modeli", + "settings.models.description": "Tukaj je mogoče konfigurirati nastavitve modela.", + "settings.agents.title": "Agenti", + "settings.agents.description": "Tukaj je mogoče konfigurirati nastavitve agenta.", + "settings.commands.title": "Ukazi", + "settings.commands.description": "Tukaj je mogoče konfigurirati nastavitve ukaza.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Tukaj je mogoče konfigurirati nastavitve MCP.", + "settings.permissions.title": "Dovoljenja", + "settings.permissions.description": "Nadzorujte, katera orodja lahko strežnik uporablja privzeto.", + "settings.permissions.section.tools": "Orodja", + "settings.permissions.toast.updateFailed.title": "Posodobitev dovoljenj ni uspela", + "settings.permissions.action.allow": "Dovoli", + "settings.permissions.action.ask": "Vprašajte", + "settings.permissions.action.deny": "Zavrni", + "settings.permissions.tool.read.title": "Preberi", + "settings.permissions.tool.read.description": "Branje datoteke (ujema se s potjo datoteke)", + "settings.permissions.tool.edit.title": "Uredi", + "settings.permissions.tool.edit.description": "Spreminjanje datotek, vključno z urejanjem, pisanjem in popravki", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Ujemite datoteke z uporabo vzorcev glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Iskanje po vsebini datoteke z uporabo regularnih izrazov", + "settings.permissions.tool.list.title": "Seznam", + "settings.permissions.tool.list.description": "Seznam datotek v imeniku", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Zagon lupinskih ukazov", + "settings.permissions.tool.task.title": "Naloga", + "settings.permissions.tool.task.description": "Zagon pod-agentov", + "settings.permissions.tool.skill.title": "Spretnost", + "settings.permissions.tool.skill.description": "Naloži veščino po imenu", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Izvedite poizvedbe jezikovnega strežnika", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Posodobite seznam opravil", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Pridobite vsebino iz URL", + "settings.permissions.tool.websearch.title": "Spletno iskanje", + "settings.permissions.tool.websearch.description": "Iskanje po spletu", + "settings.permissions.tool.external_directory.title": "Zunanji imenik", + "settings.permissions.tool.external_directory.description": "Dostopajte do datotek zunaj imenika projekta", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Zazna ponavljajoče se klice orodja z enakim vnosom", + "session.delete.failed.title": "Brisanje seje ni uspelo", + "session.delete.title": "Izbriši sejo", + "session.delete.confirm": 'Izbrišem sejo "{{name}}"?', + "session.delete.button": "Izbriši sejo", + "workspace.new": "Nov delovni prostor", + "workspace.type.local": "lokalni", + "workspace.type.sandbox": "peskovnik", + "workspace.create.failed.title": "Delovnega prostora ni bilo mogoče ustvariti", + "workspace.delete.failed.title": "Delovnega prostora ni bilo mogoče izbrisati", + "workspace.resetting.title": "Ponastavitev delovnega prostora", + "workspace.resetting.description": "To lahko traja minuto.", + "workspace.reset.failed.title": "Ponastavitev delovnega prostora ni uspela", + "workspace.reset.success.title": "Ponastavitev delovnega prostora", + "workspace.reset.success.description": "Delovni prostor se zdaj ujema s privzeto vejo.", + "workspace.error.stillPreparing": "Delovni prostor se še pripravlja", + "workspace.status.checking": "Preverjanje nespojenih sprememb ...", + "workspace.status.error": "Ni mogoče preveriti statusa git.", + "workspace.status.clean": "Ni zaznanih nespojenih sprememb.", + "workspace.status.dirty": "V tem delovnem prostoru so bile zaznane nespojene spremembe.", + "workspace.delete.title": "Izbriši delovni prostor", + "workspace.delete.confirm": 'Izbrišem delovni prostor "{{name}}"?', + "workspace.delete.button": "Izbriši delovni prostor", + "workspace.reset.title": "Ponastavi delovni prostor", + "workspace.reset.confirm": 'Ponastaviti delovni prostor "{{name}}"?', + "workspace.reset.button": "Ponastavi delovni prostor", + "workspace.reset.archived.none": "Nobena aktivna seja ne bo arhivirana.", + "workspace.reset.archived.one": "1 seja bo arhivirana.", + "workspace.reset.archived.many": "{{count}} seje bodo arhivirane.", + "workspace.reset.note": "To bo ponastavilo delovni prostor, da bo ustrezal privzeti veji.", + "dialog.usageExceeded.dontShowAgain": "Ne prikaži več", +} diff --git a/packages/app/src/i18n/sq.ts b/packages/app/src/i18n/sq.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca94793ab0e69fae439c69238e3e1276d4fefab3 --- /dev/null +++ b/packages/app/src/i18n/sq.ts @@ -0,0 +1,1163 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Skedari", + "desktop.menu.edit": "Redakto", + "desktop.menu.view": "Shiko", + "desktop.menu.go": "Shkoni", + "desktop.menu.window": "Dritare", + "desktop.menu.help": "Ndihmë", + "desktop.menu.checkForUpdates": "Kontrollo për përditësime...", + "desktop.menu.settings": "Cilësimet", + "desktop.menu.reloadWebview": "Rifresko pamjen e internetit", + "desktop.menu.restart": "Rinis", + "desktop.menu.exportLogs": "Eksporto regjistrat...", + "desktop.menu.newSession": "Sesion i ri", + "desktop.menu.openProject": "Hap projektin...", + "desktop.menu.newWindow": "Dritare e re", + "desktop.menu.closeWindow": "Mbyll dritaren", + "desktop.menu.undo": "Zhbër", + "desktop.menu.redo": "Ribëje", + "desktop.menu.cut": "Prerë", + "desktop.menu.copy": "Kopjo", + "desktop.menu.paste": "Ngjit", + "desktop.menu.delete": "Fshi", + "desktop.menu.selectAll": "Zgjidhni Të gjitha", + "desktop.menu.toggleSidebar": "Ndrysho shiritin anësor", + "desktop.menu.toggleTerminal": "Ndrysho Terminalin", + "desktop.menu.toggleFileTree": "Ndrysho Pemën e skedarit", + "desktop.menu.reload": "Ringarko", + "desktop.menu.toggleDeveloperTools": "Aktivizo Veglat e Zhvilluesit", + "desktop.menu.actualSize": "Madhësia aktuale", + "desktop.menu.zoomIn": "Zmadhoni", + "desktop.menu.zoomOut": "Zmadhoni", + "desktop.menu.toggleFullScreen": "Ndrysho ekranin e plotë", + "desktop.menu.back": "Mbrapa", + "desktop.menu.forward": "Përpara", + "desktop.menu.previousSession": "Sesioni i mëparshëm", + "desktop.menu.nextSession": "Sesioni i ardhshëm", + "desktop.menu.previousProject": "Projekti i mëparshëm", + "desktop.menu.nextProject": "Projekti i radhës", + "desktop.menu.minimize": "Minimizoje", + "desktop.menu.maximize": "Maksimizoni", + "desktop.menu.documentation": "Dokumentacioni OpenCode", + "desktop.menu.supportForum": "Forumi i Mbështetjes", + "desktop.menu.shareFeedback": "Ndani komentet", + "desktop.menu.reportBug": "Raportoni një gabim", + "desktop.menu.ariaLabel": "Menuja OpenCode", + "desktop.updater.dialog.checkFailed.message": "Kontrolli i përditësimit dështoi.", + "desktop.updater.dialog.checkFailed.title": "Gabim i përditësimit", + "desktop.updater.dialog.upToDate.message": "Ju jeni të përditësuar.", + "desktop.updater.dialog.upToDate.title": "Nuk ka përditësime", + "desktop.updater.dialog.ready.message": "Përditësimi {{version}} u shkarkua. Të riniset tani?", + "desktop.updater.dialog.ready.title": "Përditësimi gati", + "desktop.updater.dialog.restart": "Rinis", + "desktop.updater.dialog.later": "Më vonë", + "desktop.recovery.action.relaunch": "Rinis", + "desktop.recovery.action.exportLogs": "Eksporto regjistrat", + "desktop.recovery.action.keepWaiting": "Vazhdoni të prisni", + "desktop.recovery.action.quit": "Hiq dorë", + "desktop.recovery.loadFailed": "OpenCode nuk u ngarkua", + "desktop.recovery.terminated": "Dritarja OpenCode u mbyll papritur", + "desktop.recovery.unresponsive": "OpenCode nuk po përgjigjet", + "desktop.recovery.unresponsive.detail": + "Mund ta rinisni aplikacionin, të hapni regjistrat ose të vazhdoni të prisni.", + "desktop.recovery.loadFailed.detail": "Dritarja: {{window}}\nURL: {{url}}\nGabim: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Dritarja: {{window}}\nArsyeja: {{reason}}\nKodi: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Zgjidhni një dosje", + "desktop.dialog.chooseFile": "Zgjidhni një skedar", + "desktop.dialog.saveFile": "Ruaj skedarin", + "desktop.dialog.files": "Skedarët", + "desktop.server.local": "Server lokal", + "desktop.wsl.error.windowsOnly": "WSL disponohet vetëm në Windows", + "desktop.wsl.error.unavailable": "WSL është i padisponueshëm", + "desktop.wsl.error.listInstalled": "Lista e shpërndarjeve të instaluara WSL dështoi", + "desktop.wsl.error.listOnline": "Lista e shpërndarjeve në linjë WSL dështoi", + "desktop.wsl.error.executeDistro": "Nuk mund të ekzekutohen komandat në distro", + "desktop.wsl.error.installWsl": "Instalimi i WSL dështoi", + "desktop.wsl.error.installDistro": "Instalimi i shpërndarjes dështoi: {{distro}}", + "desktop.wsl.error.installOpencode": "Instalimi i OpenCode dështoi", + "desktop.wsl.error.alreadyAdded": "{{distro}} është shtuar tashmë", + "desktop.wsl.error.opencodeMissing": "Opencode nuk është i instaluar në këtë shpërndarje", + "desktop.wsl.error.opencodeCannotRun": "Opencode është instaluar por nuk mund të ekzekutohet", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode nuk është i instaluar në {{distro}}", + "desktop.wsl.error.updateVersion": + "Përditësimi i OpenCode përfundoi, por {{distro}} ende raporton {{installed}}; pritet {{expected}}", + "desktop.wsl.error.noVersion": "asnjë version", + "desktop.wsl.error.serverExited": "Serveri WSL doli pas nisjes (kodi={{code}} sinjal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Serveri WSL doli përpara se të bëhej i shëndetshëm (kodi={{code}} sinjal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Makina anësore për kontrollin shëndetësor të {{distro}} mbaroi pas {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} skadoi pas {{timeout}}ms", + "desktop.wsl.error.failedPort": "Dështoi në marrjen e portit", + "desktop.picker.error.notSelected": "Skedari nuk u zgjodh nga zgjedhësi", + "desktop.picker.error.sizeLimit": "Bashkëngjitjet e zgjedhura tejkalojnë kufirin {{limit}} MB", + "command.category.suggested": "Sugjeruar", + "command.category.view": "Shiko", + "command.category.project": "Projekti", + "command.category.provider": "Ofruesi", + "command.category.server": "Serveri", + "command.category.session": "Sesioni", + "command.category.theme": "Tema", + "command.category.language": "Gjuha", + "command.category.file": "Skedari", + "command.category.context": "Konteksti", + "command.category.terminal": "Terminali", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agjenti", + "command.category.permissions": "Lejet", + "command.category.workspace": "Hapësira e punës", + "command.category.settings": "Cilësimet", + "command.logs.export": "Eksporto regjistrat", + "theme.scheme.system": "Sistemi", + "theme.scheme.light": "Drita", + "theme.scheme.dark": "E errët", + "command.sidebar.toggle": "Ndrysho shiritin anësor", + "command.project.open": "Projekt i hapur", + "command.project.previous": "Projekti i mëparshëm", + "command.project.next": "Projekti i radhës", + "command.project.index": "Kalo te projekti {{index}}", + "command.provider.connect": "Lidhni ofruesin", + "command.server.switch": "Ndërro serverin", + "command.settings.open": "Hap cilësimet", + "command.session.previous": "Sesioni i mëparshëm", + "command.session.next": "Sesioni i ardhshëm", + "command.session.previous.unseen": "Sesioni i mëparshëm i palexuar", + "command.session.next.unseen": "Sesioni tjetër i palexuar", + "command.session.archive": "Sesioni i arkivit", + "command.palette": "Paleta e komandave", + "command.theme.cycle": "Tema e ciklit", + "command.theme.set": "Përdorni temën: {{theme}}", + "command.theme.scheme.cycle": "Skema e ngjyrave të ciklit", + "command.theme.scheme.set": "Përdorni skemën e ngjyrave: {{scheme}}", + "command.language.cycle": "Gjuha e ciklit", + "command.language.set": "Përdorni gjuhën: {{language}}", + "command.session.new": "Sesion i ri", + "command.file.open": "Hap skedarin", + "command.tab.close": "Mbyll skedën", + "command.tab.reopenClosed": "Rihap skedën e mbyllur", + "command.context.addSelection": "Shtoni përzgjedhjen në kontekst", + "command.context.addSelection.description": "Shtoni linjat e zgjedhura nga skedari aktual", + "command.input.focus": "Fokusoni hyrjen", + "command.terminal.toggle": "Ndrysho terminalin", + "command.fileTree.toggle": "Ndrysho pemën e skedarit", + "command.review.toggle": "Aktivizo rishikimin", + "command.terminal.new": "Terminali i ri", + "command.terminal.new.description": "Krijo një skedë të re terminali", + "command.steps.toggle": "Ndrysho hapat", + "command.steps.toggle.description": "Shfaq ose fshih hapat për mesazhin aktual", + "command.message.previous": "Mesazhi i mëparshëm", + "command.message.previous.description": "Shkoni te mesazhi i mëparshëm i përdoruesit", + "command.message.next": "Mesazhi i radhës", + "command.message.next.description": "Shkoni te mesazhi tjetër i përdoruesit", + "command.model.choose": "Zgjidhni modelin", + "command.model.choose.description": "Zgjidhni një model tjetër", + "command.mcp.toggle": "Aktivizo MCP-të", + "command.mcp.toggle.description": "Aktivizo MCP-të", + "command.agent.cycle": "Agjenti tjetër", + "command.agent.cycle.description": "Kalo te agjenti tjetër", + "command.agent.cycle.reverse": "Agjenti i mëparshëm", + "command.agent.cycle.reverse.description": "Kalo te agjenti i mëparshëm", + "command.model.variant.cycle": "Niveli tjetër i arsyetimit", + "command.model.variant.cycle.description": "Kalo në nivelin tjetër të përpjekjes", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Kërkesë", + "command.permissions.autoaccept.enable": "Prano automatikisht lejet", + "command.permissions.autoaccept.disable": "Ndalo pranimin automatik të lejeve", + "command.workspace.toggle": "Ndrysho hapësirat e punës", + "command.workspace.toggle.description": "Aktivizo ose çaktivizo shumë hapësira pune në shiritin anësor", + "command.session.undo": "Zhbër", + "command.session.undo.description": "Zhbër mesazhin e fundit", + "command.session.redo": "Ribëje", + "command.session.redo.description": "Ribërë mesazhin e fundit të zhbërë", + "command.session.compact": "Sesion kompakt", + "command.session.compact.description": "Përmblidhni seancën për të zvogëluar madhësinë e kontekstit", + "command.session.fork": "Fork nga mesazhi", + "command.session.fork.description": "Krijoni një seancë të re nga një mesazh i mëparshëm", + "command.session.share": "Ndani seancën", + "command.session.share.description": "Ndani këtë sesion dhe kopjoni URL në kujtesën e fragmenteve", + "command.session.unshare": "Anulo ndarjen e seancës", + "command.session.unshare.description": "Ndalo ndarjen e këtij sesioni", + "command.session.export": "Sesioni i eksportit", + "command.session.export.description": "Eksporto transkriptin e plotë të sesionit si JSON", + "palette.search.placeholder": "Kërkoni skedarë, komanda dhe sesione", + "palette.search.placeholder.home": "Kërko komandat dhe seancat", + "palette.empty": "Nuk u gjet asnjë rezultat", + "palette.group.commands": "Komandat", + "palette.group.files": "Skedarët", + "dialog.provider.search.placeholder": "Kërko ofruesit", + "dialog.provider.empty": "Nuk u gjet asnjë ofrues", + "dialog.provider.group.popular": "Popullore", + "dialog.provider.group.other": "Të tjera", + "dialog.provider.custom.label": "Ofrues i personalizuar i përputhshëm me OpenAI", + "dialog.provider.tag.recommended": "Rekomanduar", + "dialog.provider.opencode.note": "Modele të kuruara duke përfshirë Claude, GPT, Gemini dhe më shumë", + "dialog.provider.opencode.tagline": "Modele të besueshme të optimizuara", + "dialog.provider.opencodeGo.tagline": "Abonim me kosto të ulët për të gjithë", + "dialog.provider.anthropic.note": "Qasje e drejtpërdrejtë në modelet Claude, duke përfshirë Pro dhe Max", + "dialog.provider.copilot.note": "Modele të AI për ndihmë kodimi përmes GitHub Copilot", + "dialog.provider.openai.note": "Modelet GPT për detyra të shpejta dhe të afta të përgjithshme të AI", + "dialog.provider.google.note": "Modele Gemini për përgjigje të shpejta dhe të strukturuara", + "dialog.provider.openrouter.note": "Hyni në të gjitha modelet e mbështetura nga një ofrues", + "dialog.provider.vercel.note": "Qasje e unifikuar në modelet e AI me rrugëzim inteligjent", + "dialog.model.select.title": "Zgjidhni modelin", + "dialog.model.search.placeholder": "Kërko modele", + "dialog.model.empty": "Asnjë rezultat modeli", + "dialog.model.manage": "Menaxhoni modelet", + "dialog.model.manage.description": "Personalizo cilat modele shfaqen në përzgjedhësin e modelit.", + "dialog.model.manage.provider.toggle": "Ndrysho të gjitha modelet {{provider}}", + "dialog.model.unpaid.freeModels.title": "Modele falas të ofruara nga OpenCode", + "dialog.model.unpaid.addMore.title": "Shtoni më shumë modele nga ofruesit e njohur", + "dialog.model.unpaid.viewMoreProviders": "Shiko mbi 70 ofrues të tjerë", + "dialog.provider.viewAll": "Shfaq më shumë ofrues", + "provider.connect.title": "Lidhni {{provider}}", + "provider.connect.title.anthropicProMax": "Hyni me Claude Pro/Max", + "provider.connect.selectMethod": "Zgjidhni metodën e hyrjes për {{provider}}.", + "provider.connect.method.apiKey": "Tasti API", + "provider.connect.method.browser": "Shfletuesi", + "provider.connect.method.headless": "Pa kokë", + "provider.connect.status.inProgress": "Autorizimi në proces...", + "provider.connect.status.waiting": "Në pritje të autorizimit...", + "provider.connect.status.failed": "Autorizimi dështoi: {{error}}", + "provider.connect.apiKey.description": + "Futni çelësin API të {{provider}} për të lidhur llogarinë dhe për të përdorur modelet {{provider}} në OpenCode.", + "provider.connect.apiKey.label": "{{provider}} Tasti API", + "provider.connect.apiKey.placeholder": "Tasti API", + "provider.connect.apiKey.required": "Kërkohet çelësi API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ju jep akses në një grup të kuruar modelesh të optimizuara të besueshme për agjentët e kodimit.", + "provider.connect.opencodeZen.line2": + "Me një çelës të vetëm API do të keni akses në modele të tilla si Claude, GPT, Gemini, GLM dhe më shumë.", + "provider.connect.opencodeZen.visit.prefix": "Vizitoni ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " për të mbledhur çelësin tuaj API.", + "provider.connect.oauth.code.visit.prefix": "Vizitoni ", + "provider.connect.oauth.code.visit.link": "kjo lidhje", + "provider.connect.oauth.code.visit.suffix": + " për të mbledhur kodin tuaj të autorizimit për të lidhur llogarinë tuaj dhe për të përdorur modelet {{provider}} në OpenCode.", + "provider.connect.oauth.code.label": "Kodi i autorizimit {{method}}", + "provider.connect.oauth.code.placeholder": "Kodi i autorizimit", + "provider.connect.oauth.code.required": "Kërkohet kodi i autorizimit", + "provider.connect.oauth.code.invalid": "Kodi i pavlefshëm i autorizimit", + "provider.connect.oauth.auto.visit.prefix": "Vizitoni ", + "provider.connect.oauth.auto.visit.link": "kjo lidhje", + "provider.connect.oauth.auto.visit.suffix": + " dhe futni kodin më poshtë për të lidhur llogarinë tuaj dhe për të përdorur modelet {{provider}} në OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Kodi i konfirmimit", + "provider.connect.toast.connected.title": "{{provider}} i lidhur", + "provider.connect.toast.connected.description": "Modelet {{provider}} tani janë në dispozicion për t'u përdorur.", + "provider.custom.title": "Ofruesi i personalizuar", + "provider.custom.unavailable": "Ofruesit e personalizuar nuk janë të disponueshëm në këtë server", + "provider.custom.description.prefix": "Konfiguro një ofrues të pajtueshëm me OpenAI. Shihni ", + "provider.custom.description.link": "dokumentet e konfigurimit të ofruesit", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID e ofruesit", + "provider.custom.field.providerID.placeholder": "ofruesi im", + "provider.custom.field.providerID.description": "Shkronjat e vogla, numrat, vizat vijuese ose nënvizat", + "provider.custom.field.name.label": "Emri i shfaqur", + "provider.custom.field.name.placeholder": "Ofruesi im i AI", + "provider.custom.field.baseURL.label": "Baza URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "Tasti API", + "provider.custom.field.apiKey.placeholder": "Tasti API", + "provider.custom.field.apiKey.description": "Fakultative. Lëreni bosh nëse menaxhoni auth nëpërmjet titujve.", + "provider.custom.models.label": "Modelet", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Emri", + "provider.custom.models.name.placeholder": "Emri i shfaqur", + "provider.custom.models.remove": "Hiq modelin", + "provider.custom.models.add": "Shto model", + "provider.custom.headers.label": "Titujt (opsionale)", + "provider.custom.headers.key.label": "Kreu", + "provider.custom.headers.key.placeholder": "Header-Emri", + "provider.custom.headers.value.label": "Vlera", + "provider.custom.headers.value.placeholder": "vlerë", + "provider.custom.headers.remove": "Hiq kokën", + "provider.custom.headers.add": "Shto kokën", + "provider.custom.error.providerID.required": "Kërkohet ID e ofruesit", + "provider.custom.error.providerID.format": "Përdorni shkronja të vogla, numra, viza ose nënvizime", + "provider.custom.error.providerID.exists": "Ajo ID e ofruesit ekziston tashmë", + "provider.custom.error.name.required": "Kërkohet emri i shfaqur", + "provider.custom.error.baseURL.required": "Kërkohet baza URL", + "provider.custom.error.baseURL.format": "Duhet të fillojë me http:// ose https://", + "provider.custom.error.required": "E detyrueshme", + "provider.custom.error.duplicate": "Dublikatë", + "provider.disconnect.toast.disconnected.title": "{{provider}} u shkëput", + "provider.disconnect.toast.disconnected.description": "Modelet {{provider}} nuk janë më të disponueshme.", + "model.tag.free": "Falas", + "model.tag.latest": "E fundit", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "teksti", + "model.input.image": "imazh", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Lejon: {{inputs}}", + "model.tooltip.context.label": "Konteksti", + "model.tooltip.inputs": "Inputet", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Ofruesi", + "model.tooltip.reasoning": "Arsyetimi", + "model.tooltip.reasoning.allowed": "Lejon arsyetimin", + "model.tooltip.reasoning.none": "Asnjë arsyetim", + "model.tooltip.context": "Kufiri i kontekstit {{limit}}", + "common.search.placeholder": "Kërko", + "common.goBack": "Navigoni prapa", + "common.goForward": "Navigoni përpara", + "common.loading": "Po ngarkohet", + "common.loading.ellipsis": "...", + "common.cancel": "Anulo", + "common.clear": "E qartë", + "common.open": "Hapur", + "common.connect": "Lidheni", + "common.disconnect": "Shkëputeni", + "common.continue": "Vazhdoni", + "common.submit": "Paraqisni", + "common.save": "Ruaj", + "common.saving": "Po ruan...", + "common.default": "E paracaktuar", + "common.attachment": "bashkëngjitje", + "prompt.placeholder.shell": "Futni një komandë shell... {{example}}", + "prompt.placeholder.normal": 'Pyet çdo gjë... "{{example}}"', + "prompt.placeholder.simple": "Pyet ndonjë gjë...", + "prompt.placeholder.summarizeComments": "Përmblidhni komentet…", + "prompt.placeholder.summarizeComment": "Përmblidhni komentin…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Kërkesë", + "prompt.mode.shell.exit": "esc për të dalë", + "session.child.promptDisabled": "Seancat e nënagjentit nuk mund të kërkohen.", + "session.child.backToParent": "Kthehu në sesionin kryesor.", + "prompt.example.1": "Rregulloni një TODO në bazën e kodeve", + "prompt.example.2": "Cili është grupi teknologjik i këtij projekti?", + "prompt.example.3": "Rregulloni testet e prishura", + "prompt.example.4": "Shpjegoni se si funksionon vërtetimi", + "prompt.example.5": "Gjeni dhe rregulloni dobësitë e sigurisë", + "prompt.example.6": "Shtoni testet e njësisë për shërbimin e përdoruesit", + "prompt.example.7": "Rifaktoroni këtë funksion për të qenë më i lexueshëm", + "prompt.example.8": "Çfarë do të thotë ky gabim?", + "prompt.example.9": "Më ndihmo të korrigjoj këtë problem", + "prompt.example.10": "Gjeneroni dokumentacion API", + "prompt.example.11": "Optimizoni pyetjet e bazës së të dhënave", + "prompt.example.12": "Shto vërtetimin e hyrjes", + "prompt.example.13": "Krijo një komponent të ri për...", + "prompt.example.14": "Si ta vendos këtë projekt?", + "prompt.example.15": "Rishikoni kodin tim për praktikat më të mira", + "prompt.example.16": "Shto trajtimin e gabimeve në këtë funksion", + "prompt.example.17": "Shpjegoni këtë model regex", + "prompt.example.18": "Konvertojeni këtë në TypeScript", + "prompt.example.19": "Shto regjistrime në të gjithë bazën e kodeve", + "prompt.example.20": "Cilat varësi janë të vjetruara?", + "prompt.example.21": "Më ndihmo të shkruaj një skenar migrimi", + "prompt.example.22": "Zbatoni caching për këtë pikë fundore", + "prompt.example.23": "Shto faqezim në këtë listë", + "prompt.example.24": "Krijo një komandë CLI për...", + "prompt.example.25": "Si funksionojnë variablat e mjedisit këtu?", + "prompt.popover.emptyResults": "Nuk ka rezultate që përputhen", + "prompt.popover.emptyCommands": "Nuk ka komanda që përputhen", + "prompt.dropzone.label": "Hidhni këtu imazhe, PDF ose skedarë teksti", + "prompt.dropzone.file.label": "Hidheni te skedari @mention", + "prompt.slash.badge.custom": "zakon", + "prompt.slash.badge.skill": "shkathtësi", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktive", + "prompt.context.includeActiveFile": "Përfshi skedarin aktiv", + "prompt.context.removeActiveFile": "Hiq skedarin aktiv nga konteksti", + "prompt.context.removeFile": "Hiq skedarin nga konteksti", + "prompt.action.attachFile": "Shtoni skedarë", + "prompt.menu.addImagesAndFiles": "Shtoni skedarë dhe më shumë", + "prompt.menu.imagesAndFiles": "Imazhet dhe skedarët", + "prompt.menu.commands": "Komandat", + "prompt.menu.context": "Konteksti", + "prompt.menu.shellCommand": "Komanda e guaskës", + "prompt.attachment.remove": "Hiq shtojcën", + "prompt.action.send": "Dërgo", + "prompt.action.stop": "Ndalo", + "prompt.toast.pasteUnsupported.title": "Bashkëngjitje e pambështetur", + "prompt.toast.pasteUnsupported.description": + "Këtu mund të bashkëngjiten vetëm imazhe, skedarë PDF ose skedarë teksti.", + "prompt.toast.attachmentDuplicate.title": "Ky skedar tashmë është ngarkuar", + "prompt.toast.modelAgentRequired.title": "Zgjidhni një agjent dhe model", + "prompt.toast.modelAgentRequired.description": "Zgjidhni një agjent dhe model përpara se të dërgoni një kërkesë.", + "prompt.toast.worktreeCreateFailed.title": "Krijimi i pemës së punës dështoi", + "prompt.toast.sessionCreateFailed.title": "Krijimi i sesionit dështoi", + "prompt.toast.shellSendFailed.title": "Dërgimi i komandës së predhës dështoi", + "prompt.toast.commandSendFailed.title": "Dërgimi i komandës dështoi", + "prompt.toast.promptSendFailed.title": "Dërgimi i kërkesës dështoi", + "prompt.toast.promptSendFailed.description": "Sesioni nuk mund të rikthehet", + "dialog.mcp.title": "MCP-të", + "dialog.mcp.description": "{{enabled}} nga {{total}} është aktivizuar", + "dialog.mcp.empty": "Asnjë MCP nuk është konfiguruar", + "dialog.lsp.empty": "LSP-të zbulohen automatikisht nga llojet e skedarëve", + "dialog.plugins.empty": "Shtojcat e konfiguruara në opencode.json", + "mcp.status.connected": "lidhur", + "mcp.status.failed": "dështoi", + "mcp.status.needs_auth": "nevojiten auth", + "mcp.status.disabled": "me aftësi të kufizuara", + "mcp.auth.clickToAuthenticate": "Kliko për të vërtetuar", + "dialog.fork.empty": "Nuk ka mesazhe për të marrë nga", + "dialog.directory.search.placeholder": "Kërko dosjet", + "dialog.directory.empty": "Nuk u gjet asnjë dosje", + "dialog.directory.action.selectFile": "Zgjidhni skedarin", + "dialog.directory.action.selectFolder": "Zgjidhni dosjen", + "dialog.directory.root": "Rrënja", + "dialog.directory.parent": "Prindi", + "dialog.directory.readError": "Nuk mund të lexohet kjo dosje", + "app.server.unreachable": "{{server}} nuk mund të arrihet", + "app.server.retrying": "Po riprovohet automatikisht...", + "app.server.otherServers": "Serverë të tjerë", + "dialog.server.title": "Serverat", + "dialog.server.description": "Ndrysho me cilin server OpenCode lidhet ky aplikacion.", + "dialog.server.search.placeholder": "Kërko serverët", + "dialog.server.empty": "Ende nuk ka serverë", + "dialog.server.add.title": "Shto server", + "dialog.server.add.url": "Adresa e serverit", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Nuk mund të lidhej me serverin", + "dialog.server.add.checking": "Po kontrollon...", + "dialog.server.add.button": "Shto server", + "dialog.server.add.name": "Emri i serverit (opsionale)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Emri i përdoruesit (opsionale)", + "dialog.server.add.usernamePlaceholder": "emri i përdoruesit", + "dialog.server.add.password": "Fjalëkalimi (opsionale)", + "dialog.server.add.passwordPlaceholder": "fjalëkalimin", + "dialog.server.edit.title": "Redakto serverin", + "dialog.server.default.title": "Serveri i parazgjedhur", + "dialog.server.default.description": + "Lidhu me këtë server në nisjen e aplikacionit në vend që të nisësh një server lokal. Kërkon rinisje.", + "dialog.server.default.none": "Nuk është zgjedhur asnjë server", + "dialog.server.default.set": "Cakto serverin aktual si parazgjedhje", + "dialog.server.default.clear": "Pastro", + "dialog.server.action.remove": "Hiq serverin", + "dialog.server.menu.edit": "Redakto", + "dialog.server.menu.default": "Cakto si parazgjedhje", + "dialog.server.menu.defaultRemove": "Hiq parazgjedhjen", + "dialog.server.menu.delete": "Fshi", + "dialog.server.current": "Serveri aktual", + "dialog.server.status.default": "E paracaktuar", + "wsl.server.add": "Shto serverin WSL", + "wsl.server.addShort": "Shto WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "Serveri WSL", + "wsl.server.retryStart": "Riprovo fillimin", + "wsl.server.updating": "Po përditësohet...", + "wsl.onboarding.step.distro": "Zgjidhni shpërndarjen", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Po kontrollon WSL...", + "wsl.onboarding.restartRequired": "Windows ka nevojë për një rinisje për të përfunduar instalimin e WSL.", + "wsl.onboarding.ready": "WSL është gati.", + "wsl.onboarding.required": "WSL kërkohet për të vazhduar.", + "wsl.onboarding.checkingDistros": "Po kontrollon shpërndarjet...", + "wsl.onboarding.installingDistro": "Po instalon {{distro}}...", + "wsl.onboarding.checkingDistro": "Po kontrollon {{distro}}...", + "wsl.onboarding.listingDistros": "Listimi i shpërndarjeve...", + "wsl.onboarding.distroReady": "{{distro}} është gati.", + "wsl.onboarding.distroNotInstalled": "{{distro}} nuk është instaluar ende.", + "wsl.onboarding.openDistroOnce": "Hap {{distro}} një herë për të përfunduar konfigurimin.", + "wsl.onboarding.finishingDistro": "Përfundimi i konfigurimit për {{distro}}.", + "wsl.onboarding.pickDistro": "Zgjidh një shpërndarje ose instalo një më poshtë.", + "wsl.onboarding.checkingOpencode": "Po kontrollon OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Po kontrollon OpenCode në {{distro}}...", + "wsl.onboarding.updatingOpencode": "OpenCode po përditësohet...", + "wsl.onboarding.updatingOpencodeIn": "Po përditëson OpenCode në {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Përditëso OpenCode në {{distro}}.", + "wsl.onboarding.updateOpencode": "Përditëso OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode është gati në {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode është gati.", + "wsl.onboarding.installOpencodeIn": "Instaloni OpenCode në {{distro}}.", + "wsl.onboarding.installOpencode": "Instaloni OpenCode", + "wsl.onboarding.chooseDistroFirst": "Së pari zgjidhni një shpërndarje.", + "wsl.onboarding.loadFailed": "Ngarkimi i gjendjes WSL dështoi.", + "wsl.onboarding.loading": "Po ngarkohet...", + "wsl.onboarding.installedDistros": "Distro të instaluara", + "wsl.onboarding.checkAgain": "Kontrollo sërish", + "wsl.onboarding.distroStatus.ready": "Gati", + "wsl.onboarding.distroStatus.checking": "Po kontrollon...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode nuk është i instaluar", + "wsl.onboarding.distroStatus.missingTools": "Mungon bash, kaçurrela", + "wsl.onboarding.distroStatus.unsupported": "E pambështetur · Përdor WSL 2", + "wsl.onboarding.needAnotherDistro": "Keni nevojë për një shpërndarje tjetër?", + "wsl.onboarding.needAnotherDistroHint": "Instaloni një shpërndarje Linux nga katalogu WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL nuk është i instaluar", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Nënsistemi Windows për Linux) kërkohet përpara se OpenCode të shtojë një server WSL", + "wsl.onboarding.wslUnavailable.title": "WSL i padisponueshëm", + "wsl.onboarding.wslUnavailable.description": "OpenCode nuk mund ta verifikonte WSL në këtë makinë.", + "wsl.onboarding.installWsl": "Instaloni WSL", + "wsl.onboarding.windowsRestartRequired": "Rinis Windows për të përfunduar instalimin e WSL, më pas rihap OpenCode.", + "wsl.onboarding.next": "Tjetra", + "wsl.onboarding.refresh": "Rifresko", + "wsl.onboarding.allDistrosAdded": "Të gjitha shpërndarjet e instaluara janë shtuar tashmë.", + "wsl.onboarding.noDistros": "Nuk është zbuluar ende asnjë shpërndarje.", + "wsl.onboarding.install": "Instaloni", + "wsl.onboarding.installing": "Po instalohet...", + "wsl.onboarding.installDistro": "Instaloni distro", + "wsl.onboarding.searchDistros": "Kërko shpërndarjet", + "wsl.onboarding.wsl2Required": "Kërkohet WSL 2.", + "wsl.onboarding.toolsRequired": "Kjo shpërndarje ka nevojë për bash dhe kaçurrela.", + "wsl.onboarding.openTerminal": "Hap terminalin", + "wsl.onboarding.path": "Rruga: {{path}}", + "wsl.onboarding.notFound": "nuk u gjet", + "wsl.onboarding.version": "Versioni: {{version}}", + "wsl.onboarding.unknown": "i panjohur", + "wsl.onboarding.desktopVersion": "desktop {{version}}", + "wsl.onboarding.versionMismatch": "Versioni i instaluar nuk përputhet me versionin e aplikacionit desktop.", + "wsl.onboarding.adding": "Po shtohet...", + "help.tabs.toast.ariaLabel": "Prezantimi i skedave. Organizoni punën tuaj dhe seancat aktive me skeda", + "help.tabs.toast.dismiss": "Hiq informacionin e skedave", + "help.tabs.title": "Prezantimi i skedave", + "help.tabs.description": "Organizoni punën tuaj dhe seancat aktive me skeda", + "help.tabs.date": "14 korrik", + "help.tabs.introduction": "OpenCode Desktop tani është ndërtuar rreth skedave.", + "help.tabs.sessions": + "Filloni një sesion të ri në një skedë ose hapni një sesion ekzistues nga ndonjë prej projekteve tuaja. Hapni një skedë të re kur filloni diçka të re dhe mbylleni kur të keni mbaruar.", + "help.tabs.organize": + "Mbajtja e disa skedave të hapura e bën më të lehtë organizimin e seancave tuaja aktive. Riemërtoni skedat në diçka të paharrueshme nëse planifikoni t'i mbani ato përreth.", + "help.tabs.home": + "Do t'i gjeni të gjitha seancat dhe projektet tuaja në ekranin e ri Home. Zgjedhja e një sesioni e hap atë në një skedë.", + "help.tabs.persistence": "Kur rihapni aplikacionin, skedat tuaja janë ende të hapura.", + "help.tabs.worktrees": + 'Dizajni i ri nuk mbështet ende Git Worktrees, ai vjen së shpejti. Pra, nëse preferoni të vazhdoni të përdorni paraqitjen e mëparshme, mund të kaloni midis paraqitjeve te "Cilësimet". Vetëm mbani në mend se faqosja e re do të bëhet e përhershme brenda disa javësh.', + "server.row.noUsername": "asnjë emër përdoruesi", + "dialog.project.edit.title": "Redakto projektin", + "dialog.project.edit.name": "Emri", + "dialog.project.edit.icon": "Ikona", + "dialog.project.edit.icon.alt": "Ikona e projektit", + "dialog.project.edit.icon.hint": "Klikoni ose tërhiqni një imazh", + "dialog.project.edit.icon.recommended": "Rekomandohet: 128x128px", + "dialog.project.edit.color": "Ngjyra", + "dialog.project.edit.color.select": "Zgjidhni ngjyrën {{color}}", + "dialog.project.edit.worktree.startup": "Skript i nisjes së hapësirës së punës", + "dialog.project.edit.worktree.startup.description": + "Ekzekutohet pas krijimit të një hapësire të re pune (pema e punës).", + "dialog.project.edit.worktree.startup.placeholder": "p.sh. bun install", + "dialog.releaseNotes.action.getStarted": "Filloni", + "dialog.releaseNotes.action.next": "Tjetra", + "dialog.releaseNotes.action.hideFuture": "Mos i shfaq këto në të ardhmen", + "dialog.releaseNotes.media.alt": "Pamja paraprake e publikimit", + "context.breakdown.title": "Zbërthimi i kontekstit", + "context.breakdown.note": + 'Zbërthimi i përafërt i shenjave hyrëse. "Të tjera" përfshin përkufizimet e mjeteve dhe shpenzimet e përgjithshme.', + "context.breakdown.system": "Sistemi", + "context.breakdown.user": "Përdoruesi", + "context.breakdown.assistant": "Asistent", + "context.breakdown.tool": "Thirrjet e mjeteve", + "context.breakdown.other": "Të tjera", + "context.systemPrompt.title": "Prompt i sistemit", + "context.rawMessages.title": "Mesazhe të papërpunuara", + "context.export.session": "Sesioni i eksportit", + "context.stats.session": "Sesioni", + "context.stats.messages": "Mesazhet", + "context.stats.provider": "Ofruesi", + "context.stats.model": "Model", + "context.stats.limit": "Kufiri i kontekstit", + "context.stats.totalTokens": "Tokenet totale", + "context.stats.usage": "Përdorimi", + "context.stats.inputTokens": "Shenjat hyrëse", + "context.stats.outputTokens": "Shenjat e daljes", + "context.stats.reasoningTokens": "Shenjat e arsyetimit", + "context.stats.cacheTokens": "Shenjat e memories (lexo/shkruaj)", + "context.stats.userMessages": "Mesazhet e Përdoruesit", + "context.stats.assistantMessages": "Mesazhet e asistentit", + "context.stats.totalCost": "Kostoja totale", + "context.stats.sessionCreated": "Sesioni u krijua", + "context.stats.lastActivity": "Aktiviteti i fundit", + "context.usage.tokens": "Shenjat", + "context.usage.usage": "Përdorimi", + "context.usage.cost": "Kostoja", + "context.usage.clickToView": "Klikoni për të parë kontekstin", + "context.usage.view": "Shikoni përdorimin e kontekstit", + "language.en": "anglisht", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "Arabisht", + "language.no": "Norsk", + "language.br": "portugalisht (Brazil)", + "language.bs": "Bosanski", + "language.uk": "Ukrainas", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Gjuha", + "toast.language.description": "Kaloi në {{language}}", + "toast.theme.title": "Tema u ndërrua", + "toast.scheme.title": "Skema e ngjyrave", + "toast.workspace.enabled.title": "Hapësirat e punës janë aktivizuar", + "toast.workspace.enabled.description": "Pemët e shumta të punës shfaqen tani në shiritin anësor", + "toast.workspace.disabled.title": "Hapësirat e punës janë çaktivizuar", + "toast.workspace.disabled.description": "Vetëm pema kryesore e punës shfaqet në shiritin anësor", + "toast.permissions.autoaccept.on.title": "Auto-pranimi i lejeve", + "toast.permissions.autoaccept.on.description": "Kërkesat për leje do të miratohen automatikisht", + "toast.permissions.autoaccept.off.title": "Ndaloi pranimin automatik të lejeve", + "toast.permissions.autoaccept.off.description": "Kërkesat për leje do të kërkojnë miratim", + "toast.model.none.title": "Asnjë model i zgjedhur", + "toast.model.none.description": "Lidhni një ofrues për të përmbledhur këtë sesion", + "toast.file.loadFailed.title": "Ngarkimi i skedarit dështoi", + "toast.file.listFailed.title": "Lista e skedarëve dështoi", + "toast.context.noLineSelection.title": "Nuk ka përzgjedhje rreshti", + "toast.context.noLineSelection.description": "Së pari zgjidhni një varg rreshti në një skedë skedari.", + "toast.session.share.copyFailed.title": "Kopjimi i URL në kujtesën e fragmenteve dështoi", + "toast.session.share.success.title": "Sesioni i përbashkët", + "toast.session.share.success.description": "Ndani URL kopjuar në kujtesën e fragmenteve!", + "toast.session.share.failed.title": "Ndarja e sesionit dështoi", + "toast.session.share.failed.description": "Ndodhi një gabim gjatë ndarjes së seancës", + "toast.session.unshare.success.title": "Sesioni u anulua", + "toast.session.unshare.success.description": "Sesioni u anulua me sukses!", + "toast.session.unshare.failed.title": "Shpërndarja e sesionit dështoi", + "toast.session.unshare.failed.description": "Ndodhi një gabim gjatë anulimit të seancës", + "toast.session.export.success.title": "Sesioni u eksportua", + "toast.session.export.success.description": "Sesioni u ruajt në {{filename}}", + "toast.session.export.failed.title": "Eksportimi i sesionit dështoi", + "toast.session.export.failed.description": "Ndodhi një gabim gjatë eksportimit të sesionit", + "toast.session.listFailed.title": "Ngarkimi i seancave për {{project}} dështoi", + "toast.project.reloadFailed.title": "Ringarkimi i {{project}} dështoi", + "toast.update.title": "Ofrohet përditësim", + "toast.update.description": "Një version i ri i OpenCode ({{version}}) është tani i disponueshëm për t'u instaluar.", + "toast.update.action.installRestart": "Instaloni dhe rinisni", + "toast.update.action.notYet": "Ende jo", + "error.page.title": "Diçka shkoi keq", + "error.page.description": "Ndodhi një gabim gjatë ngarkimit të aplikacionit.", + "error.page.description.localServerStartup": "Ndodhi një gabim gjatë nisjes së serverit lokal.", + "error.page.details.label": "Detajet e gabimit", + "error.page.action.restart": "Rinis", + "error.page.action.report": "Raportoni një gabim", + "error.page.action.reported": "Gabim i raportuar", + "error.page.action.exportLogs": "Eksporto regjistrat", + "error.page.action.checking": "Po kontrollon...", + "error.page.action.checkUpdates": "Kontrolloni për përditësime", + "error.page.action.updateTo": "Përditëso në {{version}}", + "error.page.circular": "[Rrethore]", + "error.page.report.prefix": "Ju lutemi raportoni këtë gabim te ekipi i OpenCode", + "error.page.report.discord": "mbi Mosmarrëveshjen", + "error.page.version": "Versioni: {{version}}", + "error.dev.rootNotFound": + "Elementi rrënjë nuk u gjet. A keni harruar ta shtoni atë në index.html tuaj? Apo ndoshta atributi id është shkruar gabim?", + "error.serverSync.connectFailed": "Nuk mund të lidhej me serverin. A ka një server që funksionon në `{{url}}`?", + "error.serverSDK.noServerAvailable": "Asnjë server i disponueshëm", + "error.serverSDK.serverNotAvailable": "Serveri nuk disponohet", + "error.childStore.persistedCacheCreateFailed": "Dështoi në krijimin e memories së vazhdueshme", + "error.childStore.persistedProjectMetadataCreateFailed": + "Krijimi i meta të dhënave të vazhdueshme të projektit dështoi", + "error.childStore.persistedProjectIconCreateFailed": "Krijimi i ikonës së projektit të vazhdueshëm dështoi", + "error.childStore.storeCreateFailed": "Krijimi i dyqanit dështoi", + "directory.error.invalidUrl": "Drejtoria e pavlefshme në URL.", + "error.chain.unknown": "Gabim i panjohur", + "error.server.invalidConfiguration": "Konfigurim i pavlefshëm", + "error.chain.causedBy": "Shkaktuar nga:", + "error.chain.apiError": "Gabim API", + "error.chain.status": "Statusi: {{status}}", + "error.chain.retryable": "Mund të provohet përsëri: {{retryable}}", + "error.chain.responseBody": "Trupi i përgjigjes:\n{{body}}", + "error.chain.didYouMean": "A do të thuash: {{suggestions}}", + "error.chain.modelNotFound": "Modeli nuk u gjet: {{provider}}/{{model}}", + "error.chain.checkConfig": "Kontrolloni emrat e ofruesit/modelit tuaj të konfigurimit (opencode.json).", + "error.chain.mcpFailed": 'Serveri MCP "{{name}}" dështoi. Shënim, OpenCode nuk e mbështet ende vërtetimin MCP.', + "error.chain.providerAuthFailed": "Autentifikimi i ofruesit dështoi ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Inicializimi i ofruesit "{{provider}}" dështoi. Kontrolloni kredencialet dhe konfigurimin.', + "error.chain.configJsonInvalid": "Skedari i konfigurimit në {{path}} nuk është i vlefshëm JSON(C)", + "error.chain.configJsonInvalidWithMessage": + "Skedari i konfigurimit në {{path}} nuk është i vlefshëm JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Drejtoria "{{dir}}" në {{path}} nuk është e vlefshme. Riemërtoni direktorinë në "{{suggestion}}" ose hiqeni atë. Ky është një gabim i zakonshëm.', + "error.chain.configFrontmatterError": "Dështoi në analizimin e materialit të parë në {{path}}:\n{{message}}", + "error.chain.configInvalid": "Skedari i konfigurimit në {{path}} është i pavlefshëm", + "error.chain.configInvalidWithMessage": "Skedari i konfigurimit në {{path}} është i pavlefshëm: {{message}}", + "notification.permission.title": "Kërkohet leje", + "notification.permission.description": "{{sessionTitle}} në {{projectName}} ka nevojë për leje", + "notification.question.title": "Pyetje", + "notification.question.description": "{{sessionTitle}} në {{projectName}} ka një pyetje", + "notification.action.goToSession": "Shkoni në seancë", + "notification.session.responseReady.title": "Përgjigja gati", + "notification.session.error.title": "Gabim në seancë", + "notification.session.error.fallbackDescription": "Ndodhi një gabim", + "home.recentProjects": "Projektet e fundit", + "home.empty.title": "Asnjë projekt i fundit", + "home.empty.description": "Filloni duke hapur një projekt lokal", + "home.title": "Kreu", + "home.projects": "Projektet", + "home.project.add": "Shto projekt", + "home.recentlyClosed": "Mbyllur së fundmi", + "home.server.collapse": "Palos projektet e serverit", + "home.server.expand": "Zgjeroni projektet e serverëve", + "home.sessions.search.placeholder": "Kërko seancat", + "home.sessions.search.placeholder.scoped": "Kërkoni sesione në {{scope}}", + "home.sessions.search.sessions": "Sesionet", + "home.sessions.search.noResults": "Nuk u gjetën seanca për {{query}}", + "home.sessions.empty": "Asgjë këtu ende", + "home.sessions.empty.description": "Krijo një seancë për të filluar", + "home.sessions.group.today": "Sot", + "home.sessions.group.yesterday": "Dje", + "home.sessions.group.older": "Më të vjetër", + "home.providerTip": "Lidhu me 75+ ofrues për të përdorur modele të tjera, duke përfshirë Claude, GPT, Gemini, etj.", + "session.tab.session": "Sesioni", + "session.tab.review": "Rishikimi", + "session.tab.context": "Konteksti", + "session.tab.unknown": "Sesion i panjohur", + "session.panel.reviewAndFiles": "Rishikimi dhe dosjet", + "session.error.notFound": "Ky sesion nuk mund të gjendet", + "session.error.notFound.description": "Kjo skedë tregon për një sesion që nuk ekziston më në këtë server.", + "session.error.notFound.closeTab": "Mbyll skedën", + "session.error.serverConnection": "Nuk mund të lidhet me këtë server", + "session.review.filesChanged": "Skedarët u ndryshuan {{count}}", + "session.review.change.one": "Ndryshimi", + "session.review.change.other": "Ndryshimet", + "session.review.loadingChanges": "Ndryshimet po ngarkohen...", + "session.review.empty": "Ende nuk ka ndryshime në këtë seancë", + "session.review.noVcs": "Nuk u zbulua sistemi i kontrollit të versionit Git, ndryshimet nuk shfaqen", + "session.review.noVcs.createGit.title": "Krijoni një depo Git", + "session.review.noVcs.createGit.description": "Gjurmoni, rishikoni dhe anuloni ndryshimet në këtë projekt", + "session.review.noVcs.createGit.actionLoading": "Krijimi i depove Git...", + "session.review.noVcs.createGit.action": "Krijo depo Git", + "session.review.noSnapshot": + "Ndjekja e fotografive është çaktivizuar në konfigurim, kështu që ndryshimet e sesionit nuk ofrohen", + "session.review.noChanges": "Nuk ka ndryshime", + "session.review.noUncommittedChanges": "Ende nuk ka ndryshime të pazgjedhura", + "session.review.noBranchChanges": "Ende nuk ka ndryshime në degë", + "session.files.selectToOpen": "Zgjidhni një skedar për të hapur", + "session.files.all": "Të gjithë skedarët", + "session.files.empty": "Nuk ka skedarë", + "session.files.binaryContent": "Skedar binar (përmbajtja nuk mund të shfaqet)", + "session.messages.renderEarlier": "Jepni mesazhe të mëparshme", + "session.messages.loadingEarlier": "Mesazhet e mëparshme po ngarkohen...", + "session.messages.loadEarlier": "Ngarko mesazhet e mëparshme", + "session.messages.loading": "Po ngarkon mesazhet...", + "session.messages.jumpToLatest": "Kalo tek e fundit", + "session.context.addToContext": "Shtoni {{selection}} në kontekst", + "session.todo.title": "Detyrat", + "session.todo.collapse": "Kolapsi", + "session.todo.expand": "Zgjerojeni", + "session.todo.progress": "{{done}} e {{total}} detyrat e përfunduara", + "session.question.progress": "{{current}} e pyetjeve {{total}}", + "session.question.minimize": "Minimizo pyetjen", + "session.question.restore": "Rivendos pyetjen", + "session.question.pending.one": "{{count}} pyetje në pritje", + "session.question.pending.other": "{{count}} pyetje në pritje", + "session.followupDock.summary.one": "Mesazhi në radhë {{count}}", + "session.followupDock.summary.other": "{{count}} mesazhe në radhë", + "session.followupDock.sendNow": "Dërgo tani", + "session.followupDock.edit": "Redakto", + "session.followupDock.collapse": "Palos mesazhet në radhë", + "session.followupDock.expand": "Zgjero mesazhet në radhë", + "session.revertDock.summary.one": "{{count}} u kthye në mesazh", + "session.revertDock.summary.other": "{{count}} i ktheu mesazhet", + "session.revertDock.collapse": "Palos mesazhet e kthyera", + "session.revertDock.expand": "Zgjero mesazhet e kthyera", + "session.revertDock.restore": "Rivendos mesazhin", + "session.new.title": "Ndërtoni ndonjë gjë", + "session.new.project.new": "Projekt i ri", + "session.new.project.search": "Kërko projekte", + "session.new.project.add": "Shto projekt", + "session.new.worktree.main": "Dega kryesore", + "session.new.worktree.mainWithBranch": "Dega kryesore ({{branch}})", + "session.new.worktree.create": "Krijo një pemë të re pune", + "session.new.workspace.runIn": "Drejtoni seancën brenda", + "session.new.workspace.triggerLocal": "Lokale", + "session.new.workspace.local": "Depo lokale", + "session.new.workspace.existing": "Hapësira e punës…", + "session.new.git.none": "Jo Git", + "session.new.lastModified": "Ndryshuar së fundi", + "session.header.search.placeholder": "Kërko {{project}}", + "session.header.searchFiles": "Kërko skedarët", + "session.header.openIn": "Hape brenda", + "session.header.open.action": "Hap {{app}}", + "session.header.open.ariaLabel": "Hapni në {{app}}", + "session.header.open.menu": "Hap opsionet", + "session.header.open.copyPath": "Kopjo rrugën", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Eksploruesi i skedarëve", + "session.header.open.fileManager": "Menaxheri i skedarëve", + "session.header.reveal.finder": "Zbulojeni në Finder", + "session.header.reveal.fileExplorer": "Zbulojeni në File Explorer", + "session.header.reveal.containingFolder": "Hap dosjen që përmban", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminali", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Statusi", + "status.popover.ariaLabel": "Konfigurimet e serverit", + "status.popover.tab.servers": "Serverat", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Shtojcat", + "status.popover.action.manageServers": "Menaxho serverët", + "session.share.popover.title": "Publikimi në ueb", + "session.share.popover.description.shared": + "Ky sesion është publik në ueb. Është i aksesueshëm për këdo që ka lidhjen.", + "session.share.popover.description.unshared": + "Ndani sesionin publikisht në ueb. Ai do të jetë i aksesueshëm për këdo që ka lidhjen.", + "session.share.action.share": "Shpërndaje", + "session.share.action.publish": "Publikoni", + "session.share.action.publishing": "Po publikon...", + "session.share.action.unpublish": "Anulo publikimin", + "session.share.action.unpublishing": "Po anulohet publikimi...", + "session.share.action.view": "Shiko", + "session.share.copy.copied": "E kopjuar", + "session.share.copy.copyLink": "Kopjo lidhjen", + "lsp.tooltip.none": "Nuk ka serverë LSP", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Kërkesa po ngarkohet...", + "terminal.loading": "Po ngarkohet terminali...", + "terminal.title": "Terminali", + "terminal.title.numbered": "Terminali {{number}}", + "terminal.close": "Mbyll terminalin", + "terminal.connectionLost.title": "Lidhja e humbur", + "terminal.connectionLost.abnormalClose": "WebSocket u mbyll anormalisht: {{code}}", + "terminal.connectionLost.description": "Lidhja e terminalit u ndërpre. Kjo mund të ndodhë kur serveri riniset.", + "terminal.connectTicket.csrfError": + "Bileta lidhëse PTY e refuzuar nga origjina ose çeqet CSRF. Kontrolloni konfigurimin e serverit CORS.", + "terminal.connectTicket.statusError": "Bileta e lidhjes PTY dështoi me {{status}}", + "titlebar.update": "Përditëso", + "titlebar.updateVersion": "Përditëso {{version}}", + "common.closeTab": "Mbyll skedën", + "common.dismiss": "Largoje", + "common.moreCountSuffix": " (+{{count}} më shumë)", + "common.requestFailed": "Kërkesa dështoi", + "common.moreOptions": "Më shumë opsione", + "common.learnMore": "Mësoni më shumë", + "common.rename": "Riemërto", + "common.export": "Eksporto", + "common.reset": "Rivendos", + "common.archive": "Arkivi", + "common.delete": "Fshi", + "common.close": "Mbylle", + "common.edit": "Redakto", + "common.loadMore": "Ngarko më shumë", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "i panjohur", + "common.time.justNow": "Vetëm tani", + "common.time.minutesAgo.short": "{{count}}m më parë", + "common.time.hoursAgo.short": "{{count}}h më parë", + "common.time.daysAgo.short": "{{count}}d më parë", + "sidebar.menu.toggle": "Ndrysho menynë", + "sidebar.nav.projectsAndSessions": "Projektet dhe sesionet", + "sidebar.settings": "Cilësimet", + "sidebar.help": "Ndihmë", + "sidebar.workspaces.enable": "Aktivizo hapësirat e punës", + "sidebar.workspaces.disable": "Çaktivizo hapësirat e punës", + "sidebar.gettingStarted.title": "Duke filluar", + "sidebar.gettingStarted.line1": "OpenCode përfshin modele falas, kështu që mund të filloni menjëherë.", + "sidebar.gettingStarted.line2": "Lidhni çdo ofrues për të përdorur modele, inc. Claude, GPT, Gemini etj.", + "sidebar.project.recentSessions": "Seancat e fundit", + "sidebar.project.viewAllSessions": "Shikoni të gjitha seancat", + "sidebar.project.clearNotifications": "Pastro njoftimet", + "sidebar.empty.title": "Asnjë projekt i hapur", + "sidebar.empty.description": "Hapni një projekt për të filluar", + "debugBar.ariaLabel": "Diagnostifikimi i performancës së zhvillimit", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Tranzicioni i fundit i përfunduar i rrugës duke prekur një faqe sesioni, i matur nga fillimi i routerit deri në bojën e parë pasi të vendoset.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Korniza rrotulluese për sekondë gjatë 5 sekondave të fundit.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Koha më e keqe e kuadrove gjatë 5 sekondave të fundit.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Korniza mbi 32 ms në 5 sekondat e fundit.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Koha e bllokuar dhe numërimi i detyrave të gjata në 5 sekondat e fundit. Detyra maksimale: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Vonesa më e keqe e vërejtur e hyrjes në 5 sekondat e fundit.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Kohëzgjatja e përafërt e ndërveprimit gjatë 5 sekondave të fundit. Ky është i ngjashëm me INP, jo Web Vitals zyrtar INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Zhvendosja kumulative e paraqitjes për jetëgjatësinë aktuale të aplikacionit.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Grup i përdorur JS kundrejt kufirit të grumbullit. Vetëm Chromium.", + "debugBar.mem.tip": "Grup i përdorur JS kundrejt kufirit të grumbullit. {{used}} e {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Forconi stilet e fokusimit në të gjithë elementët ndërveprues", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Forco drejtimin e plotë të paraqitjes së aplikacionit pa ndryshuar gjuhën e zgjedhur", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Desktop", + "settings.section.server": "Serveri", + "settings.tab.general": "Gjeneral", + "settings.tab.shortcuts": "Shkurtoret", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Integrimi WSL", + "settings.desktop.wsl.description": "Ekzekutoni serverin OpenCode brenda WSL në Windows.", + "settings.general.section.appearance": "Pamja e jashtme", + "settings.general.section.advanced": "E avancuar", + "settings.general.section.notifications": "Njoftimet e sistemit", + "settings.general.section.updates": "Përditësimet", + "settings.general.section.sounds": "Efektet zanore", + "settings.general.section.feed": "Prurja", + "settings.general.section.display": "Ekrani", + "settings.general.row.language.title": "Gjuha", + "settings.general.row.language.description": "Ndryshoni gjuhën e ekranit për OpenCode", + "settings.general.row.shell.title": "Predha e terminalit", + "settings.general.row.shell.description": "Predha e përdorur nga terminali dhe mjetet e agjentit", + "settings.general.row.shell.autoDefault": "Auto (e parazgjedhur)", + "settings.general.row.shell.terminalOnly": "vetëm terminali", + "settings.general.row.appearance.title": "Pamja e jashtme", + "settings.general.row.appearance.description": "Personalizojeni se si duket OpenCode në pajisjen tuaj", + "settings.general.row.colorScheme.title": "Skema e ngjyrave", + "settings.general.row.colorScheme.description": + "Zgjidhni nëse OpenCode ndjek temën e sistemit, të lehtë ose të errët", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Personalizojeni se si është tema e OpenCode.", + "settings.general.row.font.title": "Fonti i kodit", + "settings.general.row.font.description": "Personalizoni fontin e përdorur në blloqet e kodit", + "settings.general.row.terminalFont.title": "Fonti i Terminalit", + "settings.general.row.terminalFont.description": "Personalizojeni fontin e përdorur në terminal", + "settings.general.row.uiFont.title": "Fonti UI", + "settings.general.row.uiFont.description": "Personalizoni fontin e përdorur në të gjithë ndërfaqen", + "settings.general.row.followup.title": "Sjellja pasuese", + "settings.general.row.followup.description": + "Zgjidhni nëse kërkesat e ndjekjes drejtohen menjëherë ose prisni në një radhë", + "settings.general.row.followup.option.queue": "Radhë", + "settings.general.row.followup.option.steer": "Drejtoni", + "settings.general.row.showFileTree.title": "Pema e skedarit", + "settings.general.row.showFileTree.description": "Shfaq panelin e pemës së skedarëve në sesione", + "settings.general.row.showNavigation.title": "Kontrollet e navigimit", + "settings.general.row.showNavigation.description": + "Shfaqni butonat prapa dhe përpara në shiritin e titullit të desktopit", + "settings.general.row.showSearch.title": "Paleta e komandave", + "settings.general.row.showSearch.description": + "Shfaq butonin e paletës së kërkimit dhe komandës në shiritin e titullit", + "settings.general.row.showTerminal.title": "Terminali", + "settings.general.row.showTerminal.description": "Shfaq butonin e terminalit në shiritin e titullit të desktopit", + "settings.general.row.showStatus.title": "Statusi i serverit", + "settings.general.row.showStatus.description": "Shfaq butonin e statusit të serverit në shiritin e titullit", + "settings.general.row.mobileTitlebarBottom.title": "Navigimi i poshtëm", + "settings.general.row.mobileTitlebarBottom.description": + "Vendosni shiritin e titullit dhe skedat e sesionit në fund të ekranit në celular", + "settings.general.row.showCustomAgents.title": "Trego agjentin", + "settings.general.row.showCustomAgents.description": + "Kaloni ndërmjet agjentëve në kompozitor. Kur fshihet, si parazgjedhje është Build agent.", + "settings.general.row.reasoningSummaries.title": "Trego përmbledhjet e arsyetimit", + "settings.general.row.reasoningSummaries.description": "Shfaqni përmbledhjet e arsyetimit të modelit në afatin kohor", + "settings.general.row.shellToolPartsExpanded.title": "Zgjeroni pjesët e veglave të guaskës", + "settings.general.row.shellToolPartsExpanded.description": + "Shfaq pjesët e veglës së guaskës të zgjeruara si parazgjedhje në afatin kohor", + "settings.general.row.editToolPartsExpanded.title": "Zgjero pjesët e veglave të redaktimit", + "settings.general.row.editToolPartsExpanded.description": + "Shfaq pjesët e veglave të modifikimit, shkrimit dhe korrigjimit të zgjeruara si parazgjedhje në afatin kohor", + "settings.general.row.newInterface.title": "Paraqitje e re", + "settings.general.row.newInterface.badge": "E re", + "settings.general.row.newInterface.description": + "Përdorni skedat e reja dhe paraqitjen e shtëpisë. Kaloni ndërmjet paraqitjeve për një kohë të kufizuar.", + "settings.general.row.newInterfaceNotice.title": "Tani po përdorni strukturë të re", + "settings.general.row.newInterfaceNotice.description": "Paraqitja e mëparshme nuk është më e disponueshme", + "settings.general.row.newInterfaceNotice.dismiss": "Largoje", + "settings.general.row.pinchZoom.title": "Pini për të zmadhuar", + "settings.general.row.pinchZoom.description": "Lejo zmadhimin e kapjes së tastierës dhe gjestet e lëvizjes Ctrl", + "settings.general.row.wayland.title": "Përdorni Wayland amtare", + "settings.general.row.wayland.description": "Çaktivizo kthimin e X11 në Wayland. Kërkon rinisje.", + "settings.general.row.wayland.tooltip": + "Në Linux me monitorë të përzier me shpejtësi rifreskimi, Wayland vendas mund të jetë më i qëndrueshëm.", + "settings.general.row.releaseNotes.title": "Shënimet e publikimit", + "settings.general.row.releaseNotes.description": "Shfaq dritaret kërcyese Çfarë ka të re pas përditësimeve", + "settings.updates.row.startup.title": "Kontrolloni për përditësime gjatë nisjes", + "settings.updates.row.startup.description": "Kontrolloni automatikisht për përditësime kur niset OpenCode", + "settings.updates.row.check.title": "Kontrolloni për përditësime", + "settings.updates.row.check.description": "Kontrolloni manualisht për përditësime dhe instaloni nëse ka", + "settings.updates.action.checkNow": "Kontrollo tani", + "settings.updates.action.checking": "Po kontrollon...", + "settings.updates.action.downloading": "Po shkarkohet...", + "settings.updates.action.installing": "Po instalohet...", + "settings.updates.toast.latest.title": "Ju jeni të përditësuar", + "settings.updates.toast.latest.description": "Ju jeni duke ekzekutuar versionin më të fundit të OpenCode.", + "sound.option.none": "Asnjë", + "sound.option.alert01": "Alarmi 01", + "sound.option.alert02": "Alarmi 02", + "sound.option.alert03": "Alarmi 03", + "sound.option.alert04": "Alarmi 04", + "sound.option.alert05": "Alarmi 05", + "sound.option.alert06": "Alarmi 06", + "sound.option.alert07": "Alarmi 07", + "sound.option.alert08": "Alarmi 08", + "sound.option.alert09": "Alarmi 09", + "sound.option.alert10": "Alarmi 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Jo 01", + "sound.option.nope02": "Jo 02", + "sound.option.nope03": "Jo 03", + "sound.option.nope04": "Jo 04", + "sound.option.nope05": "Jo 05", + "sound.option.nope06": "Jo 06", + "sound.option.nope07": "Jo 07", + "sound.option.nope08": "Jo 08", + "sound.option.nope09": "Jo 09", + "sound.option.nope10": "Jo 10", + "sound.option.nope11": "Jo 11", + "sound.option.nope12": "Jo 12", + "sound.option.yup01": "Po 01", + "sound.option.yup02": "Po 02", + "sound.option.yup03": "Po 03", + "sound.option.yup04": "Po 04", + "sound.option.yup05": "Po 05", + "sound.option.yup06": "Po 06", + "settings.general.notifications.agent.title": "Agjenti", + "settings.general.notifications.agent.description": + "Shfaq njoftimin e sistemit kur agjenti është i plotë ose ka nevojë për vëmendje", + "settings.general.notifications.permissions.title": "Lejet", + "settings.general.notifications.permissions.description": "Shfaq njoftimin e sistemit kur kërkohet një leje", + "settings.general.notifications.errors.title": "Gabimet", + "settings.general.notifications.errors.description": "Shfaq njoftimin e sistemit kur ndodh një gabim", + "settings.general.sounds.agent.title": "Agjenti", + "settings.general.sounds.agent.description": "Luaj tingullin kur agjenti është i plotë ose ka nevojë për vëmendje", + "settings.general.sounds.permissions.title": "Lejet", + "settings.general.sounds.permissions.description": "Luaj tingull kur kërkohet leje", + "settings.general.sounds.errors.title": "Gabimet", + "settings.general.sounds.errors.description": "Luaj tingullin kur ndodh një gabim", + "settings.shortcuts.title": "Shkurtoret e tastierës", + "settings.shortcuts.reset.button": "Rivendos në standardet", + "settings.shortcuts.reset.toast.title": "Rivendosja e shkurtoreve", + "settings.shortcuts.reset.toast.description": "Shkurtoret e tastierës janë rivendosur në standardet.", + "settings.shortcuts.conflict.title": "Shkurtorja tashmë është në përdorim", + "settings.shortcuts.conflict.description": "{{keybind}} i është caktuar tashmë {{titles}}.", + "settings.shortcuts.unassigned": "E pacaktuar", + "settings.shortcuts.pressKeys": "Shtypni tastet", + "settings.shortcuts.search.placeholder": "Shkurtoret e kërkimit", + "settings.shortcuts.search.empty": "Nuk u gjetën shkurtore", + "settings.shortcuts.group.general": "Gjeneral", + "settings.shortcuts.group.session": "Sesioni", + "settings.shortcuts.group.navigation": "Navigimi", + "settings.shortcuts.group.modelAndAgent": "Model dhe agjent", + "settings.shortcuts.group.terminal": "Terminali", + "settings.shortcuts.group.prompt": "Kërkesë", + "settings.providers.title": "Ofruesit", + "settings.providers.description": "Cilësimet e ofruesit do të konfigurohen këtu.", + "settings.providers.section.connected": "Ofruesit e lidhur", + "settings.providers.connected.empty": "Nuk ka ofrues të lidhur", + "settings.providers.connected.environmentDescription": "Lidhur nga variablat e mjedisit tuaj", + "settings.providers.section.popular": "Ofruesit e njohur", + "settings.providers.custom.description": "Shtoni një ofrues të pajtueshëm me OpenAI nga baza URL.", + "settings.providers.tag.environment": "Mjedisi", + "settings.providers.tag.config": "Konfigurimi", + "settings.providers.tag.custom": "Me porosi", + "settings.providers.tag.other": "Të tjera", + "settings.models.title": "Modelet", + "settings.models.description": "Cilësimet e modelit do të konfigurohen këtu.", + "settings.agents.title": "Agjentët", + "settings.agents.description": "Cilësimet e agjentit do të konfigurohen këtu.", + "settings.commands.title": "Komandat", + "settings.commands.description": "Cilësimet e komandës do të konfigurohen këtu.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Cilësimet MCP do të konfigurohen këtu.", + "settings.permissions.title": "Lejet", + "settings.permissions.description": "Kontrolloni se cilat mjete mund të përdorë serveri si parazgjedhje.", + "settings.permissions.section.tools": "Mjetet", + "settings.permissions.toast.updateFailed.title": "Përditësimi i lejeve dështoi", + "settings.permissions.action.allow": "Lejo", + "settings.permissions.action.ask": "Pyetni", + "settings.permissions.action.deny": "Moho", + "settings.permissions.tool.read.title": "Lexoni", + "settings.permissions.tool.read.description": "Leximi i një skedari (përputhet me shtegun e skedarit)", + "settings.permissions.tool.edit.title": "Redakto", + "settings.permissions.tool.edit.description": "Ndryshoni skedarët, duke përfshirë redaktimet, shkrimet dhe arnimet", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Përputhni skedarët duke përdorur modele glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Kërkoni përmbajtjen e skedarit duke përdorur shprehje të rregullta", + "settings.permissions.tool.list.title": "Lista", + "settings.permissions.tool.list.description": "Listoni skedarët brenda një drejtorie", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Ekzekutoni komandat e guaskës", + "settings.permissions.tool.task.title": "Detyrë", + "settings.permissions.tool.task.description": "Nisni nën-agjentët", + "settings.permissions.tool.skill.title": "Shkathtësi", + "settings.permissions.tool.skill.description": "Ngarko një aftësi me emër", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Drejtoni pyetjet e serverit të gjuhës", + "settings.permissions.tool.todowrite.title": "Todo Shkruani", + "settings.permissions.tool.todowrite.description": "Përditësoni listën e detyrave", + "settings.permissions.tool.webfetch.title": "Marrja në ueb", + "settings.permissions.tool.webfetch.description": "Merr përmbajtje nga një URL", + "settings.permissions.tool.websearch.title": "Kërkimi në ueb", + "settings.permissions.tool.websearch.description": "Kërkoni në ueb", + "settings.permissions.tool.external_directory.title": "Drejtoria e jashtme", + "settings.permissions.tool.external_directory.description": "Qasni skedarët jashtë drejtorisë së projektit", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Zbuloni thirrjet e përsëritura të mjeteve me hyrje identike", + "session.delete.failed.title": "Fshirja e sesionit dështoi", + "session.delete.title": "Fshi sesionin", + "session.delete.confirm": 'Të fshihet sesioni "{{name}}"?', + "session.delete.button": "Fshi sesionin", + "workspace.new": "Hapësirë e re pune", + "workspace.type.local": "lokale", + "workspace.type.sandbox": "mjedis i izoluar", + "workspace.create.failed.title": "Krijimi i hapësirës së punës dështoi", + "workspace.delete.failed.title": "Fshirja e hapësirës së punës dështoi", + "workspace.resetting.title": "Rivendosja e hapësirës së punës", + "workspace.resetting.description": "Kjo mund të zgjasë një minutë.", + "workspace.reset.failed.title": "Rivendosja e hapësirës së punës dështoi", + "workspace.reset.success.title": "Rivendosja e hapësirës së punës", + "workspace.reset.success.description": "Hapësira e punës tani përputhet me degën e paracaktuar.", + "workspace.error.stillPreparing": "Hapësira e punës është ende duke u përgatitur", + "workspace.status.checking": "Po kontrollon për ndryshime të pabashkuara...", + "workspace.status.error": "Statusi i git nuk mund të verifikohet.", + "workspace.status.clean": "Nuk u zbuluan ndryshime të pabashkuara.", + "workspace.status.dirty": "Ndryshime të pabashkuara u zbuluan në këtë hapësirë pune.", + "workspace.delete.title": "Fshi hapësirën e punës", + "workspace.delete.confirm": 'Të fshihet hapësira e punës "{{name}}"?', + "workspace.delete.button": "Fshi hapësirën e punës", + "workspace.reset.title": "Rivendos hapësirën e punës", + "workspace.reset.confirm": 'Të rivendoset hapësira e punës "{{name}}"?', + "workspace.reset.button": "Rivendos hapësirën e punës", + "workspace.reset.archived.none": "Asnjë seancë aktive nuk do të arkivohet.", + "workspace.reset.archived.one": "1 sesion do të arkivohet.", + "workspace.reset.archived.many": "Seancat {{count}} do të arkivohen.", + "workspace.reset.note": "Kjo do të rivendosë hapësirën e punës që të përputhet me degën e paracaktuar.", + "dialog.usageExceeded.dontShowAgain": "Mos e shfaq përsëri", +} diff --git a/packages/app/src/i18n/sr.ts b/packages/app/src/i18n/sr.ts new file mode 100644 index 0000000000000000000000000000000000000000..f059160087d83187e97291d0035ab601461c1da5 --- /dev/null +++ b/packages/app/src/i18n/sr.ts @@ -0,0 +1,1161 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "датотека", + "desktop.menu.edit": "Уреди", + "desktop.menu.view": "Приказ", + "desktop.menu.go": "Иди", + "desktop.menu.window": "Прозор", + "desktop.menu.help": "Помоћ", + "desktop.menu.checkForUpdates": "Проверите да ли постоје ажурирања...", + "desktop.menu.settings": "Подешавања", + "desktop.menu.reloadWebview": "Поново учитај Webview", + "desktop.menu.restart": "Поново покрените", + "desktop.menu.exportLogs": "Извези евиденције...", + "desktop.menu.newSession": "Нова сесија", + "desktop.menu.openProject": "Отвори пројекат...", + "desktop.menu.newWindow": "Нови прозор", + "desktop.menu.closeWindow": "Затвори прозор", + "desktop.menu.undo": "Поништи", + "desktop.menu.redo": "Понови", + "desktop.menu.cut": "Цут", + "desktop.menu.copy": "Копирај", + "desktop.menu.paste": "Налепи", + "desktop.menu.delete": "Избриши", + "desktop.menu.selectAll": "Изаберите Све", + "desktop.menu.toggleSidebar": "Пребаци бочну траку", + "desktop.menu.toggleTerminal": "Пребаци терминал", + "desktop.menu.toggleFileTree": "Прикажи или сакриј стабло датотека", + "desktop.menu.reload": "Поново учитај", + "desktop.menu.toggleDeveloperTools": "Прикажи или сакриј развојне алате", + "desktop.menu.actualSize": "стварна величина", + "desktop.menu.zoomIn": "Зоом Ин", + "desktop.menu.zoomOut": "Умањи", + "desktop.menu.toggleFullScreen": "Пребаци преко целог екрана", + "desktop.menu.back": "Назад", + "desktop.menu.forward": "Проследи", + "desktop.menu.previousSession": "Претходна сесија", + "desktop.menu.nextSession": "Следећа сесија", + "desktop.menu.previousProject": "претходни пројекат", + "desktop.menu.nextProject": "Следећи пројекат", + "desktop.menu.minimize": "Умањи", + "desktop.menu.maximize": "Увећај", + "desktop.menu.documentation": "OpenCode Документација", + "desktop.menu.supportForum": "Форум подршке", + "desktop.menu.shareFeedback": "Делите повратне информације", + "desktop.menu.reportBug": "Пријавите грешку", + "desktop.menu.ariaLabel": "OpenCode мени", + "desktop.updater.dialog.checkFailed.message": "Провера ажурирања није успела.", + "desktop.updater.dialog.checkFailed.title": "Грешка при ажурирању", + "desktop.updater.dialog.upToDate.message": "Ажурни сте.", + "desktop.updater.dialog.upToDate.title": "Нема ажурирања", + "desktop.updater.dialog.ready.message": "Преузето ажурирање {{version}}. Поново покренути сада?", + "desktop.updater.dialog.ready.title": "Спреман за ажурирање", + "desktop.updater.dialog.restart": "Поново покрените", + "desktop.updater.dialog.later": "касније", + "desktop.recovery.action.relaunch": "Поново покрени", + "desktop.recovery.action.exportLogs": "Извоз евиденције", + "desktop.recovery.action.keepWaiting": "Кееп Ваитинг", + "desktop.recovery.action.quit": "Прекини", + "desktop.recovery.loadFailed": "OpenCode није успело учитавање", + "desktop.recovery.terminated": "OpenCode прозор је неочекивано прекинут", + "desktop.recovery.unresponsive": "OpenCode не реагује", + "desktop.recovery.unresponsive.detail": + "Можете поново покренути апликацију, отворити евиденције или наставити да чекате.", + "desktop.recovery.loadFailed.detail": "Прозор: {{window}}\nURL: {{url}}\nГрешка: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Прозор: {{window}}\nРазлог: {{reason}}\nШифра: {{code}}", + "desktop.recovery.unknown": "<непознато>", + "desktop.dialog.chooseFolder": "Изаберите фасциклу", + "desktop.dialog.chooseFile": "Изаберите датотеку", + "desktop.dialog.saveFile": "Сачувај датотеку", + "desktop.dialog.files": "датотеке", + "desktop.server.local": "Локални сервер", + "desktop.wsl.error.windowsOnly": "WSL доступан је само Windows", + "desktop.wsl.error.unavailable": "WSL је недоступан", + "desktop.wsl.error.listInstalled": "Неуспешно навођење инсталираних WSL дистрибуција", + "desktop.wsl.error.listOnline": "Неуспешно навођење онлајн WSL дистрибуција", + "desktop.wsl.error.executeDistro": "Не могу да изврше команде у дистрибуцији", + "desktop.wsl.error.installWsl": "WSL инсталација није успела", + "desktop.wsl.error.installDistro": "Инсталација дистрибуције није успела: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode инсталација није успела", + "desktop.wsl.error.alreadyAdded": "{{distro}} је већ додат", + "desktop.wsl.error.opencodeMissing": "opencode није инсталиран у овој дистрибуцији", + "desktop.wsl.error.opencodeCannotRun": "opencode је инсталиран, али не може да се покрене", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode није инсталиран у {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode ажурирање је завршено, али {{distro}} и даље извештава {{installed}}; очекивано {{expected}}", + "desktop.wsl.error.noVersion": "нема верзије", + "desktop.wsl.error.serverExited": "WSL сервер је изашао након покретања (код={{code}} сигнал={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL сервер је изашао пре него што је постао здрав (код={{code}} сигнал={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Сидецар за {{distro}} проверу здравља истекло је после {{timeout}}мс", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} истекло је после {{timeout}}мс", + "desktop.wsl.error.failedPort": "није успело преузимање порта", + "desktop.picker.error.notSelected": "Бирач није изабрао датотеку", + "desktop.picker.error.sizeLimit": "Изабрани прилози премашују ограничење од {{limit}} МБ", + "command.category.suggested": "Предложено", + "command.category.view": "Приказ", + "command.category.project": "Пројекат", + "command.category.provider": "Добављач", + "command.category.server": "Сервер", + "command.category.session": "Сесија", + "command.category.theme": "Тема", + "command.category.language": "Језик", + "command.category.file": "датотека", + "command.category.context": "Контекст", + "command.category.terminal": "Терминал", + "command.category.model": "Модел", + "command.category.mcp": "MCP", + "command.category.agent": "Агент", + "command.category.permissions": "дозволе", + "command.category.workspace": "Радни простор", + "command.category.settings": "Подешавања", + "command.logs.export": "Извоз евиденције", + "theme.scheme.system": "Систем", + "theme.scheme.light": "Светло", + "theme.scheme.dark": "Тамно", + "command.sidebar.toggle": "Пребаци бочну траку", + "command.project.open": "Отвори пројекат", + "command.project.previous": "Претходни пројекат", + "command.project.next": "Следећи пројекат", + "command.project.index": "Пређи на пројекат {{index}}", + "command.provider.connect": "Повежите добављача", + "command.server.switch": "Промени сервер", + "command.settings.open": "Отворите подешавања", + "command.session.previous": "Претходна сесија", + "command.session.next": "Следећа сесија", + "command.session.previous.unseen": "Претходна непрочитана сесија", + "command.session.next.unseen": "Следећа непрочитана сесија", + "command.session.archive": "Архивска сесија", + "command.palette": "Командна палета", + "command.theme.cycle": "Тема циклуса", + "command.theme.set": "Користите тему: {{theme}}", + "command.theme.scheme.cycle": "циклусна шема боја", + "command.theme.scheme.set": "Користи шему боја: {{scheme}}", + "command.language.cycle": "језик циклуса", + "command.language.set": "Користи језик: {{language}}", + "command.session.new": "Нова сесија", + "command.file.open": "Отворите датотеку", + "command.tab.close": "Затвори картицу", + "command.tab.reopenClosed": "Поново отворите затворену картицу", + "command.context.addSelection": "Додајте избор у контекст", + "command.context.addSelection.description": "Додајте изабране линије из тренутне датотеке", + "command.input.focus": "Фокусни унос", + "command.terminal.toggle": "Пребаци терминал", + "command.fileTree.toggle": "Пребаците стабло датотека", + "command.review.toggle": "Пребаци преглед", + "command.terminal.new": "Нови терминал", + "command.terminal.new.description": "Креирајте нову картицу терминала", + "command.steps.toggle": "Пребаците кораке", + "command.steps.toggle.description": "Прикажите или сакријте кораке за тренутну поруку", + "command.message.previous": "Претходна порука", + "command.message.previous.description": "Идите на претходну корисничку поруку", + "command.message.next": "Следећа порука", + "command.message.next.description": "Идите на следећу корисничку поруку", + "command.model.choose": "Изаберите модел", + "command.model.choose.description": "Изаберите други модел", + "command.mcp.toggle": "Пребаци MCPс", + "command.mcp.toggle.description": "Пребаци MCPс", + "command.agent.cycle": "Следећи агент", + "command.agent.cycle.description": "Пребаците се на следећег агента", + "command.agent.cycle.reverse": "Претходни агент", + "command.agent.cycle.reverse.description": "Пребаците се на претходног агента", + "command.model.variant.cycle": "Следећи ниво размишљања", + "command.model.variant.cycle.description": "Пребаците се на следећи ниво напора", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Промпт", + "command.permissions.autoaccept.enable": "Аутоматско прихватање дозвола", + "command.permissions.autoaccept.disable": "Зауставите аутоматско прихватање дозвола", + "command.workspace.toggle": "Укључите радне просторе", + "command.workspace.toggle.description": "Омогућите или онемогућите више радних простора на бочној траци", + "command.session.undo": "Поништи", + "command.session.undo.description": "Поништите последњу поруку", + "command.session.redo": "Понови", + "command.session.redo.description": "Поновите последњу поништену поруку", + "command.session.compact": "Компактна сесија", + "command.session.compact.description": "резимирајте сесију да бисте смањили величину контекста", + "command.session.fork": "Нова грана од поруке", + "command.session.fork.description": "Креирајте нову сесију од претходне поруке", + "command.session.share": "Делите сесију", + "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": "Нису пронађени резултати", + "palette.group.commands": "команде", + "palette.group.files": "датотеке", + "dialog.provider.search.placeholder": "Добављачи претраге", + "dialog.provider.empty": "Провајдери нису пронађени", + "dialog.provider.group.popular": "Популарно", + "dialog.provider.group.other": "Друго", + "dialog.provider.custom.label": "Прилагођени добављач компатибилан са OpenAI", + "dialog.provider.tag.recommended": "Препоручено", + "dialog.provider.opencode.note": "одабрани модели укључујући Claude, GPT, Gemini и више", + "dialog.provider.opencode.tagline": "Поуздани оптимизовани модели", + "dialog.provider.opencodeGo.tagline": "Ниска цена претплате за све", + "dialog.provider.anthropic.note": "Директан приступ Claude моделима, укључујући Про и Мак", + "dialog.provider.copilot.note": "АИ модели за помоћ код кодирања преко GitHub Copilot", + "dialog.provider.openai.note": "GPT модела за брзе, способне опште задатке вештачке интелигенције", + "dialog.provider.google.note": "Gemini модела за брзе, структуриране одговоре", + "dialog.provider.openrouter.note": "Приступите свим подржаним моделима од једног провајдера", + "dialog.provider.vercel.note": "Обједињен приступ АИ моделима са паметним рутирањем", + "dialog.model.select.title": "Изаберите модел", + "dialog.model.search.placeholder": "Претражите моделе", + "dialog.model.empty": "Нема резултата модела", + "dialog.model.manage": "Управљајте моделима", + "dialog.model.manage.description": "Прилагодите који се модели појављују у бирачу модела.", + "dialog.model.manage.provider.toggle": "Укључите све {{provider}} моделе", + "dialog.model.unpaid.freeModels.title": "Бесплатне моделе обезбеђује OpenCode", + "dialog.model.unpaid.addMore.title": "Додајте још модела популарних провајдера", + "dialog.model.unpaid.viewMoreProviders": "Погледајте више од 70 провајдера", + "dialog.provider.viewAll": "Прикажи више добављача", + "provider.connect.title": "Повежи {{provider}}", + "provider.connect.title.anthropicProMax": "Пријавите се са Claude Про/Мак", + "provider.connect.selectMethod": "Изаберите метод пријаве за {{provider}}.", + "provider.connect.method.apiKey": "API кључ", + "provider.connect.method.browser": "Прегледач", + "provider.connect.method.headless": "Без главе", + "provider.connect.status.inProgress": "Ауторизација је у току...", + "provider.connect.status.waiting": "Чека се ауторизација...", + "provider.connect.status.failed": "Ауторизација није успела: {{error}}", + "provider.connect.apiKey.description": + "Унесите свој {{provider}} API кључ да бисте повезали свој налог и користили {{provider}} моделе у OpenCode.", + "provider.connect.apiKey.label": "{{provider}} API кључ", + "provider.connect.apiKey.placeholder": "API кључ", + "provider.connect.apiKey.required": "API кључ је неопходан", + "provider.connect.opencodeZen.line1": + "OpenCode Zen вам даје приступ одабраном скупу поузданих оптимизованих модела за агенте за кодирање.", + "provider.connect.opencodeZen.line2": + "Са једним тастером API добићете приступ моделима као што су Claude, GPT, Gemini, GLM и други.", + "provider.connect.opencodeZen.visit.prefix": "Посетите", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "да бисте прикупили ваш API кључ.", + "provider.connect.oauth.code.visit.prefix": "Посетите", + "provider.connect.oauth.code.visit.link": "ова веза", + "provider.connect.oauth.code.visit.suffix": + "да бисте прикупили ваш ауторизациони код за повезивање налога и коришћење {{provider}} модела у OpenCode.", + "provider.connect.oauth.code.label": "{{method}} ауторизациони код", + "provider.connect.oauth.code.placeholder": "Ауторизациони код", + "provider.connect.oauth.code.required": "Потребан је код за ауторизацију", + "provider.connect.oauth.code.invalid": "Неважећи ауторизациони код", + "provider.connect.oauth.auto.visit.prefix": "Посетите", + "provider.connect.oauth.auto.visit.link": "ова веза", + "provider.connect.oauth.auto.visit.suffix": + "и унесите код испод да бисте повезали свој налог и користили {{provider}} моделе у OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "код за потврду", + "provider.connect.toast.connected.title": "{{provider}} повезан", + "provider.connect.toast.connected.description": "{{provider}} модела је сада доступно за коришћење.", + "provider.custom.title": "Прилагођени добављач", + "provider.custom.unavailable": "Прилагођени добављачи нису доступни на овом серверу", + "provider.custom.description.prefix": "Конфигуришите добављача који је компатибилан са OpenAI. Видите", + "provider.custom.description.link": "документи за конфигурацију добављача", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ИД добављача", + "provider.custom.field.providerID.placeholder": "мипровидер", + "provider.custom.field.providerID.description": "мала слова, бројеви, цртице или доње црте", + "provider.custom.field.name.label": "Име за приказ", + "provider.custom.field.name.placeholder": "Мој добављач вештачке интелигенције", + "provider.custom.field.baseURL.label": "Басе URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API кључ", + "provider.custom.field.apiKey.placeholder": "API кључ", + "provider.custom.field.apiKey.description": "Опционо. Оставите празно ако аутом управљате преко заглавља.", + "provider.custom.models.label": "Модели", + "provider.custom.models.id.label": "ИД", + "provider.custom.models.id.placeholder": "модел-ид", + "provider.custom.models.name.label": "Назив", + "provider.custom.models.name.placeholder": "Име за приказ", + "provider.custom.models.remove": "Уклони модел", + "provider.custom.models.add": "Додајте модел", + "provider.custom.headers.label": "заглавља (опционо)", + "provider.custom.headers.key.label": "Заглавље", + "provider.custom.headers.key.placeholder": "Назив заглавља", + "provider.custom.headers.value.label": "вредност", + "provider.custom.headers.value.placeholder": "вредност", + "provider.custom.headers.remove": "Уклоните заглавље", + "provider.custom.headers.add": "Додајте заглавље", + "provider.custom.error.providerID.required": "ИД добављача је обавезан", + "provider.custom.error.providerID.format": "Користите мала слова, бројеве, цртице или доње црте", + "provider.custom.error.providerID.exists": "Тај ИД добављача већ постоји", + "provider.custom.error.name.required": "потребно је име за приказ", + "provider.custom.error.baseURL.required": "Потребна је база URL", + "provider.custom.error.baseURL.format": "мора да почиње са http:// или https://", + "provider.custom.error.required": "Обавезно", + "provider.custom.error.duplicate": "Дупликат", + "provider.disconnect.toast.disconnected.title": "{{provider}} прекинут", + "provider.disconnect.toast.disconnected.description": "{{provider}} модели више нису доступни.", + "model.tag.free": "Бесплатно", + "model.tag.latest": "Најновије", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "текст", + "model.input.image": "слика", + "model.input.audio": "аудио", + "model.input.video": "видео", + "model.input.pdf": "пдф", + "model.tooltip.allows": "Дозвољава: {{inputs}}", + "model.tooltip.context.label": "Контекст", + "model.tooltip.inputs": "улази", + "model.tooltip.model": "Модел", + "model.tooltip.provider": "Добављач", + "model.tooltip.reasoning": "Реасонинг", + "model.tooltip.reasoning.allowed": "Омогућава резоновање", + "model.tooltip.reasoning.none": "Без образложења", + "model.tooltip.context": "ограничење контекста {{limit}}", + "common.search.placeholder": "Претрага", + "common.goBack": "Вратите се назад", + "common.goForward": "Крећите се напред", + "common.loading": "Учитавање", + "common.loading.ellipsis": "...", + "common.cancel": "Откажи", + "common.clear": "Јасно", + "common.open": "Отвори", + "common.connect": "Повежите се", + "common.disconnect": "Прекини везу", + "common.continue": "Настави", + "common.submit": "Пошаљи", + "common.save": "Сачувај", + "common.saving": "Чување...", + "common.default": "подразумевано", + "common.attachment": "прилог", + "prompt.placeholder.shell": "Унесите команду shell... {{example}}", + "prompt.placeholder.normal": 'Питајте било шта... "{{example}}"', + "prompt.placeholder.simple": "Питајте било шта...", + "prompt.placeholder.summarizeComments": "резимирајте коментаре…", + "prompt.placeholder.summarizeComment": "резимирајте коментар…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Промпт", + "prompt.mode.shell.exit": "есц за излаз", + "session.child.promptDisabled": "сесије субагента не могу да буду упитане.", + "session.child.backToParent": "Назад на главну сесију.", + "prompt.example.1": "Поправите ТОДО у бази кода", + "prompt.example.2": "Који је технолошки скуп овог пројекта?", + "prompt.example.3": "Поправите покварене тестове", + "prompt.example.4": "Објасните како функционише аутентификација", + "prompt.example.5": "Пронађите и поправите безбедносне пропусте", + "prompt.example.6": "Додајте јединичне тестове за кориснички сервис", + "prompt.example.7": "Рефакторирајте ову функцију да би била читљивија", + "prompt.example.8": "Шта ова грешка значи?", + "prompt.example.9": "Помозите ми да отклоним овај проблем", + "prompt.example.10": "Генеришите API документацију", + "prompt.example.11": "Оптимизујте упите за базу података", + "prompt.example.12": "Додајте потврду уноса", + "prompt.example.13": "Креирајте нову компоненту за...", + "prompt.example.14": "Како да применим овај пројекат?", + "prompt.example.15": "Прегледајте мој код за најбоље праксе", + "prompt.example.16": "Додајте руковање грешкама овој функцији", + "prompt.example.17": "Објасните овај образац регуларног израза", + "prompt.example.18": "Претворите ово у TypeScript", + "prompt.example.19": "Додајте евиденцију у целој бази кода", + "prompt.example.20": "Које зависности су застареле?", + "prompt.example.21": "Помозите ми да напишем скрипту за миграцију", + "prompt.example.22": "Имплементирајте кеширање за ову крајњу тачку", + "prompt.example.23": "Додајте пагинацију овој листи", + "prompt.example.24": "Направите команду CLI за...", + "prompt.example.25": "Како овде функционишу варијабле окружења?", + "prompt.popover.emptyResults": "Нема одговарајућих резултата", + "prompt.popover.emptyCommands": "Нема одговарајућих команди", + "prompt.dropzone.label": "Овде испустите слике, PDFс или текстуалне датотеке", + "prompt.dropzone.file.label": "Испустите у датотеку @ментион", + "prompt.slash.badge.custom": "прилагођено", + "prompt.slash.badge.skill": "вештина", + "prompt.slash.badge.mcp": "мцп", + "prompt.context.active": "активан", + "prompt.context.includeActiveFile": "Укључи активну датотеку", + "prompt.context.removeActiveFile": "Уклоните активну датотеку из контекста", + "prompt.context.removeFile": "Уклоните датотеку из контекста", + "prompt.action.attachFile": "Додајте датотеке", + "prompt.menu.addImagesAndFiles": "Додајте датотеке и још много тога", + "prompt.menu.imagesAndFiles": "слике и датотеке", + "prompt.menu.commands": "команде", + "prompt.menu.context": "Контекст", + "prompt.menu.shellCommand": "команда Shell", + "prompt.attachment.remove": "Уклоните прилог", + "prompt.action.send": "Пошаљи", + "prompt.action.stop": "Стоп", + "prompt.toast.pasteUnsupported.title": "Неподржани прилог", + "prompt.toast.pasteUnsupported.description": "Овде се могу приложити само слике, PDFс или текстуалне датотеке.", + "prompt.toast.attachmentDuplicate.title": "Ова датотека је већ отпремљена", + "prompt.toast.modelAgentRequired.title": "Изаберите агента и модел", + "prompt.toast.modelAgentRequired.description": "Одаберите агента и модел пре него што пошаљете упит.", + "prompt.toast.worktreeCreateFailed.title": "Креирање радног стабла није успело", + "prompt.toast.sessionCreateFailed.title": "Креирање сесије није успело", + "prompt.toast.shellSendFailed.title": "Слање команде shell није успело", + "prompt.toast.commandSendFailed.title": "Слање команде није успело", + "prompt.toast.promptSendFailed.title": "Слање упита није успело", + "prompt.toast.promptSendFailed.description": "Није могуће преузети сесију", + "dialog.mcp.title": "MCPс", + "dialog.mcp.description": "{{enabled}} од {{total}} омогућено", + "dialog.mcp.empty": "Нема конфигурисаних MCP", + "dialog.lsp.empty": "LSPс аутоматски детектован из типова датотека", + "dialog.plugins.empty": "Додаци конфигурисани у opencode.json", + "mcp.status.connected": "повезан", + "mcp.status.failed": "није успело", + "mcp.status.needs_auth": "потребан је аутх", + "mcp.status.disabled": "онемогућен", + "mcp.auth.clickToAuthenticate": "Кликните да бисте потврдили аутентичност", + "dialog.fork.empty": "Нема порука од којих треба да се рачвате", + "dialog.directory.search.placeholder": "Претражите фасцикле", + "dialog.directory.empty": "Нису пронађене фасцикле", + "dialog.directory.action.selectFile": "Изаберите датотеку", + "dialog.directory.action.selectFolder": "Изаберите фасциклу", + "dialog.directory.root": "Роот", + "dialog.directory.parent": "Родитељ", + "dialog.directory.readError": "Није могуће прочитати ову фасциклу", + "app.server.unreachable": "Није могуће доћи до {{server}}", + "app.server.retrying": "Аутоматски поновни покушај...", + "app.server.otherServers": "Други сервери", + "dialog.server.title": "Сервери", + "dialog.server.description": "Промените на који OpenCode сервер се ова апликација повезује.", + "dialog.server.search.placeholder": "сервери за претрагу", + "dialog.server.empty": "Још нема сервера", + "dialog.server.add.title": "Додај сервер", + "dialog.server.add.url": "Адреса сервера", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Повезивање са сервером није успело", + "dialog.server.add.checking": "Проверавам...", + "dialog.server.add.button": "Додај сервер", + "dialog.server.add.name": "Име сервера (опционо)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Корисничко име (опционално)", + "dialog.server.add.usernamePlaceholder": "корисничко име", + "dialog.server.add.password": "Лозинка (опционо)", + "dialog.server.add.passwordPlaceholder": "лозинка", + "dialog.server.edit.title": "Уреди сервер", + "dialog.server.default.title": "подразумевани сервер", + "dialog.server.default.description": + "Повежите се са овим сервером при покретању апликације уместо покретања локалног сервера. Захтева поновно покретање.", + "dialog.server.default.none": "Није изабран сервер", + "dialog.server.default.set": "Подесите тренутни сервер као подразумевани", + "dialog.server.default.clear": "Јасно", + "dialog.server.action.remove": "Уклони сервер", + "dialog.server.menu.edit": "Уреди", + "dialog.server.menu.default": "Подесите као подразумевано", + "dialog.server.menu.defaultRemove": "Уклони подразумевано", + "dialog.server.menu.delete": "Избриши", + "dialog.server.current": "Тренутни сервер", + "dialog.server.status.default": "подразумевано", + "wsl.server.add": "Додајте WSL сервер", + "wsl.server.addShort": "Додај WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL сервер", + "wsl.server.retryStart": "Поново покушајте", + "wsl.server.updating": "Ажурирање...", + "wsl.onboarding.step.distro": "Изаберите дистро", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Проверавам WSL...", + "wsl.onboarding.restartRequired": "Windows потребно је поновно покретање да би се завршила инсталација WSL.", + "wsl.onboarding.ready": "WSL је спреман.", + "wsl.onboarding.required": "WSL је потребан за наставак.", + "wsl.onboarding.checkingDistros": "Проверавам дистрибуције...", + "wsl.onboarding.installingDistro": "Инсталирање {{distro}}...", + "wsl.onboarding.checkingDistro": "Проверавам {{distro}}...", + "wsl.onboarding.listingDistros": "Списак дистро...", + "wsl.onboarding.distroReady": "{{distro}} је спреман.", + "wsl.onboarding.distroNotInstalled": "{{distro}} још увек није инсталиран.", + "wsl.onboarding.openDistroOnce": "Отворите {{distro}} једном да бисте завршили подешавање.", + "wsl.onboarding.finishingDistro": "Завршавање подешавања за {{distro}}.", + "wsl.onboarding.pickDistro": "Изаберите дистро или га инсталирајте испод.", + "wsl.onboarding.checkingOpencode": "Проверавам OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Проверавам OpenCode у {{distro}}...", + "wsl.onboarding.updatingOpencode": "Ажурирање OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Ажурирање OpenCode у {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Ажурирајте OpenCode у {{distro}}.", + "wsl.onboarding.updateOpencode": "ажурирање OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode је спреман за {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode је спреман.", + "wsl.onboarding.installOpencodeIn": "Инсталирајте OpenCode у {{distro}}.", + "wsl.onboarding.installOpencode": "Инсталирај OpenCode", + "wsl.onboarding.chooseDistroFirst": "Прво изаберите дистро.", + "wsl.onboarding.loadFailed": "Учитавање WSL стања није успело.", + "wsl.onboarding.loading": "Учитавање...", + "wsl.onboarding.installedDistros": "Инсталиране дистрибуције", + "wsl.onboarding.checkAgain": "Проверите поново", + "wsl.onboarding.distroStatus.ready": "Спреман", + "wsl.onboarding.distroStatus.checking": "Проверавам...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode није инсталиран", + "wsl.onboarding.distroStatus.missingTools": "недостаје bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Неподржано · Користите WSL 2", + "wsl.onboarding.needAnotherDistro": "Треба вам још један дистро?", + "wsl.onboarding.needAnotherDistroHint": "Инсталирајте Linux дистрибуцију из каталога WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL није инсталиран", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) је потребан да би OpenCode могао да дода WSL сервер", + "wsl.onboarding.wslUnavailable.title": "WSL недоступан", + "wsl.onboarding.wslUnavailable.description": "OpenCode није могао да потврди WSL на овој машини.", + "wsl.onboarding.installWsl": "Инсталирај WSL", + "wsl.onboarding.windowsRestartRequired": + "Поново покрените Windows да бисте завршили инсталирање WSL, а затим поново отворите OpenCode.", + "wsl.onboarding.next": "Следеће", + "wsl.onboarding.refresh": "Освежи", + "wsl.onboarding.allDistrosAdded": "Све инсталиране дистрибуције су већ додате.", + "wsl.onboarding.noDistros": "Још нису откривене дистрибуције.", + "wsl.onboarding.install": "Инсталирај", + "wsl.onboarding.installing": "Инсталирање...", + "wsl.onboarding.installDistro": "Инсталирајте дистро", + "wsl.onboarding.searchDistros": "Претражи дистрибуције", + "wsl.onboarding.wsl2Required": "WSL 2 је обавезан.", + "wsl.onboarding.toolsRequired": "Овој дистрибуцији су потребни bash и curl.", + "wsl.onboarding.openTerminal": "Отвори терминал", + "wsl.onboarding.path": "Путања: {{path}}", + "wsl.onboarding.notFound": "није пронађен", + "wsl.onboarding.version": "Верзија: {{version}}", + "wsl.onboarding.unknown": "непознат", + "wsl.onboarding.desktopVersion": "десктоп {{version}}", + "wsl.onboarding.versionMismatch": "Инсталирана верзија не одговара верзији апликације за рачунар.", + "wsl.onboarding.adding": "Додавање...", + "help.tabs.toast.ariaLabel": "Представљамо картице. Организујте свој рад и активне сесије помоћу картица", + "help.tabs.toast.dismiss": "Одбаци информације о картицама", + "help.tabs.title": "Представљамо картице", + "help.tabs.description": "Организујте свој рад и активне сесије помоћу картица", + "help.tabs.date": "14. јул", + "help.tabs.introduction": "OpenCode Desktop је сада направљен око картица.", + "help.tabs.sessions": + "Започните нову сесију на картици или отворите постојећу сесију из било ког пројекта. Отворите нову картицу када започињете нешто ново и затворите је када завршите.", + "help.tabs.organize": + "Одржавање неколико отворених картица олакшава организовање активних сесија. Преименујте картице у нешто незаборавно ако планирате да их задржите.", + "help.tabs.home": + "Наћи ћете све своје сесије и пројекте на новом почетном екрану. Избор сесије отвара је на картици.", + "help.tabs.persistence": "Када поново отворите апликацију, ваше картице су и даље отворене.", + "help.tabs.worktrees": + "Нови дизајн још увек не подржава Git Ворктреес, ускоро долази. Дакле, ако желите да наставите да користите претходни изглед, можете да прелазите између изгледа у подешавањима. Само имајте на уму да ће нови изглед постати трајан за неколико недеља.", + "server.row.noUsername": "нема корисничког имена", + "dialog.project.edit.title": "Уреди пројекат", + "dialog.project.edit.name": "Назив", + "dialog.project.edit.icon": "Икона", + "dialog.project.edit.icon.alt": "икона пројекта", + "dialog.project.edit.icon.hint": "Кликните или превуците слику", + "dialog.project.edit.icon.recommended": "Препоручено: 128к128пк", + "dialog.project.edit.color": "боја", + "dialog.project.edit.color.select": "Изаберите {{color}} боју", + "dialog.project.edit.worktree.startup": "скрипта за покретање радног простора", + "dialog.project.edit.worktree.startup.description": + "Покреће се након креирања новог радног простора (радног стабла).", + "dialog.project.edit.worktree.startup.placeholder": "нпр. bun install", + "dialog.releaseNotes.action.getStarted": "Започните", + "dialog.releaseNotes.action.next": "Следеће", + "dialog.releaseNotes.action.hideFuture": "Немојте их приказивати у будућности", + "dialog.releaseNotes.media.alt": "Преглед издања", + "context.breakdown.title": "Анализа контекста", + "context.breakdown.note": "Приближна анализа улазних токена. „Остало“ укључује дефиниције алата и режијске трошкове.", + "context.breakdown.system": "Систем", + "context.breakdown.user": "Корисник", + "context.breakdown.assistant": "Помоћник", + "context.breakdown.tool": "Позиви алатки", + "context.breakdown.other": "Друго", + "context.systemPrompt.title": "Системски упит", + "context.rawMessages.title": "необрађене поруке", + "context.export.session": "Извоз сесија", + "context.stats.session": "Сесија", + "context.stats.messages": "Поруке", + "context.stats.provider": "Добављач", + "context.stats.model": "Модел", + "context.stats.limit": "Ограничење контекста", + "context.stats.totalTokens": "Укупно токена", + "context.stats.usage": "Употреба", + "context.stats.inputTokens": "Токени за унос", + "context.stats.outputTokens": "Излазни токени", + "context.stats.reasoningTokens": "Реасонинг Токенс", + "context.stats.cacheTokens": "Кеш токени (читање/писање)", + "context.stats.userMessages": "корисничке поруке", + "context.stats.assistantMessages": "Поруке помоћника", + "context.stats.totalCost": "укупни трошкови", + "context.stats.sessionCreated": "сесија креирана", + "context.stats.lastActivity": "Последња активност", + "context.usage.tokens": "Токени", + "context.usage.usage": "Употреба", + "context.usage.cost": "Цена", + "context.usage.clickToView": "Кликните да бисте видели контекст", + "context.usage.view": "Погледајте употребу контекста", + "language.en": "енглески", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Деутсцх", + "language.es": "Еспанол", + "language.fr": "Францаис", + "language.da": "Данск", + "language.ja": "日本語", + "language.pl": "Полски", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Норск", + "language.br": "португуес (Бразил)", + "language.bs": "Босански", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Туркце", + "toast.language.title": "Језик", + "toast.language.description": "пребачен на {{language}}", + "toast.theme.title": "Тема је промењена", + "toast.scheme.title": "Шема боја", + "toast.workspace.enabled.title": "Радни простори су омогућени", + "toast.workspace.enabled.description": "Више стабала рада је сада приказано на бочној траци", + "toast.workspace.disabled.title": "Радни простори су онемогућени", + "toast.workspace.disabled.description": "На бочној траци је приказано само главно стабло рада", + "toast.permissions.autoaccept.on.title": "Аутоматско прихватање дозвола", + "toast.permissions.autoaccept.on.description": "Захтеви за дозволу ће бити аутоматски одобрени", + "toast.permissions.autoaccept.off.title": "Заустављено аутоматско прихватање дозвола", + "toast.permissions.autoaccept.off.description": "Захтеви за дозволу ће захтевати одобрење", + "toast.model.none.title": "Није изабран ниједан модел", + "toast.model.none.description": "Повежите провајдера да бисте резимирали ову сесију", + "toast.file.loadFailed.title": "Учитавање датотеке није успело", + "toast.file.listFailed.title": "Неуспешно навођење датотека", + "toast.context.noLineSelection.title": "Нема избора линија", + "toast.context.noLineSelection.description": "Прво изаберите опсег линија на картици датотеке.", + "toast.session.share.copyFailed.title": "Копирање URL у међуспремник није успело", + "toast.session.share.success.title": "Сесија је дељена", + "toast.session.share.success.description": "Дељење URL копирано у међуспремник!", + "toast.session.share.failed.title": "Дељење сесије није успело", + "toast.session.share.failed.description": "Дошло је до грешке приликом дељења сесије", + "toast.session.unshare.success.title": "сесија није дељена", + "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": "доступно је ажурирање", + "toast.update.description": "Нова верзија OpenCode ({{version}}) је сада доступна за инсталацију.", + "toast.update.action.installRestart": "Инсталирајте и поново покрените", + "toast.update.action.notYet": "Не још", + "error.page.title": "Нешто је пошло наопако", + "error.page.description": "Дошло је до грешке при учитавању апликације.", + "error.page.description.localServerStartup": "Дошло је до грешке при покретању локалног сервера.", + "error.page.details.label": "Детаљи о грешци", + "error.page.action.restart": "Поново покрените", + "error.page.action.report": "Извештај о грешци", + "error.page.action.reported": "пријављена грешка", + "error.page.action.exportLogs": "Извоз евиденције", + "error.page.action.checking": "Проверавам...", + "error.page.action.checkUpdates": "Проверите да ли постоје ажурирања", + "error.page.action.updateTo": "ажурирање на {{version}}", + "error.page.circular": "[Цирцулар]", + "error.page.report.prefix": "пријавите ову грешку OpenCode тиму", + "error.page.report.discord": "дана Discord", + "error.page.version": "Верзија: {{version}}", + "error.dev.rootNotFound": + "Основни елемент није пронађен. Да ли сте заборавили да га додате у свој index.html? Или је можда атрибут ид погрешно написан?", + "error.serverSync.connectFailed": "Повезивање са сервером није успело. Да ли постоји сервер који ради на `{{url}}`?", + "error.serverSDK.noServerAvailable": "Није доступан сервер", + "error.serverSDK.serverNotAvailable": "Сервер није доступан", + "error.childStore.persistedCacheCreateFailed": "Креирање трајног кеша није успело", + "error.childStore.persistedProjectMetadataCreateFailed": "Креирање трајних метаподатака пројекта није успело", + "error.childStore.persistedProjectIconCreateFailed": "Креирање иконе трајног пројекта није успело", + "error.childStore.storeCreateFailed": "Креирање продавнице није успело", + "directory.error.invalidUrl": "Неважећи директоријум у URL.", + "error.chain.unknown": "непозната грешка", + "error.server.invalidConfiguration": "Неважећа конфигурација", + "error.chain.causedBy": "Узрок:", + "error.chain.apiError": "API грешка", + "error.chain.status": "Статус: {{status}}", + "error.chain.retryable": "Поновни покушај: {{retryable}}", + "error.chain.responseBody": "Тело одговора:\n{{body}}", + "error.chain.didYouMean": "Да ли сте мислили: {{suggestions}}", + "error.chain.modelNotFound": "Модел није пронађен: {{provider}}/{{model}}", + "error.chain.checkConfig": "Проверите имена добављача/модела конфигурације (opencode.json).", + "error.chain.mcpFailed": + "MCP сервер „{{name}}“ није успео. Напомена, OpenCode још увек не подржава MCP аутентификацију.", + "error.chain.providerAuthFailed": "аутентификација добављача није успела ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + "Иницијализација добављача „{{provider}}“ није успела. Проверите акредитиве и конфигурацију.", + "error.chain.configJsonInvalid": "Конфигурациона датотека на {{path}} није важећа JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Конфигурациона датотека на {{path}} није важећа JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + "Директоријум „{{dir}}“ у {{path}} није важећи. Преименујте директоријум у „{{suggestion}}“ или га уклоните. Ово је уобичајена грешка у куцању.", + "error.chain.configFrontmatterError": "рашчлањивање frontmatter у {{path}} није успело:\n{{message}}", + "error.chain.configInvalid": "Конфигурациони фајл на {{path}} је неважећи", + "error.chain.configInvalidWithMessage": "Конфигурациони фајл на {{path}} је неважећи: {{message}}", + "notification.permission.title": "потребна је дозвола", + "notification.permission.description": "{{sessionTitle}} у {{projectName}} потребна је дозвола", + "notification.question.title": "Питање", + "notification.question.description": "{{sessionTitle}} у {{projectName}} има питање", + "notification.action.goToSession": "Иди на сесију", + "notification.session.responseReady.title": "Одговор спреман", + "notification.session.error.title": "Грешка сесије", + "notification.session.error.fallbackDescription": "Дошло је до грешке", + "home.recentProjects": "Недавни пројекти", + "home.empty.title": "Нема недавних пројеката", + "home.empty.description": "Започните отварањем локалног пројекта", + "home.title": "Почетна", + "home.projects": "Пројекти", + "home.project.add": "Додај пројекат", + "home.recentlyClosed": "Недавно затворено", + "home.server.collapse": "Скупи серверске пројекте", + "home.server.expand": "Проширите пројекте сервера", + "home.sessions.search.placeholder": "Сесије претраге", + "home.sessions.search.placeholder.scoped": "Сесије претраге у {{scope}}", + "home.sessions.search.sessions": "Сесије", + "home.sessions.search.noResults": "Нису пронађене сесије за {{query}}", + "home.sessions.empty": "Овде још ништа", + "home.sessions.empty.description": "Креирајте сесију да бисте започели", + "home.sessions.group.today": "Данас", + "home.sessions.group.yesterday": "јуче", + "home.sessions.group.older": "старији", + "home.providerTip": + "Повежите се са 75+ провајдера да бисте користили друге моделе, укључујући Claude, GPT, Gemini итд", + "session.tab.session": "Сесија", + "session.tab.review": "Преглед", + "session.tab.context": "Контекст", + "session.tab.unknown": "Непозната сесија", + "session.panel.reviewAndFiles": "Преглед и датотеке", + "session.error.notFound": "Ова сесија није пронађена", + "session.error.notFound.description": "Ова картица указује на сесију која више не постоји на овом серверу.", + "session.error.notFound.closeTab": "Затвори картицу", + "session.error.serverConnection": "Не могу да се повежем са овим сервером", + "session.review.filesChanged": "Промењене датотеке {{count}}", + "session.review.change.one": "Промена", + "session.review.change.other": "Промене", + "session.review.loadingChanges": "Учитавање промена...", + "session.review.empty": "Још нема промена у овој сесији", + "session.review.noVcs": "Није откривен Git Систем контроле верзија, промене нису приказане", + "session.review.noVcs.createGit.title": "Направите Git спремиште", + "session.review.noVcs.createGit.description": "Пратите, прегледајте и опозовите промене у овом пројекту", + "session.review.noVcs.createGit.actionLoading": "Креирање Git спремишта...", + "session.review.noVcs.createGit.action": "Креирајте Git спремиште", + "session.review.noSnapshot": "Праћење снимака је онемогућено у конфигурацији, тако да промене сесије нису доступне", + "session.review.noChanges": "Нема промена", + "session.review.noUncommittedChanges": "Још увек нема неизвршених промена", + "session.review.noBranchChanges": "Још нема промена гране", + "session.files.selectToOpen": "Изаберите датотеку за отварање", + "session.files.all": "Све датотеке", + "session.files.empty": "Нема датотека", + "session.files.binaryContent": "Бинарна датотека (садржај се не може приказати)", + "session.messages.renderEarlier": "Рендерујте раније поруке", + "session.messages.loadingEarlier": "Учитавање ранијих порука...", + "session.messages.loadEarlier": "Учитајте раније поруке", + "session.messages.loading": "Учитавање порука...", + "session.messages.jumpToLatest": "Скочи на најновије", + "session.context.addToContext": "Додајте {{selection}} у контекст", + "session.todo.title": "Тодос", + "session.todo.collapse": "Скупи", + "session.todo.expand": "Прошири", + "session.todo.progress": "{{done}} од {{total}} задатака је завршено", + "session.question.progress": "{{current}} од {{total}} питања", + "session.question.minimize": "Смањите питање", + "session.question.restore": "Врати питање", + "session.question.pending.one": "{{count}} питање на чекању", + "session.question.pending.few": "{{count}} питања на чекању", + "session.question.pending.other": "{{count}} питања на чекању", + "session.followupDock.summary.one": "{{count}} порука у реду чекања", + "session.followupDock.summary.few": "{{count}} поруке у реду чекања", + "session.followupDock.summary.other": "{{count}} порука у реду чекања", + "session.followupDock.sendNow": "Пошаљи одмах", + "session.followupDock.edit": "Уреди", + "session.followupDock.collapse": "Скупи поруке у реду чекања", + "session.followupDock.expand": "Проширите поруке у реду чекања", + "session.revertDock.summary.one": "{{count}} враћена порука", + "session.revertDock.summary.few": "{{count}} враћене поруке", + "session.revertDock.summary.other": "{{count}} враћених порука", + "session.revertDock.collapse": "Скупи враћене поруке", + "session.revertDock.expand": "Проширите враћене поруке", + "session.revertDock.restore": "Врати поруку", + "session.new.title": "Направите било шта", + "session.new.project.new": "Нови пројекат", + "session.new.project.search": "Претрага пројеката", + "session.new.project.add": "Додај пројекат", + "session.new.worktree.main": "Главна грана", + "session.new.worktree.mainWithBranch": "Главна грана ({{branch}})", + "session.new.worktree.create": "Креирајте ново стабло рада", + "session.new.workspace.runIn": "Покрените сесију у", + "session.new.workspace.triggerLocal": "локално", + "session.new.workspace.local": "Локално спремиште", + "session.new.workspace.existing": "Радни простор…", + "session.new.git.none": "Не Git", + "session.new.lastModified": "Последња измена", + "session.header.search.placeholder": "Претрага {{project}}", + "session.header.searchFiles": "Претрага датотека", + "session.header.openIn": "Отвори у", + "session.header.open.action": "Отвори {{app}}", + "session.header.open.ariaLabel": "Отвори у {{app}}", + "session.header.open.menu": "Отворите опције", + "session.header.open.copyPath": "Копирај путању", + "session.header.open.finder": "Финдер", + "session.header.open.fileExplorer": "Истраживач датотека", + "session.header.open.fileManager": "Управљач датотекама", + "session.header.reveal.finder": "Откриј у Финдер-у", + "session.header.reveal.fileExplorer": "Откриј у File Explorer", + "session.header.reveal.containingFolder": "Отворите фасциклу која садржи", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Терминал", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Статус", + "status.popover.ariaLabel": "Конфигурације сервера", + "status.popover.tab.servers": "Сервери", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Додаци", + "status.popover.action.manageServers": "Управљајте серверима", + "session.share.popover.title": "Објавите на вебу", + "session.share.popover.description.shared": "Ова сесија је јавна на вебу. Доступан је свима који имају везу.", + "session.share.popover.description.unshared": "Јавно делите сесију на вебу. Биће доступан свима који имају везу.", + "session.share.action.share": "Дели", + "session.share.action.publish": "Објави", + "session.share.action.publishing": "Објављивање...", + "session.share.action.unpublish": "Опозови објављивање", + "session.share.action.unpublishing": "Опозив објављивања...", + "session.share.action.view": "Приказ", + "session.share.copy.copied": "Копирано", + "session.share.copy.copyLink": "Копирај везу", + "lsp.tooltip.none": "Нема LSP сервера", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Учитавање упита...", + "terminal.loading": "Учитавање терминала...", + "terminal.title": "Терминал", + "terminal.title.numbered": "Терминал {{number}}", + "terminal.close": "Затвори терминал", + "terminal.connectionLost.title": "Веза је изгубљена", + "terminal.connectionLost.abnormalClose": "WebSocket затворен ненормално: {{code}}", + "terminal.connectionLost.description": + "Терминална веза је прекинута. Ово се може десити када се сервер поново покрене.", + "terminal.connectTicket.csrfError": + "PTY улазница за повезивање одбијена од стране порекла или CSRF провера. Проверите конфигурацију сервера CORS.", + "terminal.connectTicket.statusError": "PTY улазница за повезивање није успела са {{status}}", + "titlebar.update": "ажурирање", + "titlebar.updateVersion": "ажурирање {{version}}", + "common.closeTab": "Затвори картицу", + "common.dismiss": "Одбаци", + "common.moreCountSuffix": "(+{{count}} више)", + "common.requestFailed": "Захтев није успео", + "common.moreOptions": "Више опција", + "common.learnMore": "Сазнајте више", + "common.rename": "Преименуј", + "common.export": "Извоз", + "common.reset": "Ресет", + "common.archive": "Архива", + "common.delete": "Избриши", + "common.close": "Затвори", + "common.edit": "Уреди", + "common.loadMore": "Учитајте више", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "непознат", + "common.time.justNow": "Управо сада", + "common.time.minutesAgo.short": "пре {{count}}м", + "common.time.hoursAgo.short": "пре {{count}}х", + "common.time.daysAgo.short": "пре {{count}}д", + "sidebar.menu.toggle": "Пребаци мени", + "sidebar.nav.projectsAndSessions": "Пројекти и сесије", + "sidebar.settings": "Подешавања", + "sidebar.help": "Помоћ", + "sidebar.workspaces.enable": "Омогућите радне просторе", + "sidebar.workspaces.disable": "Онемогућите радне просторе", + "sidebar.gettingStarted.title": "Почетак", + "sidebar.gettingStarted.line1": "OpenCode укључује бесплатне моделе тако да можете одмах да почнете.", + "sidebar.gettingStarted.line2": + "Повежите било ког провајдера да бисте користили моделе, вк. Claude, GPT, Gemini итд.", + "sidebar.project.recentSessions": "Недавне сесије", + "sidebar.project.viewAllSessions": "Погледајте све сесије", + "sidebar.project.clearNotifications": "Обришите обавештења", + "sidebar.empty.title": "Нема отворених пројеката", + "sidebar.empty.description": "Отворите пројекат да бисте започели", + "debugBar.ariaLabel": "Дијагностика перформанси развоја", + "debugBar.na": "н/а", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Последњи довршени прелаз руте додирује страницу сесије, мерено од почетка рутера до прве боје након што се слегне.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Померање кадрова у секунди у последњих 5 секунди.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Најгоре време кадра у последњих 5 секунди.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "кадрови преко 32 мс у последњих 5 секунди.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Блокирано време и број дугих задатака у последњих 5 секунди. Максимални задатак: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Најгоре примећено кашњење уноса у последњих 5 секунди.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Приближно трајање интеракције у последњих 5 секунди. Ово је као INP, а не званични Веб Виталс INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Кумулативна промена изгледа за тренутни животни век апликације.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Коришћена ЈС хрпа наспрам ограничења гомиле. Само Chromium.", + "debugBar.mem.tip": "Коришћена ЈС хрпа наспрам ограничења гомиле. {{used}} од {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Форсирајте стилове фокуса на све интерактивне елементе", + "debugBar.focus.on": "УКЉУЧЕНО", + "debugBar.focus.off": "ИСКЉУЧЕНО", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Форсирајте пун смер распореда апликације без промене изабраног језика", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Десктоп", + "settings.section.server": "Сервер", + "settings.tab.general": "Опште", + "settings.tab.shortcuts": "Пречице", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL интеграција", + "settings.desktop.wsl.description": "Покрените OpenCode сервер унутар WSL на Windows.", + "settings.general.section.appearance": "Изглед", + "settings.general.section.advanced": "Напредно", + "settings.general.section.notifications": "Системска обавештења", + "settings.general.section.updates": "Ажурирања", + "settings.general.section.sounds": "Звучни ефекти", + "settings.general.section.feed": "Феед", + "settings.general.section.display": "Приказ", + "settings.general.row.language.title": "Језик", + "settings.general.row.language.description": "Промените језик приказа за OpenCode", + "settings.general.row.shell.title": "Терминал shell", + "settings.general.row.shell.description": "Shell који користе алати терминала и агента", + "settings.general.row.shell.autoDefault": "аутоматски (подразумевано)", + "settings.general.row.shell.terminalOnly": "само терминал", + "settings.general.row.appearance.title": "Изглед", + "settings.general.row.appearance.description": "Прилагодите како OpenCode изгледа на вашем уређају", + "settings.general.row.colorScheme.title": "Шема боја", + "settings.general.row.colorScheme.description": "Одаберите да ли OpenCode прати системску, светлу или тамну тему", + "settings.general.row.theme.title": "Тема", + "settings.general.row.theme.description": "Прилагодите како је OpenCode тематски.", + "settings.general.row.font.title": "Фонт кода", + "settings.general.row.font.description": "Прилагодите фонт који се користи у блоковима кода", + "settings.general.row.terminalFont.title": "Фонт терминала", + "settings.general.row.terminalFont.description": "Прилагодите фонт који се користи у терминалу", + "settings.general.row.uiFont.title": "фонт корисничког интерфејса", + "settings.general.row.uiFont.description": "Прилагодите фонт који се користи у интерфејсу", + "settings.general.row.followup.title": "Праћење понашања", + "settings.general.row.followup.description": "Одаберите да ли ће се пратећи упити усмеравати одмах или чекати у реду", + "settings.general.row.followup.option.queue": "Ред", + "settings.general.row.followup.option.steer": "Стеер", + "settings.general.row.showFileTree.title": "стабло датотека", + "settings.general.row.showFileTree.description": "Прикажи панел стабла датотека у сесијама", + "settings.general.row.showNavigation.title": "Контроле навигације", + "settings.general.row.showNavigation.description": + "Прикажите дугмад за назад и напред у насловној траци радне површине", + "settings.general.row.showSearch.title": "Командна палета", + "settings.general.row.showSearch.description": "Прикажите дугме за претрагу и палету команди у насловној траци", + "settings.general.row.showTerminal.title": "Терминал", + "settings.general.row.showTerminal.description": "Прикажите дугме терминала у насловној траци радне површине", + "settings.general.row.showStatus.title": "Статус сервера", + "settings.general.row.showStatus.description": "Прикажите дугме статуса сервера у насловној траци", + "settings.general.row.mobileTitlebarBottom.title": "Доња навигација", + "settings.general.row.mobileTitlebarBottom.description": + "Поставите насловну траку и картице сесије на дно екрана на мобилном телефону", + "settings.general.row.showCustomAgents.title": "Прикажи агента", + "settings.general.row.showCustomAgents.description": + "Пребацивање између агената у композитору. Када је скривено, подразумевано је Буилд агент.", + "settings.general.row.reasoningSummaries.title": "Прикажи резимее образложења", + "settings.general.row.reasoningSummaries.description": "Приказ резимеа образложења модела на временској линији", + "settings.general.row.shellToolPartsExpanded.title": "Проширите shell делова алата", + "settings.general.row.shellToolPartsExpanded.description": + "Прикажи shell делове алата проширене подразумевано на временској линији", + "settings.general.row.editToolPartsExpanded.title": "Проширите делове алата за уређивање", + "settings.general.row.editToolPartsExpanded.description": + "Прикажите делове алата за уређивање, писање и закрпе проширене подразумевано на временској линији", + "settings.general.row.newInterface.title": "Нови изглед", + "settings.general.row.newInterface.badge": "Ново", + "settings.general.row.newInterface.description": + "Користите нове картице и изглед куће. Пребацивање између распореда ограничено време.", + "settings.general.row.newInterfaceNotice.title": "Сада користите нови изглед", + "settings.general.row.newInterfaceNotice.description": "Претходни изглед више није доступан", + "settings.general.row.newInterfaceNotice.dismiss": "Одбаци", + "settings.general.row.pinchZoom.title": "Пинцх за зумирање", + "settings.general.row.pinchZoom.description": + "Дозволите зумирање прстима додиром додирне табле и покретима Цтрл и скроловањем", + "settings.general.row.wayland.title": "Користи изворни Wayland", + "settings.general.row.wayland.description": "Онемогући X11 резервни на Wayland. Захтева поновно покретање.", + "settings.general.row.wayland.tooltip": + "На Linux са мешовитим мониторима брзине освежавања, изворни Wayland може бити стабилнији.", + "settings.general.row.releaseNotes.title": "белешке о издању", + "settings.general.row.releaseNotes.description": "Прикажи шта је ново након ажурирања", + "settings.updates.row.startup.title": "Проверите да ли има ажурирања при покретању", + "settings.updates.row.startup.description": "Аутоматски провери да ли постоје ажурирања када се OpenCode покрене", + "settings.updates.row.check.title": "Проверите да ли постоје ажурирања", + "settings.updates.row.check.description": "Ручно проверите да ли постоје ажурирања и инсталирајте ако су доступна", + "settings.updates.action.checkNow": "Проверите сада", + "settings.updates.action.checking": "Проверавам...", + "settings.updates.action.downloading": "Преузимање...", + "settings.updates.action.installing": "Инсталирање...", + "settings.updates.toast.latest.title": "Ажурни сте", + "settings.updates.toast.latest.description": "Користите најновију верзију OpenCode.", + "sound.option.none": "Нема", + "sound.option.alert01": "Упозорење 01", + "sound.option.alert02": "Упозорење 02", + "sound.option.alert03": "Упозорење 03", + "sound.option.alert04": "Упозорење 04", + "sound.option.alert05": "Упозорење 05", + "sound.option.alert06": "Упозорење 06", + "sound.option.alert07": "Упозорење 07", + "sound.option.alert08": "Упозорење 08", + "sound.option.alert09": "Упозорење 09", + "sound.option.alert10": "Упозорење 10", + "sound.option.bipbop01": "Бип-боп 01", + "sound.option.bipbop02": "Бип-боп 02", + "sound.option.bipbop03": "Бип-боп 03", + "sound.option.bipbop04": "Бип-боп 04", + "sound.option.bipbop05": "Бип-боп 05", + "sound.option.bipbop06": "Бип-боп 06", + "sound.option.bipbop07": "Бип-боп 07", + "sound.option.bipbop08": "Бип-боп 08", + "sound.option.bipbop09": "Бип-боп 09", + "sound.option.bipbop10": "Бип-боп 10", + "sound.option.staplebops01": "Стаплебопс 01", + "sound.option.staplebops02": "Стаплебопс 02", + "sound.option.staplebops03": "Стаплебопс 03", + "sound.option.staplebops04": "Стаплебопс 04", + "sound.option.staplebops05": "Стаплебопс 05", + "sound.option.staplebops06": "Стаплебопс 06", + "sound.option.staplebops07": "Стаплебопс 07", + "sound.option.nope01": "Не 01", + "sound.option.nope02": "Не 02", + "sound.option.nope03": "Не 03", + "sound.option.nope04": "Не 04", + "sound.option.nope05": "Не 05", + "sound.option.nope06": "Не 06", + "sound.option.nope07": "Не 07", + "sound.option.nope08": "Не 08", + "sound.option.nope09": "Не 09", + "sound.option.nope10": "Не 10", + "sound.option.nope11": "Не 11", + "sound.option.nope12": "Не 12", + "sound.option.yup01": "Да 01", + "sound.option.yup02": "Да 02", + "sound.option.yup03": "Да 03", + "sound.option.yup04": "Да 04", + "sound.option.yup05": "Да 05", + "sound.option.yup06": "Да 06", + "settings.general.notifications.agent.title": "Агент", + "settings.general.notifications.agent.description": + "Прикажи системско обавештење када је агент готов или му је потребна пажња", + "settings.general.notifications.permissions.title": "дозволе", + "settings.general.notifications.permissions.description": "Прикажи системско обавештење када је потребна дозвола", + "settings.general.notifications.errors.title": "Грешке", + "settings.general.notifications.errors.description": "Прикажи системско обавештење када дође до грешке", + "settings.general.sounds.agent.title": "Агент", + "settings.general.sounds.agent.description": "Пусти звук када је агент завршен или му је потребна пажња", + "settings.general.sounds.permissions.title": "дозволе", + "settings.general.sounds.permissions.description": "Пусти звук када је потребна дозвола", + "settings.general.sounds.errors.title": "Грешке", + "settings.general.sounds.errors.description": "Пусти звук када дође до грешке", + "settings.shortcuts.title": "Пречице на тастатури", + "settings.shortcuts.reset.button": "Ресетујте на подразумеване вредности", + "settings.shortcuts.reset.toast.title": "Ресетовање пречица", + "settings.shortcuts.reset.toast.description": "Пречице на тастатури су враћене на подразумеване вредности.", + "settings.shortcuts.conflict.title": "Пречица се већ користи", + "settings.shortcuts.conflict.description": "{{keybind}} је већ додељен {{titles}}.", + "settings.shortcuts.unassigned": "није додељен", + "settings.shortcuts.pressKeys": "Притисните тастере", + "settings.shortcuts.search.placeholder": "Пречице за претрагу", + "settings.shortcuts.search.empty": "Нису пронађене пречице", + "settings.shortcuts.group.general": "Опште", + "settings.shortcuts.group.session": "Сесија", + "settings.shortcuts.group.navigation": "Навигација", + "settings.shortcuts.group.modelAndAgent": "Модел и агент", + "settings.shortcuts.group.terminal": "Терминал", + "settings.shortcuts.group.prompt": "Промпт", + "settings.providers.title": "Добављачи", + "settings.providers.description": "Овде ће се моћи конфигурисати подешавања добављача.", + "settings.providers.section.connected": "Повезани провајдери", + "settings.providers.connected.empty": "Нема повезаних добављача", + "settings.providers.connected.environmentDescription": "Повезано из променљивих окружења", + "settings.providers.section.popular": "Популарни провајдери", + "settings.providers.custom.description": "Додајте добављача компатибилног са OpenAI по бази URL.", + "settings.providers.tag.environment": "окружење", + "settings.providers.tag.config": "Конфигурација", + "settings.providers.tag.custom": "Прилагођено", + "settings.providers.tag.other": "Друго", + "settings.models.title": "Модели", + "settings.models.description": "Овде ће се моћи конфигурисати подешавања модела.", + "settings.agents.title": "Агенти", + "settings.agents.description": "Овде ће се моћи конфигурисати подешавања агента.", + "settings.commands.title": "команде", + "settings.commands.description": "Овде ће се моћи конфигурисати подешавања команде.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP подешавања ће се моћи конфигурисати овде.", + "settings.permissions.title": "дозволе", + "settings.permissions.description": "Контролишите које алатке сервер може да користи подразумевано.", + "settings.permissions.section.tools": "Алати", + "settings.permissions.toast.updateFailed.title": "Ажурирање дозвола није успело", + "settings.permissions.action.allow": "Дозволи", + "settings.permissions.action.ask": "Питајте", + "settings.permissions.action.deny": "Дени", + "settings.permissions.tool.read.title": "Прочитај", + "settings.permissions.tool.read.description": "Читање датотеке (одговара путањи датотеке)", + "settings.permissions.tool.edit.title": "Уреди", + "settings.permissions.tool.edit.description": "Измените датотеке, укључујући измене, писање и закрпе", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Упарите датотеке помоћу глоб образаца", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Претражујте садржај датотеке користећи регуларне изразе", + "settings.permissions.tool.list.title": "Листа", + "settings.permissions.tool.list.description": "Листа датотека унутар директоријума", + "settings.permissions.tool.bash.title": "Басх", + "settings.permissions.tool.bash.description": "Покрените shell команде", + "settings.permissions.tool.task.title": "Задатак", + "settings.permissions.tool.task.description": "Покрените под-агенте", + "settings.permissions.tool.skill.title": "Вештина", + "settings.permissions.tool.skill.description": "Учитајте вештину по имену", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Покрените упите сервера језика", + "settings.permissions.tool.todowrite.title": "Тодо Врите", + "settings.permissions.tool.todowrite.description": "Ажурирајте листу обавеза", + "settings.permissions.tool.webfetch.title": "Веб Фетцх", + "settings.permissions.tool.webfetch.description": "Преузмите садржај са URL", + "settings.permissions.tool.websearch.title": "Веб претрага", + "settings.permissions.tool.websearch.description": "Претражујте веб", + "settings.permissions.tool.external_directory.title": "Спољни директоријум", + "settings.permissions.tool.external_directory.description": "Приступ датотекама изван директоријума пројекта", + "settings.permissions.tool.doom_loop.title": "Доом Лооп", + "settings.permissions.tool.doom_loop.description": "Откријте поновљене позиве алата са идентичним уносом", + "session.delete.failed.title": "Брисање сесије није успело", + "session.delete.title": "Избриши сесију", + "session.delete.confirm": "Избрисати сесију „{{name}}“?", + "session.delete.button": "Избриши сесију", + "workspace.new": "Нови радни простор", + "workspace.type.local": "локални", + "workspace.type.sandbox": "сандбок", + "workspace.create.failed.title": "није успело креирање радног простора", + "workspace.delete.failed.title": "Брисање радног простора није успело", + "workspace.resetting.title": "Ресетовање радног простора", + "workspace.resetting.description": "Ово може потрајати минут.", + "workspace.reset.failed.title": "Ресетовање радног простора није успело", + "workspace.reset.success.title": "Ресетовање радног простора", + "workspace.reset.success.description": "Радни простор сада одговара подразумеваној грани.", + "workspace.error.stillPreparing": "Радни простор се још увек припрема", + "workspace.status.checking": "Провера необједињених промена...", + "workspace.status.error": "Није могуће проверити статус гит-а.", + "workspace.status.clean": "Нису откривене необјављене промене.", + "workspace.status.dirty": "Необједињене промене су откривене у овом радном простору.", + "workspace.delete.title": "Избришите радни простор", + "workspace.delete.confirm": "Избрисати радни простор „{{name}}“?", + "workspace.delete.button": "Избришите радни простор", + "workspace.reset.title": "Ресетујте радни простор", + "workspace.reset.confirm": "Ресетовати радни простор „{{name}}“?", + "workspace.reset.button": "Ресетујте радни простор", + "workspace.reset.archived.none": "Ниједна активна сесија неће бити архивирана.", + "workspace.reset.archived.one": "1 сесија ће бити архивирана.", + "workspace.reset.archived.many": "{{count}} сесије ће бити архивиране.", + "workspace.reset.note": "Ово ће ресетовати радни простор тако да одговара подразумеваној грани.", + "dialog.usageExceeded.dontShowAgain": "Не приказуј поново", +} diff --git a/packages/app/src/i18n/sv.ts b/packages/app/src/i18n/sv.ts new file mode 100644 index 0000000000000000000000000000000000000000..39b363c6b65776209cd5bc62c7624dff7951b863 --- /dev/null +++ b/packages/app/src/i18n/sv.ts @@ -0,0 +1,1160 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Arkiv", + "desktop.menu.edit": "Redigera", + "desktop.menu.view": "Visa", + "desktop.menu.go": "Gå", + "desktop.menu.window": "Fönster", + "desktop.menu.help": "Hjälp", + "desktop.menu.checkForUpdates": "Sök efter uppdateringar...", + "desktop.menu.settings": "Inställningar", + "desktop.menu.reloadWebview": "Läs in webbvyn igen", + "desktop.menu.restart": "Starta om", + "desktop.menu.exportLogs": "Exportera loggar...", + "desktop.menu.newSession": "Ny session", + "desktop.menu.openProject": "Öppna projekt...", + "desktop.menu.newWindow": "Nytt fönster", + "desktop.menu.closeWindow": "Stäng fönster", + "desktop.menu.undo": "Ångra", + "desktop.menu.redo": "Gör om", + "desktop.menu.cut": "Klipp ut", + "desktop.menu.copy": "Kopiera", + "desktop.menu.paste": "Klistra in", + "desktop.menu.delete": "Radera", + "desktop.menu.selectAll": "Markera allt", + "desktop.menu.toggleSidebar": "Växla sidofältet", + "desktop.menu.toggleTerminal": "Växla terminal", + "desktop.menu.toggleFileTree": "Växla filträdet", + "desktop.menu.reload": "Läs in igen", + "desktop.menu.toggleDeveloperTools": "Växla utvecklarverktyg", + "desktop.menu.actualSize": "Verklig storlek", + "desktop.menu.zoomIn": "Zooma in", + "desktop.menu.zoomOut": "Zooma ut", + "desktop.menu.toggleFullScreen": "Växla helskärmsläge", + "desktop.menu.back": "Bakåt", + "desktop.menu.forward": "Framåt", + "desktop.menu.previousSession": "Föregående session", + "desktop.menu.nextSession": "Nästa session", + "desktop.menu.previousProject": "Föregående projekt", + "desktop.menu.nextProject": "Nästa projekt", + "desktop.menu.minimize": "Minimera", + "desktop.menu.maximize": "Maximera", + "desktop.menu.documentation": "OpenCode-dokumentation", + "desktop.menu.supportForum": "Supportforum", + "desktop.menu.shareFeedback": "Skicka feedback", + "desktop.menu.reportBug": "Rapportera ett fel", + "desktop.menu.ariaLabel": "OpenCode-meny", + "desktop.updater.dialog.checkFailed.message": "Det gick inte att söka efter uppdateringar.", + "desktop.updater.dialog.checkFailed.title": "Uppdateringsfel", + "desktop.updater.dialog.upToDate.message": "Du har den senaste versionen.", + "desktop.updater.dialog.upToDate.title": "Inga uppdateringar", + "desktop.updater.dialog.ready.message": "Uppdateringen {{version}} har hämtats. Vill du starta om nu?", + "desktop.updater.dialog.ready.title": "Uppdateringen är klar", + "desktop.updater.dialog.restart": "Starta om", + "desktop.updater.dialog.later": "Senare", + "desktop.recovery.action.relaunch": "Starta om appen", + "desktop.recovery.action.exportLogs": "Exportera loggar", + "desktop.recovery.action.keepWaiting": "Fortsätt vänta", + "desktop.recovery.action.quit": "Avsluta", + "desktop.recovery.loadFailed": "OpenCode kunde inte läsas in", + "desktop.recovery.terminated": "OpenCode-fönstret avslutades oväntat", + "desktop.recovery.unresponsive": "OpenCode svarar inte", + "desktop.recovery.unresponsive.detail": "Du kan starta om appen, öppna loggarna eller fortsätta vänta.", + "desktop.recovery.loadFailed.detail": "Fönster: {{window}}\nURL: {{url}}\nFel: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Fönster: {{window}}\nOrsak: {{reason}}\nKod: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Välj en mapp", + "desktop.dialog.chooseFile": "Välj en fil", + "desktop.dialog.saveFile": "Spara fil", + "desktop.dialog.files": "Filer", + "desktop.server.local": "Lokal server", + "desktop.wsl.error.windowsOnly": "WSL är endast tillgängligt i Windows", + "desktop.wsl.error.unavailable": "WSL är inte tillgängligt", + "desktop.wsl.error.listInstalled": "Det gick inte att lista installerade WSL-distributioner", + "desktop.wsl.error.listOnline": "Det gick inte att lista tillgängliga WSL-distributioner", + "desktop.wsl.error.executeDistro": "Det går inte att köra kommandon i distributionen", + "desktop.wsl.error.installWsl": "Installationen av WSL misslyckades", + "desktop.wsl.error.installDistro": "Det gick inte att installera distributionen: {{distro}}", + "desktop.wsl.error.installOpencode": "Installationen av OpenCode misslyckades", + "desktop.wsl.error.alreadyAdded": "{{distro}} har redan lagts till", + "desktop.wsl.error.opencodeMissing": "opencode är inte installerat i den här distributionen", + "desktop.wsl.error.opencodeCannotRun": "opencode är installerat men kunde inte köras", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode är inte installerat i {{distro}}", + "desktop.wsl.error.updateVersion": + "OpenCode-uppdateringen slutfördes, men {{distro}} rapporterar fortfarande {{installed}}. {{expected}} förväntades", + "desktop.wsl.error.noVersion": "ingen version", + "desktop.wsl.error.serverExited": "WSL-servern avslutades efter start (kod={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL-servern avslutades innan den blev felfri (kod={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Tidsgränsen för hälsokontrollen av sidoprocessen för {{distro}} överskreds efter {{timeout}} ms", + "desktop.wsl.error.commandTimeout": "Tidsgränsen för {{command}} {{args}} överskreds efter {{timeout}} ms", + "desktop.wsl.error.failedPort": "Det gick inte att hämta porten", + "desktop.picker.error.notSelected": "Ingen fil valdes i filväljaren", + "desktop.picker.error.sizeLimit": "De valda bilagorna överskrider gränsen på {{limit}} MB", + "command.category.suggested": "Föreslagna", + "command.category.view": "Visa", + "command.category.project": "Projekt", + "command.category.provider": "Leverantör", + "command.category.server": "Server", + "command.category.session": "Session", + "command.category.theme": "Tema", + "command.category.language": "Språk", + "command.category.file": "Fil", + "command.category.context": "Kontext", + "command.category.terminal": "Terminal", + "command.category.model": "Modell", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Behörigheter", + "command.category.workspace": "Arbetsyta", + "command.category.settings": "Inställningar", + "command.logs.export": "Exportera loggar", + "theme.scheme.system": "System", + "theme.scheme.light": "Ljus", + "theme.scheme.dark": "Mörk", + "command.sidebar.toggle": "Växla sidofältet", + "command.project.open": "Öppna projekt", + "command.project.previous": "Föregående projekt", + "command.project.next": "Nästa projekt", + "command.project.index": "Byt till projekt {{index}}", + "command.provider.connect": "Anslut leverantör", + "command.server.switch": "Byt server", + "command.settings.open": "Öppna inställningar", + "command.session.previous": "Föregående session", + "command.session.next": "Nästa session", + "command.session.previous.unseen": "Föregående oläst session", + "command.session.next.unseen": "Nästa olästa session", + "command.session.archive": "Arkivera session", + "command.palette": "Kommandopalett", + "command.theme.cycle": "Växla tema", + "command.theme.set": "Använd tema: {{theme}}", + "command.theme.scheme.cycle": "Växla färgschema", + "command.theme.scheme.set": "Använd färgschema: {{scheme}}", + "command.language.cycle": "Växla språk", + "command.language.set": "Använd språk: {{language}}", + "command.session.new": "Ny session", + "command.file.open": "Öppna filen", + "command.tab.close": "Stäng fliken", + "command.tab.reopenClosed": "Öppna stängd flik igen", + "command.context.addSelection": "Lägg till markering i kontexten", + "command.context.addSelection.description": "Lägg till markerade rader från den aktuella filen", + "command.input.focus": "Fokusera inmatningsfältet", + "command.terminal.toggle": "Växla terminal", + "command.fileTree.toggle": "Växla filträdet", + "command.review.toggle": "Växla granskning", + "command.terminal.new": "Ny terminal", + "command.terminal.new.description": "Skapa en ny terminalflik", + "command.steps.toggle": "Växla steg", + "command.steps.toggle.description": "Visa eller dölj steg för det aktuella meddelandet", + "command.message.previous": "Föregående meddelande", + "command.message.previous.description": "Gå till föregående användarmeddelande", + "command.message.next": "Nästa meddelande", + "command.message.next.description": "Gå till nästa användarmeddelande", + "command.model.choose": "Välj modell", + "command.model.choose.description": "Välj en annan modell", + "command.mcp.toggle": "Växla MCP:er", + "command.mcp.toggle.description": "Växla MCP:er", + "command.agent.cycle": "Växla agent", + "command.agent.cycle.description": "Byt till nästa agent", + "command.agent.cycle.reverse": "Växla agent bakåt", + "command.agent.cycle.reverse.description": "Byt till föregående agent", + "command.model.variant.cycle": "Växla resonemangsnivå", + "command.model.variant.cycle.description": "Växla till nästa resonemangsnivå", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Autoacceptera behörigheter", + "command.permissions.autoaccept.disable": "Sluta att automatiskt acceptera behörigheter", + "command.workspace.toggle": "Växla arbetsytor", + "command.workspace.toggle.description": "Aktivera eller inaktivera flera arbetsytor i sidofältet", + "command.session.undo": "Ångra", + "command.session.undo.description": "Ångra det senaste meddelandet", + "command.session.redo": "Gör om", + "command.session.redo.description": "Gör om det senaste ångrade meddelandet", + "command.session.compact": "Komprimera session", + "command.session.compact.description": "Sammanfatta sessionen för att minska kontextstorleken", + "command.session.fork": "Förgrena från meddelande", + "command.session.fork.description": "Skapa en ny session från ett tidigare meddelande", + "command.session.share": "Dela session", + "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", + "palette.group.commands": "Kommandon", + "palette.group.files": "Filer", + "dialog.provider.search.placeholder": "Sök leverantörer", + "dialog.provider.empty": "Inga leverantörer hittades", + "dialog.provider.group.popular": "Populära", + "dialog.provider.group.other": "Andra", + "dialog.provider.custom.label": "Anpassad OpenAI-kompatibel leverantör", + "dialog.provider.tag.recommended": "Rekommenderad", + "dialog.provider.opencode.note": "Kurerade modeller inklusive Claude, GPT, Gemini och mer", + "dialog.provider.opencode.tagline": "Pålitliga optimerade modeller", + "dialog.provider.opencodeGo.tagline": "Lågprisprenumeration för alla", + "dialog.provider.anthropic.note": "Direkt tillgång till Claude-modeller, inklusive Pro och Max", + "dialog.provider.copilot.note": "AI-modeller för kodningshjälp via GitHub Copilot", + "dialog.provider.openai.note": "GPT-modeller för snabba, kapabla allmänna AI-uppgifter", + "dialog.provider.google.note": "Gemini-modeller för snabba, strukturerade svar", + "dialog.provider.openrouter.note": "Få tillgång till alla modeller som stöds från en leverantör", + "dialog.provider.vercel.note": "Enhetlig tillgång till AI-modeller med smart routing", + "dialog.model.select.title": "Välj modell", + "dialog.model.search.placeholder": "Sök efter modeller", + "dialog.model.empty": "Inga modellresultat", + "dialog.model.manage": "Hantera modeller", + "dialog.model.manage.description": "Anpassa vilka modeller som visas i modellväljaren.", + "dialog.model.manage.provider.toggle": "Växla alla {{provider}}-modeller", + "dialog.model.unpaid.freeModels.title": "Gratis modeller tillhandahålls av OpenCode", + "dialog.model.unpaid.addMore.title": "Lägg till fler modeller från populära leverantörer", + "dialog.model.unpaid.viewMoreProviders": "Se fler än 70 leverantörer", + "dialog.provider.viewAll": "Visa fler leverantörer", + "provider.connect.title": "Anslut {{provider}}", + "provider.connect.title.anthropicProMax": "Logga in med Claude Pro/Max", + "provider.connect.selectMethod": "Välj inloggningsmetod för {{provider}}.", + "provider.connect.method.apiKey": "API-nyckel", + "provider.connect.method.browser": "Webbläsare", + "provider.connect.method.headless": "Huvudlös", + "provider.connect.status.inProgress": "Auktorisering pågår...", + "provider.connect.status.waiting": "Väntar på auktorisering...", + "provider.connect.status.failed": "Auktorisering misslyckades: {{error}}", + "provider.connect.apiKey.description": + "Ange din {{provider}}-API-nyckel för att ansluta ditt konto och använda {{provider}}-modeller i OpenCode.", + "provider.connect.apiKey.label": "{{provider}}-API-nyckel", + "provider.connect.apiKey.placeholder": "API-nyckel", + "provider.connect.apiKey.required": "API-nyckel krävs", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ger dig tillgång till en utvald uppsättning pålitliga optimerade modeller för kodningsagenter.", + "provider.connect.opencodeZen.line2": + "Med en enda API-nyckel får du tillgång till modeller som Claude, GPT, Gemini, GLM och mer.", + "provider.connect.opencodeZen.visit.prefix": "Besök ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " för att hämta din API-nyckel.", + "provider.connect.oauth.code.visit.prefix": "Besök ", + "provider.connect.oauth.code.visit.link": "denna länk", + "provider.connect.oauth.code.visit.suffix": + " för att hämta auktoriseringskoden som ansluter ditt konto och låter dig använda {{provider}}-modeller i OpenCode.", + "provider.connect.oauth.code.label": "Auktoriseringskod för {{method}}", + "provider.connect.oauth.code.placeholder": "Auktoriseringskod", + "provider.connect.oauth.code.required": "Auktoriseringskod krävs", + "provider.connect.oauth.code.invalid": "Ogiltig auktoriseringskod", + "provider.connect.oauth.auto.visit.prefix": "Besök ", + "provider.connect.oauth.auto.visit.link": "denna länk", + "provider.connect.oauth.auto.visit.suffix": + " och ange koden nedan för att ansluta ditt konto och använda {{provider}}-modeller i OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Bekräftelsekod", + "provider.connect.toast.connected.title": "{{provider}} ansluten", + "provider.connect.toast.connected.description": "{{provider}}-modeller är nu tillgängliga.", + "provider.custom.title": "Anpassad leverantör", + "provider.custom.unavailable": "Anpassade leverantörer är inte tillgängliga på den här servern", + "provider.custom.description.prefix": "Konfigurera en OpenAI-kompatibel leverantör. Se ", + "provider.custom.description.link": "dokumentationen om leverantörskonfiguration", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Leverantörs-ID", + "provider.custom.field.providerID.placeholder": "minleverantor", + "provider.custom.field.providerID.description": "Små bokstäver, siffror, bindestreck eller understreck", + "provider.custom.field.name.label": "Visningsnamn", + "provider.custom.field.name.placeholder": "Min AI-leverantör", + "provider.custom.field.baseURL.label": "Bas-URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API-nyckel", + "provider.custom.field.apiKey.placeholder": "API-nyckel", + "provider.custom.field.apiKey.description": "Valfritt. Lämna tomt om du hanterar autentisering via HTTP-huvuden.", + "provider.custom.models.label": "Modeller", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "modell-id", + "provider.custom.models.name.label": "Namn", + "provider.custom.models.name.placeholder": "Visningsnamn", + "provider.custom.models.remove": "Ta bort modellen", + "provider.custom.models.add": "Lägg till modell", + "provider.custom.headers.label": "HTTP-huvuden (valfritt)", + "provider.custom.headers.key.label": "HTTP-huvud", + "provider.custom.headers.key.placeholder": "Namn på HTTP-huvud", + "provider.custom.headers.value.label": "Värde", + "provider.custom.headers.value.placeholder": "värde", + "provider.custom.headers.remove": "Ta bort HTTP-huvud", + "provider.custom.headers.add": "Lägg till HTTP-huvud", + "provider.custom.error.providerID.required": "Leverantörs-ID krävs", + "provider.custom.error.providerID.format": "Använd små bokstäver, siffror, bindestreck eller understreck", + "provider.custom.error.providerID.exists": "Det leverantörs-ID:t finns redan", + "provider.custom.error.name.required": "Visningsnamn krävs", + "provider.custom.error.baseURL.required": "Bas-URL krävs", + "provider.custom.error.baseURL.format": "Måste börja med http:// eller https://", + "provider.custom.error.required": "Obligatoriskt", + "provider.custom.error.duplicate": "Dubblett", + "provider.disconnect.toast.disconnected.title": "{{provider}} frånkopplad", + "provider.disconnect.toast.disconnected.description": "{{provider}}-modeller är inte längre tillgängliga.", + "model.tag.free": "Gratis", + "model.tag.latest": "Senaste", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "text", + "model.input.image": "bild", + "model.input.audio": "ljud", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Tillåter: {{inputs}}", + "model.tooltip.context.label": "Kontext", + "model.tooltip.inputs": "Indata", + "model.tooltip.model": "Modell", + "model.tooltip.provider": "Leverantör", + "model.tooltip.reasoning": "Resonemang", + "model.tooltip.reasoning.allowed": "Tillåter resonemang", + "model.tooltip.reasoning.none": "Inget resonemang", + "model.tooltip.context": "Kontextgräns {{limit}}", + "common.search.placeholder": "Sök", + "common.goBack": "Navigera tillbaka", + "common.goForward": "Navigera framåt", + "common.loading": "Laddar", + "common.loading.ellipsis": "...", + "common.cancel": "Avbryt", + "common.clear": "Rensa", + "common.open": "Öppna", + "common.connect": "Anslut", + "common.disconnect": "Koppla från", + "common.continue": "Fortsätt", + "common.submit": "Skicka", + "common.save": "Spara", + "common.saving": "Sparar...", + "common.default": "Standard", + "common.attachment": "bilaga", + "prompt.placeholder.shell": "Ange skalkommando... {{example}}", + "prompt.placeholder.normal": 'Fråga vad som helst... "{{example}}"', + "prompt.placeholder.simple": "Fråga vad som helst...", + "prompt.placeholder.summarizeComments": "Sammanfatta kommentarer...", + "prompt.placeholder.summarizeComment": "Sammanfatta kommentar...", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "esc för att avsluta", + "session.child.promptDisabled": "Det går inte att skicka promptar till underagentsessioner.", + "session.child.backToParent": "Tillbaka till huvudsessionen.", + "prompt.example.1": "Fixa en TODO i kodbasen", + "prompt.example.2": "Vad är den tekniska stacken i det här projektet?", + "prompt.example.3": "Åtgärda trasiga tester", + "prompt.example.4": "Förklara hur autentisering fungerar", + "prompt.example.5": "Hitta och åtgärda säkerhetsbrister", + "prompt.example.6": "Lägg till enhetstester för användartjänsten", + "prompt.example.7": "Refaktorera denna funktion för att bli mer läsbar", + "prompt.example.8": "Vad betyder detta fel?", + "prompt.example.9": "Hjälp mig att felsöka det här problemet", + "prompt.example.10": "Skapa API-dokumentation", + "prompt.example.11": "Optimera databasfrågor", + "prompt.example.12": "Lägg till indatavalidering", + "prompt.example.13": "Skapa en ny komponent för...", + "prompt.example.14": "Hur distribuerar jag det här projektet?", + "prompt.example.15": "Granska min kod för bästa praxis", + "prompt.example.16": "Lägg till felhantering till denna funktion", + "prompt.example.17": "Förklara detta regexmönster", + "prompt.example.18": "Konvertera detta till TypeScript", + "prompt.example.19": "Lägg till loggning i hela kodbasen", + "prompt.example.20": "Vilka beroenden är föråldrade?", + "prompt.example.21": "Hjälp mig att skriva ett migreringsskript", + "prompt.example.22": "Implementera cachning för denna slutpunkt", + "prompt.example.23": "Lägg till paginering till den här listan", + "prompt.example.24": "Skapa ett CLI-kommando för...", + "prompt.example.25": "Hur fungerar miljövariabler här?", + "prompt.popover.emptyResults": "Inga matchande resultat", + "prompt.popover.emptyCommands": "Inga matchande kommandon", + "prompt.dropzone.label": "Släpp bilder, PDF-filer eller textfiler här", + "prompt.dropzone.file.label": "Släpp för att @-omnämna filen", + "prompt.slash.badge.custom": "anpassad", + "prompt.slash.badge.skill": "färdighet", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "aktiv", + "prompt.context.includeActiveFile": "Inkludera aktiv fil", + "prompt.context.removeActiveFile": "Ta bort aktiv fil från kontexten", + "prompt.context.removeFile": "Ta bort filen från kontexten", + "prompt.action.attachFile": "Lägg till filer", + "prompt.menu.addImagesAndFiles": "Lägg till filer och mer", + "prompt.menu.imagesAndFiles": "Bilder och filer", + "prompt.menu.commands": "Kommandon", + "prompt.menu.context": "Kontext", + "prompt.menu.shellCommand": "Skalkommando", + "prompt.attachment.remove": "Ta bort bilagan", + "prompt.action.send": "Skicka", + "prompt.action.stop": "Stoppa", + "prompt.toast.pasteUnsupported.title": "Bilaga som inte stöds", + "prompt.toast.pasteUnsupported.description": "Endast bilder, PDF-filer eller textfiler kan bifogas här.", + "prompt.toast.attachmentDuplicate.title": "Den här filen har redan laddats upp", + "prompt.toast.modelAgentRequired.title": "Välj en agent och modell", + "prompt.toast.modelAgentRequired.description": "Välj en agent och modell innan du skickar en prompt.", + "prompt.toast.worktreeCreateFailed.title": "Det gick inte att skapa worktree", + "prompt.toast.sessionCreateFailed.title": "Det gick inte att skapa session", + "prompt.toast.shellSendFailed.title": "Det gick inte att skicka skalkommandot", + "prompt.toast.commandSendFailed.title": "Det gick inte att skicka kommandot", + "prompt.toast.promptSendFailed.title": "Det gick inte att skicka meddelandet", + "prompt.toast.promptSendFailed.description": "Det gick inte att hämta sessionen", + "dialog.mcp.title": "MCP:er", + "dialog.mcp.description": "{{enabled}} av {{total}} aktiverade", + "dialog.mcp.empty": "Inga MCP:er konfigurerade", + "dialog.lsp.empty": "LSP:er detekteras automatiskt från filtyper", + "dialog.plugins.empty": "Insticksprogram konfigurerade i opencode.json", + "mcp.status.connected": "ansluten", + "mcp.status.failed": "misslyckades", + "mcp.status.needs_auth": "behöver autentisering", + "mcp.status.disabled": "inaktiverad", + "mcp.auth.clickToAuthenticate": "Klicka för att autentisera", + "dialog.fork.empty": "Inga meddelanden att förgrena från", + "dialog.directory.search.placeholder": "Sök i mappar", + "dialog.directory.empty": "Inga mappar hittades", + "dialog.directory.action.selectFile": "Välj fil", + "dialog.directory.action.selectFolder": "Välj mapp", + "dialog.directory.root": "Rotmapp", + "dialog.directory.parent": "Överordnad mapp", + "dialog.directory.readError": "Det går inte att läsa den här mappen", + "app.server.unreachable": "Kunde inte nå {{server}}", + "app.server.retrying": "Försöker igen automatiskt...", + "app.server.otherServers": "Andra servrar", + "dialog.server.title": "Servrar", + "dialog.server.description": "Växla vilken OpenCode-server som denna app ansluter till.", + "dialog.server.search.placeholder": "Sök servrar", + "dialog.server.empty": "Inga servrar ännu", + "dialog.server.add.title": "Lägg till server", + "dialog.server.add.url": "Serveradress", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Kunde inte ansluta till servern", + "dialog.server.add.checking": "Kontrollerar...", + "dialog.server.add.button": "Lägg till server", + "dialog.server.add.name": "Servernamn (valfritt)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Användarnamn (valfritt)", + "dialog.server.add.usernamePlaceholder": "användarnamn", + "dialog.server.add.password": "Lösenord (valfritt)", + "dialog.server.add.passwordPlaceholder": "lösenord", + "dialog.server.edit.title": "Redigera server", + "dialog.server.default.title": "Standardserver", + "dialog.server.default.description": + "Anslut till den här servern vid appstart istället för att starta en lokal server. Kräver omstart.", + "dialog.server.default.none": "Ingen server vald", + "dialog.server.default.set": "Ställ in aktuell server som standard", + "dialog.server.default.clear": "Rensa", + "dialog.server.action.remove": "Ta bort servern", + "dialog.server.menu.edit": "Redigera", + "dialog.server.menu.default": "Ställ in som standard", + "dialog.server.menu.defaultRemove": "Ta bort standard", + "dialog.server.menu.delete": "Radera", + "dialog.server.current": "Aktuell server", + "dialog.server.status.default": "Standard", + "wsl.server.add": "Lägg till WSL-server", + "wsl.server.addShort": "Lägg till WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL-server", + "wsl.server.retryStart": "Försök att starta igen", + "wsl.server.updating": "Uppdaterar...", + "wsl.onboarding.step.distro": "Välj distro", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Kontrollerar WSL...", + "wsl.onboarding.restartRequired": "Windows behöver en omstart för att slutföra installationen av WSL.", + "wsl.onboarding.ready": "WSL är redo.", + "wsl.onboarding.required": "WSL krävs för att fortsätta.", + "wsl.onboarding.checkingDistros": "Kontrollerar distributioner...", + "wsl.onboarding.installingDistro": "Installerar {{distro}}...", + "wsl.onboarding.checkingDistro": "Kontrollerar {{distro}}...", + "wsl.onboarding.listingDistros": "Listar distributioner...", + "wsl.onboarding.distroReady": "{{distro}} är klar.", + "wsl.onboarding.distroNotInstalled": "{{distro}} är inte installerad än.", + "wsl.onboarding.openDistroOnce": "Öppna {{distro}} en gång för att avsluta installationen.", + "wsl.onboarding.finishingDistro": "Slutför installationen för {{distro}}.", + "wsl.onboarding.pickDistro": "Välj en distro eller installera en nedan.", + "wsl.onboarding.checkingOpencode": "Kontrollerar OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Kontrollerar OpenCode i {{distro}}...", + "wsl.onboarding.updatingOpencode": "Uppdaterar OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Uppdaterar OpenCode i {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Uppdatera OpenCode i {{distro}}.", + "wsl.onboarding.updateOpencode": "Uppdatera OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode är redo i {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode är redo.", + "wsl.onboarding.installOpencodeIn": "Installera OpenCode i {{distro}}.", + "wsl.onboarding.installOpencode": "Installera OpenCode", + "wsl.onboarding.chooseDistroFirst": "Välj en distro först.", + "wsl.onboarding.loadFailed": "Det gick inte att ladda WSL-tillståndet.", + "wsl.onboarding.loading": "Laddar...", + "wsl.onboarding.installedDistros": "Installerade distros", + "wsl.onboarding.checkAgain": "Kontrollera igen", + "wsl.onboarding.distroStatus.ready": "Redo", + "wsl.onboarding.distroStatus.checking": "Kontrollerar...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode är inte installerat", + "wsl.onboarding.distroStatus.missingTools": "Saknar bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Stöds inte · Använd WSL 2", + "wsl.onboarding.needAnotherDistro": "Behöver du en annan distro?", + "wsl.onboarding.needAnotherDistroHint": "Installera en Linux-distribution från WSL-katalogen", + "wsl.onboarding.wslNotInstalled.title": "WSL är inte installerat", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) krävs innan OpenCode kan lägga till en WSL-server", + "wsl.onboarding.wslUnavailable.title": "WSL är inte tillgängligt", + "wsl.onboarding.wslUnavailable.description": "OpenCode kunde inte verifiera WSL på den här maskinen.", + "wsl.onboarding.installWsl": "Installera WSL", + "wsl.onboarding.windowsRestartRequired": + "Starta om Windows för att slutföra installationen av WSL och öppna sedan OpenCode igen.", + "wsl.onboarding.next": "Nästa", + "wsl.onboarding.refresh": "Uppdatera", + "wsl.onboarding.allDistrosAdded": "Alla installerade distros har redan lagts till.", + "wsl.onboarding.noDistros": "Inga distros har upptäckts ännu.", + "wsl.onboarding.install": "Installera", + "wsl.onboarding.installing": "Installerar...", + "wsl.onboarding.installDistro": "Installera distro", + "wsl.onboarding.searchDistros": "Sök efter distributioner", + "wsl.onboarding.wsl2Required": "WSL 2 krävs.", + "wsl.onboarding.toolsRequired": "Denna distro behöver bash och curl.", + "wsl.onboarding.openTerminal": "Öppna terminalen", + "wsl.onboarding.path": "Sökväg: {{path}}", + "wsl.onboarding.notFound": "hittades inte", + "wsl.onboarding.version": "Version: {{version}}", + "wsl.onboarding.unknown": "okänd", + "wsl.onboarding.desktopVersion": "desktop {{version}}", + "wsl.onboarding.versionMismatch": "Den installerade versionen matchar inte versionen av skrivbordsappen.", + "wsl.onboarding.adding": "Lägger till...", + "help.tabs.toast.ariaLabel": "Vi presenterar flikar. Ordna ditt arbete och dina aktiva sessioner med flikar", + "help.tabs.toast.dismiss": "Stäng informationen om flikar", + "help.tabs.title": "Vi presenterar flikar", + "help.tabs.description": "Ordna ditt arbete och dina aktiva sessioner med flikar", + "help.tabs.date": "14 juli", + "help.tabs.introduction": "OpenCode Desktop är nu uppbyggt kring flikar.", + "help.tabs.sessions": + "Starta en ny session på en flik eller öppna en befintlig session från något av dina projekt. Öppna en ny flik när du påbörjar något nytt och stäng den när du är klar.", + "help.tabs.organize": + "Genom att hålla några flikar öppna blir det enklare att ordna dina aktiva sessioner. Byt namn på flikarna till något du kommer ihåg om du tänker behålla dem.", + "help.tabs.home": + "Du hittar alla dina sessioner och projekt på den nya hemskärmen. När du väljer en session öppnas den på en flik.", + "help.tabs.persistence": "När du öppnar appen igen är dina flikar fortfarande öppna.", + "help.tabs.worktrees": + "Den nya designen stöder inte Git Worktrees ännu, men stöd kommer snart. Om du föredrar att fortsätta använda den tidigare layouten kan du därför växla mellan layouterna i Inställningar. Tänk bara på att den nya layouten blir permanent om några veckor.", + "server.row.noUsername": "inget användarnamn", + "dialog.project.edit.title": "Redigera projekt", + "dialog.project.edit.name": "Namn", + "dialog.project.edit.icon": "Ikon", + "dialog.project.edit.icon.alt": "Projektikonen", + "dialog.project.edit.icon.hint": "Klicka eller dra en bild", + "dialog.project.edit.icon.recommended": "Rekommenderat: 128x128px", + "dialog.project.edit.color": "Färg", + "dialog.project.edit.color.select": "Välj färgen {{color}}", + "dialog.project.edit.worktree.startup": "Startskript för arbetsyta", + "dialog.project.edit.worktree.startup.description": "Körs efter att en ny arbetsyta (worktree) har skapats.", + "dialog.project.edit.worktree.startup.placeholder": "t.ex. bun install", + "dialog.releaseNotes.action.getStarted": "Kom igång", + "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.', + "context.breakdown.system": "System", + "context.breakdown.user": "Användare", + "context.breakdown.assistant": "Assistent", + "context.breakdown.tool": "Verktygsanrop", + "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", + "context.stats.model": "Modell", + "context.stats.limit": "Kontextgräns", + "context.stats.totalTokens": "Totala tokens", + "context.stats.usage": "Användning", + "context.stats.inputTokens": "Indatatokens", + "context.stats.outputTokens": "Utdatatokens", + "context.stats.reasoningTokens": "Resonemangstokens", + "context.stats.cacheTokens": "Cache-tokens (läs/skriv)", + "context.stats.userMessages": "Användarmeddelanden", + "context.stats.assistantMessages": "Assistentmeddelanden", + "context.stats.totalCost": "Total kostnad", + "context.stats.sessionCreated": "Session skapades", + "context.stats.lastActivity": "Senaste aktivitet", + "context.usage.tokens": "Tokens", + "context.usage.usage": "Användning", + "context.usage.cost": "Kostnad", + "context.usage.clickToView": "Klicka för att visa kontexten", + "context.usage.view": "Visa kontextanvändning", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Språk", + "toast.language.description": "Bytte språk till {{language}}", + "toast.theme.title": "Tema har bytts", + "toast.scheme.title": "Färgschema", + "toast.workspace.enabled.title": "Arbetsytor aktiverade", + "toast.workspace.enabled.description": "Flera worktrees visas nu i sidofältet", + "toast.workspace.disabled.title": "Arbetsytor inaktiverade", + "toast.workspace.disabled.description": "Endast huvudworktreen visas i sidofältet", + "toast.permissions.autoaccept.on.title": "Autoaccepterar behörigheter", + "toast.permissions.autoaccept.on.description": "Behörighetsförfrågningar kommer att godkännas automatiskt", + "toast.permissions.autoaccept.off.title": "Slutade att automatiskt acceptera behörigheter", + "toast.permissions.autoaccept.off.description": "Behörighetsförfrågningar kommer att kräva godkännande", + "toast.model.none.title": "Ingen modell har valts", + "toast.model.none.description": "Anslut en leverantör för att sammanfatta denna session", + "toast.file.loadFailed.title": "Det gick inte att ladda filen", + "toast.file.listFailed.title": "Det gick inte att lista filer", + "toast.context.noLineSelection.title": "Inget radval", + "toast.context.noLineSelection.description": "Välj ett radintervall på en filflik först.", + "toast.session.share.copyFailed.title": "Det gick inte att kopiera URL till urklipp", + "toast.session.share.success.title": "Sessionen har delats", + "toast.session.share.success.description": "Delningsadressen har kopierats till urklipp!", + "toast.session.share.failed.title": "Det gick inte att dela sessionen", + "toast.session.share.failed.description": "Ett fel uppstod när sessionen delades", + "toast.session.unshare.success.title": "Sessionen delas inte längre", + "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", + "toast.update.description": "En ny version av OpenCode ({{version}}) är nu tillgänglig att installera.", + "toast.update.action.installRestart": "Installera och starta om", + "toast.update.action.notYet": "Inte än", + "error.page.title": "Något gick fel", + "error.page.description": "Ett fel uppstod när programmet laddades.", + "error.page.description.localServerStartup": "Ett fel uppstod vid start av den lokala servern.", + "error.page.details.label": "Feldetaljer", + "error.page.action.restart": "Starta om", + "error.page.action.report": "Rapportera fel", + "error.page.action.reported": "Fel rapporterat", + "error.page.action.exportLogs": "Exportera loggar", + "error.page.action.checking": "Kontrollerar...", + "error.page.action.checkUpdates": "Sök efter uppdateringar", + "error.page.action.updateTo": "Uppdatera till {{version}}", + "error.page.circular": "[Circular]", + "error.page.report.prefix": "Rapportera det här felet till OpenCode-teamet", + "error.page.report.discord": "på Discord", + "error.page.version": "Version: {{version}}", + "error.dev.rootNotFound": + "Rotelementet hittades inte. Har du glömt att lägga till det i din index.html? Eller kanske id-attributet är felstavat?", + "error.serverSync.connectFailed": "Kunde inte ansluta till servern. Körs det en server på `{{url}}`?", + "error.serverSDK.noServerAvailable": "Ingen server tillgänglig", + "error.serverSDK.serverNotAvailable": "Servern är inte tillgänglig", + "error.childStore.persistedCacheCreateFailed": "Det gick inte att skapa beständig cache", + "error.childStore.persistedProjectMetadataCreateFailed": "Det gick inte att skapa beständig projektmetadata", + "error.childStore.persistedProjectIconCreateFailed": "Det gick inte att skapa en beständig projektikon", + "error.childStore.storeCreateFailed": "Det gick inte att skapa lagring", + "directory.error.invalidUrl": "Ogiltig katalog i URL.", + "error.chain.unknown": "Okänt fel", + "error.server.invalidConfiguration": "Ogiltig konfiguration", + "error.chain.causedBy": "Orsakas av:", + "error.chain.apiError": "API-fel", + "error.chain.status": "Status: {{status}}", + "error.chain.retryable": "Kan försökas igen: {{retryable}}", + "error.chain.responseBody": "Svarstext:\n{{body}}", + "error.chain.didYouMean": "Menade du: {{suggestions}}", + "error.chain.modelNotFound": "Modellen hittades inte: {{provider}}/{{model}}", + "error.chain.checkConfig": "Kontrollera leverantörs- och modellnamnen i konfigurationen (opencode.json)", + "error.chain.mcpFailed": 'MCP-servern "{{name}}" misslyckades. Obs, OpenCode stöder inte MCP-autentisering ännu.', + "error.chain.providerAuthFailed": "Leverantörsautentisering misslyckades ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Det gick inte att initiera leverantören "{{provider}}". Kontrollera autentiseringsuppgifter och konfiguration.', + "error.chain.configJsonInvalid": "Konfigurationsfilen på {{path}} är inte giltig JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Konfigurationsfilen på {{path}} är inte giltig JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Katalogen "{{dir}}" i {{path}} är inte giltig. Byt namn på katalogen till "{{suggestion}}" eller ta bort den. Detta är ett vanligt stavfel.', + "error.chain.configFrontmatterError": "Det gick inte att analysera frontmatter i {{path}}:\n{{message}}", + "error.chain.configInvalid": "Konfigurationsfilen på {{path}} är ogiltig", + "error.chain.configInvalidWithMessage": "Konfigurationsfilen på {{path}} är ogiltig: {{message}}", + "notification.permission.title": "Behörighet krävs", + "notification.permission.description": "{{sessionTitle}} i {{projectName}} behöver behörighet", + "notification.question.title": "Fråga", + "notification.question.description": "{{sessionTitle}} i {{projectName}} har en fråga", + "notification.action.goToSession": "Gå till session", + "notification.session.responseReady.title": "Svar redo", + "notification.session.error.title": "Sessionsfel", + "notification.session.error.fallbackDescription": "Ett fel uppstod", + "home.recentProjects": "Senaste projekten", + "home.empty.title": "Inga senaste projekt", + "home.empty.description": "Kom igång genom att öppna ett lokalt projekt", + "home.title": "Hem", + "home.projects": "Projekt", + "home.project.add": "Lägg till projekt", + "home.recentlyClosed": "Nyligen stängda", + "home.server.collapse": "Fäll ihop serverprojekt", + "home.server.expand": "Fäll ut serverprojekt", + "home.sessions.search.placeholder": "Sök sessioner", + "home.sessions.search.placeholder.scoped": "Sök sessioner i {{scope}}", + "home.sessions.search.sessions": "Sessioner", + "home.sessions.search.noResults": "Inga sessioner hittades för {{query}}", + "home.sessions.empty": "Inget här ännu", + "home.sessions.empty.description": "Skapa en session för att komma igång", + "home.sessions.group.today": "I dag", + "home.sessions.group.yesterday": "I går", + "home.sessions.group.older": "Äldre", + "home.providerTip": "Anslut till 75+ leverantörer för att använda andra modeller, inklusive Claude, GPT, Gemini, etc", + "session.tab.session": "Session", + "session.tab.review": "Granskning", + "session.tab.context": "Kontext", + "session.tab.unknown": "Okänd session", + "session.panel.reviewAndFiles": "Granskning och filer", + "session.error.notFound": "Denna session kan inte hittas", + "session.error.notFound.description": "Den här fliken pekar på en session som inte längre finns på den här servern.", + "session.error.notFound.closeTab": "Stäng flik", + "session.error.serverConnection": "Det går inte att ansluta till den här servern", + "session.review.filesChanged": "Ändrade filer: {{count}}", + "session.review.change.one": "Ändring", + "session.review.change.other": "Ändringar", + "session.review.loadingChanges": "Läser in ändringar...", + "session.review.empty": "Inga ändringar i denna session ännu", + "session.review.noVcs": "Inget Git-versionskontrollsystem upptäcktes, ändringar visas inte", + "session.review.noVcs.createGit.title": "Skapa ett Git-arkiv", + "session.review.noVcs.createGit.description": "Spåra, granska och ångra ändringar i det här projektet", + "session.review.noVcs.createGit.actionLoading": "Skapar Git-arkiv...", + "session.review.noVcs.createGit.action": "Skapa Git-arkiv", + "session.review.noSnapshot": + "Spårning av ögonblicksbilder är inaktiverad i konfigurationen, så sessionsändringar är inte tillgängliga", + "session.review.noChanges": "Inga ändringar", + "session.review.noUncommittedChanges": "Inga ändringar som inte har checkats in ännu", + "session.review.noBranchChanges": "Inga grenändringar ännu", + "session.files.selectToOpen": "Välj en fil att öppna", + "session.files.all": "Alla filer", + "session.files.empty": "Inga filer", + "session.files.binaryContent": "Binär fil (innehåll kan inte visas)", + "session.messages.renderEarlier": "Återge tidigare meddelanden", + "session.messages.loadingEarlier": "Läser in tidigare meddelanden...", + "session.messages.loadEarlier": "Ladda tidigare meddelanden", + "session.messages.loading": "Läser in meddelanden...", + "session.messages.jumpToLatest": "Hoppa till senaste", + "session.context.addToContext": "Lägg till {{selection}} i kontexten", + "session.todo.title": "Att göra", + "session.todo.collapse": "Fäll ihop", + "session.todo.expand": "Fäll ut", + "session.todo.progress": "{{done}} av {{total}} uppgifter slutförda", + "session.question.progress": "{{current}} av {{total}} frågor", + "session.question.minimize": "Minimera frågan", + "session.question.restore": "Återställ fråga", + "session.question.pending.one": "{{count}} väntande fråga", + "session.question.pending.other": "{{count}} väntande frågor", + "session.followupDock.summary.one": "{{count}} meddelande i kö", + "session.followupDock.summary.other": "{{count}} meddelanden i kö", + "session.followupDock.sendNow": "Skicka nu", + "session.followupDock.edit": "Redigera", + "session.followupDock.collapse": "Fäll ihop köade meddelanden", + "session.followupDock.expand": "Fäll ut köade meddelanden", + "session.revertDock.summary.one": "{{count}} återställt meddelande", + "session.revertDock.summary.other": "{{count}} återställda meddelanden", + "session.revertDock.collapse": "Fäll ihop återställda meddelanden", + "session.revertDock.expand": "Fäll ut återställda meddelanden", + "session.revertDock.restore": "Återställ meddelande", + "session.new.title": "Bygg vad som helst", + "session.new.project.new": "Nytt projekt", + "session.new.project.search": "Sök efter projekt", + "session.new.project.add": "Lägg till projekt", + "session.new.worktree.main": "Huvudgren", + "session.new.worktree.mainWithBranch": "Huvudgren ({{branch}})", + "session.new.worktree.create": "Skapa ny worktree", + "session.new.workspace.runIn": "Kör sessionen i", + "session.new.workspace.triggerLocal": "Lokal", + "session.new.workspace.local": "Lokalt arkiv", + "session.new.workspace.existing": "Arbetsyta...", + "session.new.git.none": "Ingen Git", + "session.new.lastModified": "Senast ändrad", + "session.header.search.placeholder": "Sök {{project}}", + "session.header.searchFiles": "Sök filer", + "session.header.openIn": "Öppna i", + "session.header.open.action": "Öppna {{app}}", + "session.header.open.ariaLabel": "Öppna i {{app}}", + "session.header.open.menu": "Öppna alternativ", + "session.header.open.copyPath": "Kopiera sökväg", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Filutforskaren", + "session.header.open.fileManager": "Filhanteraren", + "session.header.reveal.finder": "Visa i Finder", + "session.header.reveal.fileExplorer": "Visa i Utforskaren", + "session.header.reveal.containingFolder": "Öppna överordnad mapp", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Status", + "status.popover.ariaLabel": "Serverkonfigurationer", + "status.popover.tab.servers": "Servrar", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Insticksprogram", + "status.popover.action.manageServers": "Hantera servrar", + "session.share.popover.title": "Publicera på webben", + "session.share.popover.description.shared": + "Denna session är offentlig på webben. Den är tillgänglig för alla med länken.", + "session.share.popover.description.unshared": + "Dela sessionen offentligt på webben. Den kommer att vara tillgänglig för alla med länken.", + "session.share.action.share": "Dela", + "session.share.action.publish": "Publicera", + "session.share.action.publishing": "Publicerar...", + "session.share.action.unpublish": "Avpublicera", + "session.share.action.unpublishing": "Avpublicerar...", + "session.share.action.view": "Visa", + "session.share.copy.copied": "Kopierad", + "session.share.copy.copyLink": "Kopiera länk", + "lsp.tooltip.none": "Inga LSP-servrar", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Laddar prompt...", + "terminal.loading": "Laddar terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Stäng terminalen", + "terminal.connectionLost.title": "Anslutning förlorad", + "terminal.connectionLost.abnormalClose": "WebSocket stängdes onormalt: {{code}}", + "terminal.connectionLost.description": "Terminalanslutningen avbröts. Detta kan hända när servern startar om.", + "terminal.connectTicket.csrfError": + "PTY-anslutningsbiljetten avvisades vid ursprungs- eller CSRF-kontrollen. Kontrollera serverns CORS-konfiguration.", + "terminal.connectTicket.statusError": "PTY-anslutningsbiljetten misslyckades med status {{status}}", + "titlebar.update": "Uppdatera", + "titlebar.updateVersion": "Uppdatera {{version}}", + "common.closeTab": "Stäng fliken", + "common.dismiss": "Avfärda", + "common.moreCountSuffix": " (+{{count}} till)", + "common.requestFailed": "Begäran misslyckades", + "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", + "common.close": "Stäng", + "common.edit": "Redigera", + "common.loadMore": "Ladda mer", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Mellanslag", + "common.key.backspace": "Backsteg", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "okänd", + "common.time.justNow": "Just nu", + "common.time.minutesAgo.short": "{{count}}m sedan", + "common.time.hoursAgo.short": "{{count}}h sedan", + "common.time.daysAgo.short": "{{count}}d sedan", + "sidebar.menu.toggle": "Växla meny", + "sidebar.nav.projectsAndSessions": "Projekt och sessioner", + "sidebar.settings": "Inställningar", + "sidebar.help": "Hjälp", + "sidebar.workspaces.enable": "Aktivera arbetsytor", + "sidebar.workspaces.disable": "Inaktivera arbetsytor", + "sidebar.gettingStarted.title": "Kom igång", + "sidebar.gettingStarted.line1": "OpenCode innehåller gratis modeller så att du kan börja direkt.", + "sidebar.gettingStarted.line2": "Anslut valfri leverantör för att använda modeller, inkl. Claude, GPT, Gemini etc.", + "sidebar.project.recentSessions": "Senaste sessioner", + "sidebar.project.viewAllSessions": "Se alla sessioner", + "sidebar.project.clearNotifications": "Rensa aviseringar", + "sidebar.empty.title": "Inga projekt öppna", + "sidebar.empty.description": "Öppna ett projekt för att komma igång", + "debugBar.ariaLabel": "Utvecklingsprestandadiagnostik", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Senast slutförda ruttövergång som berör en sessionssida, mätt från routerns start till första bildrutans rendering efter att övergången har slutförts.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Rullande bilder per sekund under de senaste 5 sekunderna.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Sämsta bildtid under de senaste 5 sekunderna.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Bildrutor över 32 ms under de senaste 5 sekunderna.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Blockerad tid och antal långa uppgifter under de senaste 5 sekunderna. Längsta uppgift: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Längsta observerade inmatningsfördröjning under de senaste 5 sekunderna.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Ungefärlig interaktionslängd under de senaste 5 sekunderna. Detta är INP-liknande, inte den officiella Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Kumulativ layoutförskjutning för den aktuella appens livslängd.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Använd JS-heap jämfört med heapgränsen. Endast Chromium.", + "debugBar.mem.tip": "Använd JS-heap jämfört med heapgränsen. {{used}} av {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Tvinga fram fokusstilar på alla interaktiva element", + "debugBar.focus.on": "PÅ", + "debugBar.focus.off": "AV", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Tvinga fram hela appens layoutriktning utan att ändra valt språk", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Skrivbord", + "settings.section.server": "Server", + "settings.tab.general": "Allmänt", + "settings.tab.shortcuts": "Kortkommandon", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL-integration", + "settings.desktop.wsl.description": "Kör OpenCode-servern inuti WSL på Windows.", + "settings.general.section.appearance": "Utseende", + "settings.general.section.advanced": "Avancerat", + "settings.general.section.notifications": "Systemaviseringar", + "settings.general.section.updates": "Uppdateringar", + "settings.general.section.sounds": "Ljudeffekter", + "settings.general.section.feed": "Flöde", + "settings.general.section.display": "Visning", + "settings.general.row.language.title": "Språk", + "settings.general.row.language.description": "Ändra visningsspråket för OpenCode", + "settings.general.row.shell.title": "Terminalskal", + "settings.general.row.shell.description": "Skal som används av terminalen och agentverktygen", + "settings.general.row.shell.autoDefault": "Auto (standard)", + "settings.general.row.shell.terminalOnly": "endast terminal", + "settings.general.row.appearance.title": "Utseende", + "settings.general.row.appearance.description": "Anpassa hur OpenCode ser ut på din enhet", + "settings.general.row.colorScheme.title": "Färgschema", + "settings.general.row.colorScheme.description": "Välj om OpenCode följer system-, ljus- eller mörktemat", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "Anpassa OpenCodes tema.", + "settings.general.row.font.title": "Kodtypsnitt", + "settings.general.row.font.description": "Anpassa teckensnittet som används i kodblock", + "settings.general.row.terminalFont.title": "Terminaltypsnitt", + "settings.general.row.terminalFont.description": "Anpassa teckensnittet som används i terminalen", + "settings.general.row.uiFont.title": "UI-teckensnitt", + "settings.general.row.uiFont.description": "Anpassa typsnittet som används i hela gränssnittet", + "settings.general.row.followup.title": "Uppföljningsbeteende", + "settings.general.row.followup.description": "Välj om uppföljningsmeddelanden ska styra direkt eller vänta i kö", + "settings.general.row.followup.option.queue": "Kö", + "settings.general.row.followup.option.steer": "Styr", + "settings.general.row.showFileTree.title": "Filträd", + "settings.general.row.showFileTree.description": "Visa filträdspanelen i sessioner", + "settings.general.row.showNavigation.title": "Navigationskontroller", + "settings.general.row.showNavigation.description": "Visa bakåt- och framåtknapparna i skrivbordets namnlist", + "settings.general.row.showSearch.title": "Kommandopalett", + "settings.general.row.showSearch.description": "Visa sök- och kommandopalettens knapp i namnlisten", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Visa terminalknappen i skrivbordets namnlist", + "settings.general.row.showStatus.title": "Serverstatus", + "settings.general.row.showStatus.description": "Visa serverstatusknappen i namnlisten", + "settings.general.row.mobileTitlebarBottom.title": "Bottennavigering", + "settings.general.row.mobileTitlebarBottom.description": + "Placera titelfältet och sessionsflikarna längst ned på skärmen på mobilen", + "settings.general.row.showCustomAgents.title": "Visa agent", + "settings.general.row.showCustomAgents.description": + "Växla mellan agenter i inmatningsfältet. När alternativet är dolt används agenten Bygg som standard.", + "settings.general.row.reasoningSummaries.title": "Visa resonemangssammanfattningar", + "settings.general.row.reasoningSummaries.description": "Visa modellresonemangssammanfattningar i tidslinjen", + "settings.general.row.shellToolPartsExpanded.title": "Expandera skalverktygsdelar", + "settings.general.row.shellToolPartsExpanded.description": + "Visa skalverktygsdelar expanderade som standard på tidslinjen", + "settings.general.row.editToolPartsExpanded.title": "Fäll ut delar för redigeringsverktyg", + "settings.general.row.editToolPartsExpanded.description": + "Visa delar för redigerings-, skriv- och patchverktyg utfällda som standard i tidslinjen", + "settings.general.row.newInterface.title": "Ny layout", + "settings.general.row.newInterface.badge": "Ny", + "settings.general.row.newInterface.description": + "Använd de nya flikarna och hemlayouten. Växla mellan layouter under en begränsad tid.", + "settings.general.row.newInterfaceNotice.title": "Du använder nu ny layout", + "settings.general.row.newInterfaceNotice.description": "Den tidigare layouten är inte längre tillgänglig", + "settings.general.row.newInterfaceNotice.dismiss": "Avfärda", + "settings.general.row.pinchZoom.title": "Nyp för att zooma", + "settings.general.row.pinchZoom.description": "Tillåt nypgester på styrplattan och Ctrl-rullning för att zooma", + "settings.general.row.wayland.title": "Använd inbyggt Wayland-stöd", + "settings.general.row.wayland.description": "Inaktivera X11-reservläget på Wayland. Kräver omstart.", + "settings.general.row.wayland.tooltip": + "På Linux med skärmar som har olika uppdateringsfrekvens kan inbyggt Wayland-stöd vara stabilare.", + "settings.general.row.releaseNotes.title": "Versionsinformation", + "settings.general.row.releaseNotes.description": "Visa popup-fönster för vad som är nytt efter uppdateringar", + "settings.updates.row.startup.title": "Sök efter uppdateringar vid start", + "settings.updates.row.startup.description": "Sök automatiskt efter uppdateringar när OpenCode startas", + "settings.updates.row.check.title": "Sök efter uppdateringar", + "settings.updates.row.check.description": "Sök manuellt efter uppdateringar och installera dem om de är tillgängliga", + "settings.updates.action.checkNow": "Kontrollera nu", + "settings.updates.action.checking": "Kontrollerar...", + "settings.updates.action.downloading": "Laddar ned...", + "settings.updates.action.installing": "Installerar...", + "settings.updates.toast.latest.title": "Du är uppdaterad", + "settings.updates.toast.latest.description": "Du kör den senaste versionen av OpenCode.", + "sound.option.none": "Inget", + "sound.option.alert01": "Varning 01", + "sound.option.alert02": "Varning 02", + "sound.option.alert03": "Varning 03", + "sound.option.alert04": "Varning 04", + "sound.option.alert05": "Varning 05", + "sound.option.alert06": "Varning 06", + "sound.option.alert07": "Alarm 07", + "sound.option.alert08": "Alarm 08", + "sound.option.alert09": "Alarm 09", + "sound.option.alert10": "Varning 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nej 01", + "sound.option.nope02": "Nej 02", + "sound.option.nope03": "Nej 03", + "sound.option.nope04": "Nej 04", + "sound.option.nope05": "Nej 05", + "sound.option.nope06": "Nej 06", + "sound.option.nope07": "Nej 07", + "sound.option.nope08": "Nej 08", + "sound.option.nope09": "Nej 09", + "sound.option.nope10": "Nej 10", + "sound.option.nope11": "Nej 11", + "sound.option.nope12": "Nej 12", + "sound.option.yup01": "Japp 01", + "sound.option.yup02": "Japp 02", + "sound.option.yup03": "Japp 03", + "sound.option.yup04": "Japp 04", + "sound.option.yup05": "Japp 05", + "sound.option.yup06": "Japp 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Visa en systemavisering när agenten är klar eller behöver uppmärksamhet", + "settings.general.notifications.permissions.title": "Behörigheter", + "settings.general.notifications.permissions.description": "Visa systemavisering när en behörighet krävs", + "settings.general.notifications.errors.title": "Fel", + "settings.general.notifications.errors.description": "Visa systemavisering när ett fel uppstår", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Spela upp ett ljud när agenten är klar eller behöver uppmärksamhet", + "settings.general.sounds.permissions.title": "Behörigheter", + "settings.general.sounds.permissions.description": "Spela upp ett ljud när en behörighet krävs", + "settings.general.sounds.errors.title": "Fel", + "settings.general.sounds.errors.description": "Spela upp ljud när ett fel uppstår", + "settings.shortcuts.title": "Kortkommandon", + "settings.shortcuts.reset.button": "Återställ till standardinställningar", + "settings.shortcuts.reset.toast.title": "Kortkommandon återställda", + "settings.shortcuts.reset.toast.description": "Kortkommandon har återställts till standardinställningarna.", + "settings.shortcuts.conflict.title": "Kortkommandot används redan", + "settings.shortcuts.conflict.description": "{{keybind}} är redan tilldelat till {{titles}}.", + "settings.shortcuts.unassigned": "Otilldelad", + "settings.shortcuts.pressKeys": "Tryck på tangenter", + "settings.shortcuts.search.placeholder": "Sök kortkommandon", + "settings.shortcuts.search.empty": "Inga kortkommandon hittades", + "settings.shortcuts.group.general": "Allmänt", + "settings.shortcuts.group.session": "Session", + "settings.shortcuts.group.navigation": "Navigering", + "settings.shortcuts.group.modelAndAgent": "Modell och agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + "settings.providers.title": "Leverantörer", + "settings.providers.description": "Leverantörsinställningar kommer att kunna konfigureras här.", + "settings.providers.section.connected": "Anslutna leverantörer", + "settings.providers.connected.empty": "Inga anslutna leverantörer", + "settings.providers.connected.environmentDescription": "Anslutna via dina miljövariabler", + "settings.providers.section.popular": "Populära leverantörer", + "settings.providers.custom.description": "Lägg till en OpenAI-kompatibel leverantör genom bas-URL.", + "settings.providers.tag.environment": "Miljö", + "settings.providers.tag.config": "Konfiguration", + "settings.providers.tag.custom": "Anpassad", + "settings.providers.tag.other": "Övrigt", + "settings.models.title": "Modeller", + "settings.models.description": "Modellinställningar kommer att kunna konfigureras här.", + "settings.agents.title": "Agenter", + "settings.agents.description": "Agentinställningar kommer att kunna konfigureras här.", + "settings.commands.title": "Kommandon", + "settings.commands.description": "Kommandoinställningarna kommer att kunna konfigureras här.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP-inställningar kommer att kunna konfigureras här.", + "settings.permissions.title": "Behörigheter", + "settings.permissions.description": "Kontrollera vilka verktyg servern kan använda som standard.", + "settings.permissions.section.tools": "Verktyg", + "settings.permissions.toast.updateFailed.title": "Det gick inte att uppdatera behörigheter", + "settings.permissions.action.allow": "Tillåt", + "settings.permissions.action.ask": "Fråga", + "settings.permissions.action.deny": "Neka", + "settings.permissions.tool.read.title": "Läs", + "settings.permissions.tool.read.description": "Läsa en fil (matchar filsökvägen)", + "settings.permissions.tool.edit.title": "Redigera", + "settings.permissions.tool.edit.description": "Ändra filer, inklusive redigeringar, skrivningar och patchar", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Matcha filer med globmönster", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Sök filinnehåll med hjälp av reguljära uttryck", + "settings.permissions.tool.list.title": "Lista", + "settings.permissions.tool.list.description": "Lista filer i en katalog", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Kör skalkommandon", + "settings.permissions.tool.task.title": "Uppgift", + "settings.permissions.tool.task.description": "Starta underagenter", + "settings.permissions.tool.skill.title": "Färdighet", + "settings.permissions.tool.skill.description": "Ladda en färdighet efter namn", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Kör språkserverfrågor", + "settings.permissions.tool.todowrite.title": "Skriv att göra-lista", + "settings.permissions.tool.todowrite.description": "Uppdatera att göra-listan", + "settings.permissions.tool.webfetch.title": "Webbhämtning", + "settings.permissions.tool.webfetch.description": "Hämta innehåll från en URL", + "settings.permissions.tool.websearch.title": "Webbsökning", + "settings.permissions.tool.websearch.description": "Sök på webben", + "settings.permissions.tool.external_directory.title": "Extern katalog", + "settings.permissions.tool.external_directory.description": "Få åtkomst till filer utanför projektkatalogen", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Upptäck upprepade verktygsanrop med identiska indata", + "session.delete.failed.title": "Det gick inte att ta bort sessionen", + "session.delete.title": "Ta bort session", + "session.delete.confirm": 'Ta bort session "{{name}}"?', + "session.delete.button": "Ta bort session", + "workspace.new": "Ny arbetsyta", + "workspace.type.local": "lokal", + "workspace.type.sandbox": "sandlåda", + "workspace.create.failed.title": "Det gick inte att skapa arbetsyta", + "workspace.delete.failed.title": "Det gick inte att ta bort arbetsytan", + "workspace.resetting.title": "Återställer arbetsytan", + "workspace.resetting.description": "Detta kan ta en minut.", + "workspace.reset.failed.title": "Det gick inte att återställa arbetsytan", + "workspace.reset.success.title": "Arbetsytan har återställts", + "workspace.reset.success.description": "Arbetsytan matchar nu standardgrenen.", + "workspace.error.stillPreparing": "Arbetsytan förbereds fortfarande", + "workspace.status.checking": "Söker efter osammanslagna ändringar...", + "workspace.status.error": "Det gick inte att verifiera git-status.", + "workspace.status.clean": "Inga osammanslagna ändringar upptäcktes.", + "workspace.status.dirty": "Osammanslagna ändringar upptäcktes i den här arbetsytan.", + "workspace.delete.title": "Ta bort arbetsyta", + "workspace.delete.confirm": 'Ta bort arbetsytan "{{name}}"?', + "workspace.delete.button": "Ta bort arbetsyta", + "workspace.reset.title": "Återställ arbetsytan", + "workspace.reset.confirm": 'Återställa arbetsytan "{{name}}"?', + "workspace.reset.button": "Återställ arbetsytan", + "workspace.reset.archived.none": "Inga aktiva sessioner kommer att arkiveras.", + "workspace.reset.archived.one": "1 session kommer att arkiveras.", + "workspace.reset.archived.many": "{{count}} sessioner kommer att arkiveras.", + "workspace.reset.note": "Detta kommer att återställa arbetsytan så att den matchar standardgrenen.", +} diff --git a/packages/app/src/i18n/tg.ts b/packages/app/src/i18n/tg.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffa08409f684aa855566b8236082ae660481ff48 --- /dev/null +++ b/packages/app/src/i18n/tg.ts @@ -0,0 +1,1164 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Файл", + "desktop.menu.edit": "Таҳрир", + "desktop.menu.view": "Намоиш", + "desktop.menu.go": "Бирав", + "desktop.menu.window": "Тиреза", + "desktop.menu.help": "Кӯмак", + "desktop.menu.checkForUpdates": "Навсозиро санҷед...", + "desktop.menu.settings": "Танзимотҳо", + "desktop.menu.reloadWebview": "Аз нав бор кунед Webview", + "desktop.menu.restart": "Оғози дубора", + "desktop.menu.exportLogs": "Содироти гузоришҳо...", + "desktop.menu.newSession": "Сессияи нав", + "desktop.menu.openProject": "Лоиҳаи кушода...", + "desktop.menu.newWindow": "Равзанаи нав", + "desktop.menu.closeWindow": "Пӯшидани Тиреза", + "desktop.menu.undo": "Бекор кардан", + "desktop.menu.redo": "Такрор кунед", + "desktop.menu.cut": "Бурида", + "desktop.menu.copy": "Нусхабардорӣ", + "desktop.menu.paste": "Часбонед", + "desktop.menu.delete": "Нобуд кунед", + "desktop.menu.selectAll": "Ҳамаро интихоб кунед", + "desktop.menu.toggleSidebar": "Гузариш кардани панели паҳлӯ", + "desktop.menu.toggleTerminal": "Терминалро иваз кунед", + "desktop.menu.toggleFileTree": "Гузариш кардани дарахти файл", + "desktop.menu.reload": "Аз нав бор кунед", + "desktop.menu.toggleDeveloperTools": "Воситаҳои таҳиякунандаро иваз кунед", + "desktop.menu.actualSize": "Андозаи воқеӣ", + "desktop.menu.zoomIn": "Масштаб кардан", + "desktop.menu.zoomOut": "Масоҳат", + "desktop.menu.toggleFullScreen": "Гузариш ба экрани пурра", + "desktop.menu.back": "Бозгашт", + "desktop.menu.forward": "Ба пеш", + "desktop.menu.previousSession": "Сессияи гузашта", + "desktop.menu.nextSession": "Сеанси оянда", + "desktop.menu.previousProject": "Лоиҳаи қаблӣ", + "desktop.menu.nextProject": "Лоиҳаи навбатӣ", + "desktop.menu.minimize": "Кам кардан", + "desktop.menu.maximize": "Максимум кардан", + "desktop.menu.documentation": "OpenCode Ҳуҷҷатҳо", + "desktop.menu.supportForum": "Форуми дастгирӣ", + "desktop.menu.shareFeedback": "Мубодилаи фикру ақида", + "desktop.menu.reportBug": "Дар бораи хато хабар диҳед", + "desktop.menu.ariaLabel": "OpenCode меню", + "desktop.updater.dialog.checkFailed.message": "Санҷиши навсозӣ ноком шуд.", + "desktop.updater.dialog.checkFailed.title": "Хатои навсозӣ", + "desktop.updater.dialog.upToDate.message": "Шумо навсозӣ доред.", + "desktop.updater.dialog.upToDate.title": "Навсозиҳо нест", + "desktop.updater.dialog.ready.message": "Навсозии {{version}} бор карда шуд. Ҳоло аз нав оғоз кунед?", + "desktop.updater.dialog.ready.title": "Навсозӣ омода аст", + "desktop.updater.dialog.restart": "Оғози дубора", + "desktop.updater.dialog.later": "Баъдтар", + "desktop.recovery.action.relaunch": "Аз нав оғоз кунед", + "desktop.recovery.action.exportLogs": "Содироти гузоришҳо", + "desktop.recovery.action.keepWaiting": "Интизор бошед", + "desktop.recovery.action.quit": "Истед", + "desktop.recovery.loadFailed": "OpenCode бор карда натавонист", + "desktop.recovery.terminated": "OpenCode тиреза ба таври ғайричашмдошт қатъ карда шуд", + "desktop.recovery.unresponsive": "OpenCode ҷавоб намедиҳад", + "desktop.recovery.unresponsive.detail": + "Шумо метавонед барномаро аз нав оғоз кунед, гузоришҳоро кушоед ё интизор шавед.", + "desktop.recovery.loadFailed.detail": "Тиреза: {{window}}\nURL: {{url}}\nХатогӣ: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Тиреза: {{window}}\nСабаб: {{reason}}\nРамз: {{code}}", + "desktop.recovery.unknown": "<номаълум>", + "desktop.dialog.chooseFolder": "Папкаро интихоб кунед", + "desktop.dialog.chooseFile": "Файлро интихоб кунед", + "desktop.dialog.saveFile": "Файлро захира кунед", + "desktop.dialog.files": "Файлҳо", + "desktop.server.local": "Сервери маҳаллӣ", + "desktop.wsl.error.windowsOnly": "WSL танҳо дар Windows дастрас аст", + "desktop.wsl.error.unavailable": "WSL дастрас нест", + "desktop.wsl.error.listInstalled": "Рӯйхати WSL дистрибюторҳои насбшуда иҷро нашуд", + "desktop.wsl.error.listOnline": "Рӯйхати WSL паҳнкунии онлайн номуваффақ шуд", + "desktop.wsl.error.executeDistro": "Фармонҳоро дар distro иҷро карда наметавонад", + "desktop.wsl.error.installWsl": "WSL насб карда нашуд", + "desktop.wsl.error.installDistro": "Насб кардани distro иҷро нашуд: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode насб карда нашуд", + "desktop.wsl.error.alreadyAdded": "{{distro}} аллакай илова карда шудааст", + "desktop.wsl.error.opencodeMissing": "opencode дар ин паҳнкунӣ насб нашудааст", + "desktop.wsl.error.opencodeCannotRun": "opencode насб шудааст, аммо иҷро карда натавонист", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode дар {{distro}} насб нашудааст", + "desktop.wsl.error.updateVersion": + "OpenCode навсозӣ анҷом ёфт, аммо {{distro}} то ҳол гузориш медиҳад {{installed}}; интизорӣ {{expected}}", + "desktop.wsl.error.noVersion": "версия нест", + "desktop.wsl.error.serverExited": "WSL сервер пас аз оғозшавӣ хориҷ шуд (рамз = {{code}} сигнал = {{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL сервер пеш аз солим шудан хориҷ шуд (рамз={{code}} сигнал ={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "Санҷиши саломатии Sidecar барои {{distro}} пас аз {{timeout}} мс ба охир расид", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} пас аз {{timeout}}мс ба охир расид", + "desktop.wsl.error.failedPort": "Ба даст овардани порт муяссар нашуд", + "desktop.picker.error.notSelected": "Файлро интихобкунанда интихоб накардааст", + "desktop.picker.error.sizeLimit": "Замимаҳои интихобшуда аз ҳадди {{limit}} МБ зиёданд", + "command.category.suggested": "Тавсия дода мешавад", + "command.category.view": "Намоиш", + "command.category.project": "Лоиҳа", + "command.category.provider": "Таъминкунанда", + "command.category.server": "Сервер", + "command.category.session": "Сессия", + "command.category.theme": "Мавзӯъ", + "command.category.language": "Забон", + "command.category.file": "Файл", + "command.category.context": "Контекст", + "command.category.terminal": "Терминал", + "command.category.model": "Модели", + "command.category.mcp": "MCP", + "command.category.agent": "Агент", + "command.category.permissions": "Иҷозатҳо", + "command.category.workspace": "Фазои корӣ", + "command.category.settings": "Танзимотҳо", + "command.logs.export": "Сабтҳои содиротӣ", + "theme.scheme.system": "Система", + "theme.scheme.light": "Нур", + "theme.scheme.dark": "Торик", + "command.sidebar.toggle": "Паҳлӯи паҳлӯро иваз кунед", + "command.project.open": "Лоиҳаи кушода", + "command.project.previous": "Лоиҳаи қаблӣ", + "command.project.next": "Лоиҳаи навбатӣ", + "command.project.index": "Гузариш ба лоиҳа {{index}}", + "command.provider.connect": "Провайдери пайвастшавӣ", + "command.server.switch": "Гузаронидани сервер", + "command.settings.open": "Танзимотро кушоед", + "command.session.previous": "Сессияи гузашта", + "command.session.next": "Сеанси оянда", + "command.session.previous.unseen": "Сеанси қаблии хонданашуда", + "command.session.next.unseen": "Сеанси навбатии нохонда", + "command.session.archive": "Сеанси архив", + "command.palette": "Палитраи фармон", + "command.theme.cycle": "Мавзӯи велосипедронӣ", + "command.theme.set": "Мавзӯъро истифода баред: {{theme}}", + "command.theme.scheme.cycle": "Схемаи рангҳои даврӣ", + "command.theme.scheme.set": "Схемаи рангро истифода баред: {{scheme}}", + "command.language.cycle": "Забони даврӣ", + "command.language.set": "Истифодаи забон: {{language}}", + "command.session.new": "Сеанси нав", + "command.file.open": "Файлро кушоед", + "command.tab.close": "Варақаро пӯшед", + "command.tab.reopenClosed": "Варақаи пӯшидаро аз нав кушоед", + "command.context.addSelection": "Интихобро ба контекст илова кунед", + "command.context.addSelection.description": "Аз файли ҷорӣ сатрҳои интихобшударо илова кунед", + "command.input.focus": "Вуруди фокус", + "command.terminal.toggle": "Терминалро иваз кунед", + "command.fileTree.toggle": "Гузариш кардани дарахти файл", + "command.review.toggle": "Гузариш аз барраси", + "command.terminal.new": "Терминали нав", + "command.terminal.new.description": "Варақаи нави терминал эҷод кунед", + "command.steps.toggle": "Қадамҳоро иваз кунед", + "command.steps.toggle.description": "Қадамҳоро барои паёми ҷорӣ нишон диҳед ё пинҳон кунед", + "command.message.previous": "Паёми қаблӣ", + "command.message.previous.description": "Ба паёми пешинаи корбар гузаред", + "command.message.next": "Паёми навбатӣ", + "command.message.next.description": "Ба паёми навбатии корбар гузаред", + "command.model.choose": "Модели интихоб кунед", + "command.model.choose.description": "Модели дигарро интихоб кунед", + "command.mcp.toggle": "Гузариш MCPс", + "command.mcp.toggle.description": "Гузариш MCPс", + "command.agent.cycle": "Агенти сикли", + "command.agent.cycle.description": "Ба агенти навбатӣ гузаред", + "command.agent.cycle.reverse": "Агенти даврӣ ба ақиб", + "command.agent.cycle.reverse.description": "Ба агенти қаблӣ гузаред", + "command.model.variant.cycle": "Кӯшиши фикрронии даврӣ", + "command.model.variant.cycle.description": "Ба сатҳи навбатии кӯшишҳо гузаред", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Промпт", + "command.permissions.autoaccept.enable": "Иҷозатҳоро худкор қабул кунед", + "command.permissions.autoaccept.disable": "Қабули худкори иҷозатҳоро қатъ кунед", + "command.workspace.toggle": "Ҷойҳои корӣ иваз кунед", + "command.workspace.toggle.description": "Дар панели паҳлӯ ҷойҳои кории сершуморро фаъол ё ғайрифаъол кунед", + "command.session.undo": "Бекор кардан", + "command.session.undo.description": "Паёми охиринро бекор кунед", + "command.session.redo": "Такрор кунед", + "command.session.redo.description": "Паёми охирини бекоршударо такрор кунед", + "command.session.compact": "Сеанси компакт", + "command.session.compact.description": "Барои кам кардани андозаи контекст ҷаласаро ҷамъбаст кунед", + "command.session.fork": "Аз паём шоха созед", + "command.session.fork.description": "Аз паёми қаблӣ сессияи нав эҷод кунед", + "command.session.share": "Сеанси мубодила", + "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": "Ягон натиҷа ёфт нашуд", + "palette.group.commands": "Фармонҳо", + "palette.group.files": "Файлҳо", + "dialog.provider.search.placeholder": "Ҷустуҷӯи провайдерҳо", + "dialog.provider.empty": "Ягон провайдер ёфт нашуд", + "dialog.provider.group.popular": "Маъмул", + "dialog.provider.group.other": "Дигар", + "dialog.provider.custom.label": "Фармоишгар OpenAI-провайдери мувофиқ", + "dialog.provider.tag.recommended": "Тавсия дода мешавад", + "dialog.provider.opencode.note": "Моделҳои интихобшуда, аз ҷумла Claude, GPT, Gemini ва ғайра", + "dialog.provider.opencode.tagline": "Моделҳои боэътимоди оптимизатсияшуда", + "dialog.provider.opencodeGo.tagline": "Обунаи арзон барои ҳама", + "dialog.provider.anthropic.note": "Дастрасии мустақим ба моделҳои Claude, аз ҷумла Pro ва Max", + "dialog.provider.copilot.note": "Моделҳои AI барои кӯмаки рамзгузорӣ тавассути GitHub Copilot", + "dialog.provider.openai.note": "GPT моделҳо барои вазифаҳои зуд ва тавонои умумии AI", + "dialog.provider.google.note": "Gemini моделҳо барои посухҳои зуд ва сохторӣ", + "dialog.provider.openrouter.note": "Ба ҳама моделҳои дастгирӣшаванда аз як провайдер дастрасӣ пайдо кунед", + "dialog.provider.vercel.note": "Дастрасии ягона ба моделҳои AI бо масири оқилона", + "dialog.model.select.title": "Модели интихоб кунед", + "dialog.model.search.placeholder": "Ҷустуҷӯи моделҳо", + "dialog.model.empty": "Натоиҷи модел нест", + "dialog.model.manage": "Моделҳоро идора кунед", + "dialog.model.manage.description": "Муайян кунед, ки кадом моделҳо дар интихоби модел пайдо мешаванд.", + "dialog.model.manage.provider.toggle": "Ҳама моделҳои {{provider}}-ро иваз кунед", + "dialog.model.unpaid.freeModels.title": "Моделҳои ройгон аз ҷониби OpenCode пешниҳодшуда", + "dialog.model.unpaid.addMore.title": "Моделҳои бештар аз провайдерҳои маъмул илова кунед", + "dialog.model.unpaid.viewMoreProviders": "Ба 70+ провайдерҳои дигар нигаред", + "dialog.provider.viewAll": "Провайдерҳои бештарро нишон диҳед", + "provider.connect.title": "Пайвастшавӣ {{provider}}", + "provider.connect.title.anthropicProMax": "Воридшавӣ бо Claude Pro/Max", + "provider.connect.selectMethod": "Усули воридшавиро барои {{provider}} интихоб кунед.", + "provider.connect.method.apiKey": "API калид", + "provider.connect.method.browser": "Браузер", + "provider.connect.method.headless": "Бесар", + "provider.connect.status.inProgress": "Иҷозатдиҳӣ идома дорад...", + "provider.connect.status.waiting": "Мунтазири иҷозат...", + "provider.connect.status.failed": "Иҷозат дода нашуд: {{error}}", + "provider.connect.apiKey.description": + "Барои пайваст кардани ҳисоби худ калиди {{provider}} API-ро ворид кунед ва моделҳои {{provider}}-ро дар OpenCode истифода баред.", + "provider.connect.apiKey.label": "{{provider}} API калид", + "provider.connect.apiKey.placeholder": "API калид", + "provider.connect.apiKey.required": "API калид лозим аст", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ба шумо дастрасӣ ба маҷмӯи мураттабшудаи моделҳои боэътимоди оптимизатсияшуда барои агентҳои рамзгузориро медиҳад.", + "provider.connect.opencodeZen.line2": + "Бо як калиди API шумо метавонед ба моделҳое мисли Claude, GPT, Gemini, GLM ва ғайра дастрасӣ пайдо кунед.", + "provider.connect.opencodeZen.visit.prefix": "Ташриф", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": "барои гирифтани калиди API худ.", + "provider.connect.oauth.code.visit.prefix": "Ташриф", + "provider.connect.oauth.code.visit.link": "ин пайванд", + "provider.connect.oauth.code.visit.suffix": + "Барои ҷамъ овардани рамзи иҷозати шумо барои пайваст кардани ҳисоби шумо ва истифода бурдани моделҳои {{provider}} дар OpenCode.", + "provider.connect.oauth.code.label": "{{method}} рамзи иҷозат", + "provider.connect.oauth.code.placeholder": "Рамзи авторизатсия", + "provider.connect.oauth.code.required": "Рамзи авторизатсия лозим аст", + "provider.connect.oauth.code.invalid": "Рамзи иҷозати нодуруст", + "provider.connect.oauth.auto.visit.prefix": "Ташриф", + "provider.connect.oauth.auto.visit.link": "ин пайванд", + "provider.connect.oauth.auto.visit.suffix": + "ва рамзи зерро ворид кунед, то ҳисоби худро пайваст кунед ва моделҳои {{provider}}-ро дар OpenCode истифода баред.", + "provider.connect.oauth.auto.confirmationCode": "Рамзи тасдиқ", + "provider.connect.toast.connected.title": "{{provider}} пайваст", + "provider.connect.toast.connected.description": "{{provider}} моделҳо ҳоло барои истифода дастрасанд.", + "provider.custom.title": "Провайдери фармоишӣ", + "provider.custom.unavailable": "Провайдерҳои фармоишӣ дар ин сервер дастрас нестанд", + "provider.custom.description.prefix": "Провайдери OpenAI-мувофиқро танзим кунед. нигаред", + "provider.custom.description.link": "ҳуҷҷатҳои конфигуратсияи провайдер", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID провайдер", + "provider.custom.field.providerID.placeholder": "провайдери ман", + "provider.custom.field.providerID.description": "Ҳарфҳои хурд, рақамҳо, дефис ё зерхат", + "provider.custom.field.name.label": "Бозтоби ном", + "provider.custom.field.name.placeholder": "Провайдери ман AI", + "provider.custom.field.baseURL.label": "Асос URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API калид", + "provider.custom.field.apiKey.placeholder": "API калид", + "provider.custom.field.apiKey.description": + "Ихтиёрӣ. Агар шумо аутентификатсияро тавассути сарлавҳаҳо идора кунед, холӣ гузоред.", + "provider.custom.models.label": "Моделҳо", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "модели ID", + "provider.custom.models.name.label": "Ном", + "provider.custom.models.name.placeholder": "Бозтоби ном", + "provider.custom.models.remove": "Модели хориҷ кунед", + "provider.custom.models.add": "Илова кардани модел", + "provider.custom.headers.label": "Сарлавҳаҳо (ихтиёрӣ)", + "provider.custom.headers.key.label": "Сарлавҳа", + "provider.custom.headers.key.placeholder": "Сарлавҳа - Ном", + "provider.custom.headers.value.label": "Арзиш", + "provider.custom.headers.value.placeholder": "арзиш", + "provider.custom.headers.remove": "Сарлавҳаро хориҷ кунед", + "provider.custom.headers.add": "Иловаи сарлавҳа", + "provider.custom.error.providerID.required": "ID провайдер лозим аст", + "provider.custom.error.providerID.format": "Ҳарфҳои хурд, рақамҳо, дефис ё зерхатро истифода баред", + "provider.custom.error.providerID.exists": "Ин ID провайдер аллакай вуҷуд дорад", + "provider.custom.error.name.required": "Номи намоиш талаб карда мешавад", + "provider.custom.error.baseURL.required": "Базаи URL талаб карда мешавад", + "provider.custom.error.baseURL.format": "Бояд бо http:// ё https:// оғоз шавад", + "provider.custom.error.required": "Талаб карда мешавад", + "provider.custom.error.duplicate": "Дубликат", + "provider.disconnect.toast.disconnected.title": "{{provider}} ҷудо карда шудааст", + "provider.disconnect.toast.disconnected.description": "{{provider}} моделҳо дигар дастрас нестанд.", + "model.tag.free": "Озод", + "model.tag.latest": "Охирин", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "матн", + "model.input.image": "тасвир", + "model.input.audio": "аудио", + "model.input.video": "видео", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Имкон медиҳад: {{inputs}}", + "model.tooltip.context.label": "Контекст", + "model.tooltip.inputs": "Вурудҳо", + "model.tooltip.model": "Модели", + "model.tooltip.provider": "Таъминкунанда", + "model.tooltip.reasoning": "Мулоҳиза", + "model.tooltip.reasoning.allowed": "Ба ақида имкон медиҳад", + "model.tooltip.reasoning.none": "Бе далел", + "model.tooltip.context": "Маҳдудияти контекст {{limit}}", + "common.search.placeholder": "Ҷустуҷӯ", + "common.goBack": "Бозгашт ба ақиб", + "common.goForward": "Ба пеш ҳаракат кунед", + "common.loading": "Бор карда мешавад", + "common.loading.ellipsis": "...", + "common.cancel": "Бекор кардан", + "common.clear": "Тоза", + "common.open": "Кушода", + "common.connect": "Пайваст кунед", + "common.disconnect": "Қатъ кунед", + "common.continue": "Давом додан", + "common.submit": "Пешниҳод кунед", + "common.save": "Захира кунед", + "common.saving": "Захира...", + "common.default": "Пешфарз", + "common.attachment": "замима", + "prompt.placeholder.shell": "Фармони shellро ворид кунед... {{example}}", + "prompt.placeholder.normal": 'Ҳама чизро пурсед... "{{example}}"', + "prompt.placeholder.simple": "Ҳар чизеро пурсед...", + "prompt.placeholder.summarizeComments": "Ҷамъбасти шарҳҳо…", + "prompt.placeholder.summarizeComment": "Ҷамъбасти шарҳ…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Промпт", + "prompt.mode.shell.exit": "esc барои баромадан", + "session.child.promptDisabled": "Сеансҳои субагентро дархост кардан мумкин нест.", + "session.child.backToParent": "Бозгашт ба ҷаласаи асосӣ.", + "prompt.example.1": "TODO-ро дар пойгоҳи код ислоҳ кунед", + "prompt.example.2": "Андозаи техникии ин лоиҳа чӣ гуна аст?", + "prompt.example.3": "Санҷишҳои шикастаро ислоҳ кунед", + "prompt.example.4": "Фаҳмонед, ки аутентификатсия чӣ гуна кор мекунад", + "prompt.example.5": "Заифии амниятро дарёфт ва ислоҳ кунед", + "prompt.example.6": "Санҷишҳои воҳиди хидматрасонии корбарро илова кунед", + "prompt.example.7": "Ин функсияро бозсозӣ кунед, то хондан бештар бошад", + "prompt.example.8": "Ин хато чӣ маъно дорад?", + "prompt.example.9": "Ба ман кӯмак кунед, ки ин масъаларо ислоҳ кунед", + "prompt.example.10": "Ҳуҷҷатҳои API эҷод кунед", + "prompt.example.11": "Оптимизатсияи дархостҳои пойгоҳи додаҳо", + "prompt.example.12": "Иловаи тасдиқи вуруд", + "prompt.example.13": "Эҷоди ҷузъи нав барои...", + "prompt.example.14": "Чӣ тавр ман ин лоиҳаро татбиқ мекунам?", + "prompt.example.15": "Рамзи маро барои таҷрибаҳои беҳтарин аз назар гузаронед", + "prompt.example.16": "Ба ин функсия коркарди хатогиҳоро илова кунед", + "prompt.example.17": "Ин намунаи регексро шарҳ диҳед", + "prompt.example.18": "Инро ба TypeScript табдил диҳед", + "prompt.example.19": "Ба қайдгириро дар тамоми пойгоҳи код илова кунед", + "prompt.example.20": "Кадом вобастагӣ кӯҳна шудаанд?", + "prompt.example.21": "Ба ман дар навиштани скрипти муҳоҷират кӯмак кунед", + "prompt.example.22": "Барои ин нуқтаи ниҳоӣ кэшро иҷро кунед", + "prompt.example.23": "Ба ин рӯйхат саҳифагузорӣ илова кунед", + "prompt.example.24": "Фармони CLI барои...", + "prompt.example.25": "Дар ин ҷо тағирёбандаҳои муҳити зист чӣ гуна кор мекунанд?", + "prompt.popover.emptyResults": "Натоиҷи мувофиқ нест", + "prompt.popover.emptyCommands": "Фармонҳои мувофиқ нест", + "prompt.dropzone.label": "Тасвирҳо, PDFс ё файлҳои матниро дар ин ҷо гузоред", + "prompt.dropzone.file.label": "Ба файли @mention гузоред", + "prompt.slash.badge.custom": "одат", + "prompt.slash.badge.skill": "махорат", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "фаъол", + "prompt.context.includeActiveFile": "Файли фаъолро дохил кунед", + "prompt.context.removeActiveFile": "Файли фаъолро аз контекст хориҷ кунед", + "prompt.context.removeFile": "Файлро аз контекст хориҷ кунед", + "prompt.action.attachFile": "Илова кардани файлҳо", + "prompt.menu.addImagesAndFiles": "Илова кардани файлҳо ва ғайра", + "prompt.menu.imagesAndFiles": "Тасвирҳо ва файлҳо", + "prompt.menu.commands": "Фармонҳо", + "prompt.menu.context": "Контекст", + "prompt.menu.shellCommand": "Shell фармон", + "prompt.attachment.remove": "Замимаро хориҷ кунед", + "prompt.action.send": "Фиристодан", + "prompt.action.stop": "Ист", + "prompt.toast.pasteUnsupported.title": "Замимаи дастгирӣнашаванда", + "prompt.toast.pasteUnsupported.description": + "Дар ин ҷо танҳо тасвирҳо, PDFс ё файлҳои матнӣ замима кардан мумкин аст.", + "prompt.toast.attachmentDuplicate.title": "Ин файл аллакай бор карда шудааст", + "prompt.toast.modelAgentRequired.title": "Агент ва моделро интихоб кунед", + "prompt.toast.modelAgentRequired.description": "Пеш аз фиристодани промпт агент ва моделро интихоб кунед.", + "prompt.toast.worktreeCreateFailed.title": "Эҷоди Git worktree муяссар нашуд", + "prompt.toast.sessionCreateFailed.title": "Эҷоди сессия натавонист", + "prompt.toast.shellSendFailed.title": "Фармони shell фиристода нашуд", + "prompt.toast.commandSendFailed.title": "Фармон фиристода нашуд", + "prompt.toast.promptSendFailed.title": "Ирсоли промпт муяссар нашуд", + "prompt.toast.promptSendFailed.description": "Сеансро дарёфт кардан ғайриимкон аст", + "dialog.mcp.title": "MCPс", + "dialog.mcp.description": "{{enabled}} аз {{total}} фаъол", + "dialog.mcp.empty": "Ягон MCP танзим карда нашудааст", + "dialog.lsp.empty": "LSPs аз намуди файлҳо худкор муайян карда мешавад", + "dialog.plugins.empty": "Плагинҳо дар opencode.json танзим карда шудаанд", + "mcp.status.connected": "пайваст", + "mcp.status.failed": "ноком шуд", + "mcp.status.needs_auth": "аутент лозим аст", + "mcp.status.disabled": "маъюб", + "mcp.auth.clickToAuthenticate": "Барои тасдиқ кардан клик кунед", + "dialog.fork.empty": "Паёме барои сохтани шоха нест", + "dialog.directory.search.placeholder": "Ҷустуҷӯи ҷузвдонҳо", + "dialog.directory.empty": "Ягон папка ёфт нашуд", + "dialog.directory.action.selectFile": "Файлро интихоб кунед", + "dialog.directory.action.selectFolder": "Папкаро интихоб кунед", + "dialog.directory.root": "Реша", + "dialog.directory.parent": "Падару модар", + "dialog.directory.readError": "Ин ҷузвдонро хондан ғайриимкон аст", + "app.server.unreachable": "Ба {{server}} расида натавонистам", + "app.server.retrying": "Кӯшиши худкор...", + "app.server.otherServers": "Дигар серверҳо", + "dialog.server.title": "Серверҳо", + "dialog.server.description": "Гузариш кунед, ки ин барнома ба кадом сервери OpenCode пайваст мешавад.", + "dialog.server.search.placeholder": "Ҷустуҷӯи серверҳо", + "dialog.server.empty": "Ҳанӯз сервер нест", + "dialog.server.add.title": "Илова кардани сервер", + "dialog.server.add.url": "Суроғаи сервер", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Ба сервер пайваст шуда натавонист", + "dialog.server.add.checking": "Санҷиш...", + "dialog.server.add.button": "Илова кардани сервер", + "dialog.server.add.name": "Номи сервер (ихтиёрӣ)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Номи корбар (ихтиёрӣ)", + "dialog.server.add.usernamePlaceholder": "номи корбар", + "dialog.server.add.password": "Рамз (ихтиёрӣ)", + "dialog.server.add.passwordPlaceholder": "парол", + "dialog.server.edit.title": "Серверро таҳрир кунед", + "dialog.server.default.title": "Сервери пешфарз", + "dialog.server.default.description": + "Ба ҷои оғози сервери маҳаллӣ ҳангоми оғози барнома ба ин сервер пайваст шавед. Аз нав оғоз карданро талаб мекунад.", + "dialog.server.default.none": "Ягон сервер интихоб нашудааст", + "dialog.server.default.set": "Сервери ҷорӣро ҳамчун пешфарз таъин кунед", + "dialog.server.default.clear": "Тоза", + "dialog.server.action.remove": "Серверро нест кунед", + "dialog.server.menu.edit": "Таҳрир", + "dialog.server.menu.default": "Ҳамчун пешфарз таъин кунед", + "dialog.server.menu.defaultRemove": "Пешфарзро хориҷ кунед", + "dialog.server.menu.delete": "Нобуд кунед", + "dialog.server.current": "Сервери ҷорӣ", + "dialog.server.status.default": "Пешфарз", + "wsl.server.add": "Илова кардани сервер WSL", + "wsl.server.addShort": "Илова кардан WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL сервер", + "wsl.server.retryStart": "Оғози дубора кӯшиш кунед", + "wsl.server.updating": "Навсозӣ...", + "wsl.onboarding.step.distro": "Дистро интихоб кунед", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Санҷиш WSL...", + "wsl.onboarding.restartRequired": "Windows барои анҷом додани насби WSL аз нав оғоз кардан лозим аст.", + "wsl.onboarding.ready": "WSL тайёр аст.", + "wsl.onboarding.required": "WSL давом додан лозим аст.", + "wsl.onboarding.checkingDistros": "Санҷиши дистрибюторҳо...", + "wsl.onboarding.installingDistro": "Насб карда мешавад {{distro}}...", + "wsl.onboarding.checkingDistro": "Санҷиш {{distro}}...", + "wsl.onboarding.listingDistros": "Рӯйхати дистрибюсияҳо...", + "wsl.onboarding.distroReady": "{{distro}} тайёр аст.", + "wsl.onboarding.distroNotInstalled": "{{distro}} хануз насб карда нашудааст.", + "wsl.onboarding.openDistroOnce": "Барои анҷом додани танзим {{distro}} -ро як маротиба кушоед.", + "wsl.onboarding.finishingDistro": "Анҷоми насб барои {{distro}}.", + "wsl.onboarding.pickDistro": "Дистро-ро интихоб кунед ё якеро дар зер насб кунед.", + "wsl.onboarding.checkingOpencode": "Санҷиш OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Санҷиш OpenCode дар {{distro}}...", + "wsl.onboarding.updatingOpencode": "Навсозӣ OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Навсозии OpenCode дар {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Навсозии OpenCode дар {{distro}}.", + "wsl.onboarding.updateOpencode": "Навсозӣ OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode дар {{distro}} тайёр аст.", + "wsl.onboarding.opencodeReady": "OpenCode тайёр аст.", + "wsl.onboarding.installOpencodeIn": "OpenCode дар {{distro}} насб кунед.", + "wsl.onboarding.installOpencode": "Насб кунед OpenCode", + "wsl.onboarding.chooseDistroFirst": "Аввал як дистрибюторро интихоб кунед.", + "wsl.onboarding.loadFailed": "Ҳолати WSL бор карда нашуд.", + "wsl.onboarding.loading": "Бор шуда истодааст...", + "wsl.onboarding.installedDistros": "Дистроҳои насбшуда", + "wsl.onboarding.checkAgain": "Боз санҷед", + "wsl.onboarding.distroStatus.ready": "Тайёр", + "wsl.onboarding.distroStatus.checking": "Санҷиш...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode насб нашудааст", + "wsl.onboarding.distroStatus.missingTools": "Бедарак bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Дастгирнашаванда · Истифода WSL 2", + "wsl.onboarding.needAnotherDistro": "Дистрои дигар лозим аст?", + "wsl.onboarding.needAnotherDistroHint": "Аз каталоги WSL тақсимоти Linux насб кунед", + "wsl.onboarding.wslNotInstalled.title": "WSL насб нашудааст", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Windows Subsystem for Linux) лозим аст, то OpenCode сервери WSL илова карда шавад", + "wsl.onboarding.wslUnavailable.title": "WSL дастрас нест", + "wsl.onboarding.wslUnavailable.description": "OpenCode WSL-ро дар ин мошин тафтиш карда натавонист.", + "wsl.onboarding.installWsl": "Насб кунед WSL", + "wsl.onboarding.windowsRestartRequired": + "Барои анҷом додани насби WSL Windows-ро аз нав оғоз кунед, сипас OpenCode-ро боз кунед.", + "wsl.onboarding.next": "Баъдӣ", + "wsl.onboarding.refresh": "Навсозӣ", + "wsl.onboarding.allDistrosAdded": "Ҳама дистрибюторҳои насбшуда аллакай илова карда шудаанд.", + "wsl.onboarding.noDistros": "То ҳол ягон паҳнкунӣ ошкор нашудааст.", + "wsl.onboarding.install": "Насб кунед", + "wsl.onboarding.installing": "Насб карда мешавад...", + "wsl.onboarding.installDistro": "Дистро насб кунед", + "wsl.onboarding.searchDistros": "Ҷустуҷӯи дистрибюторҳо", + "wsl.onboarding.wsl2Required": "WSL 2 лозим аст.", + "wsl.onboarding.toolsRequired": "Ин дистро лозим аст bash ва curl.", + "wsl.onboarding.openTerminal": "Терминали кушода", + "wsl.onboarding.path": "Роҳ: {{path}}", + "wsl.onboarding.notFound": "дарёфт нашуд", + "wsl.onboarding.version": "Версия: {{version}}", + "wsl.onboarding.unknown": "номаълум", + "wsl.onboarding.desktopVersion": "мизи корӣ {{version}}", + "wsl.onboarding.versionMismatch": "Версияи насбшуда ба версияи барномаи мизи корӣ мувофиқат намекунад.", + "wsl.onboarding.adding": "Илова кардан...", + "help.tabs.toast.ariaLabel": "Муаррифии ҷадвалҳо. Кор ва ҷаласаҳои фаъоли худро бо ҷадвалҳо ташкил кунед", + "help.tabs.toast.dismiss": "Бекор кардани маълумоти ҷадвалҳо", + "help.tabs.title": "Муаррифии ҷадвалҳо", + "help.tabs.description": "Кор ва ҷаласаҳои фаъоли худро бо ҷадвалҳо ташкил кунед", + "help.tabs.date": "14 июл", + "help.tabs.introduction": "OpenCode Desktop ҳоло дар атрофи ҷадвалҳо сохта шудааст.", + "help.tabs.sessions": + "Сеанси навро дар ҷадвал оғоз кунед ё сеанси мавҷударо аз ягон лоиҳаи худ кушоед. Вақте ки шумо ягон чизи навро оғоз мекунед, ҷадвали нав кушоед ва пас аз анҷоми он онро пӯшед.", + "help.tabs.organize": + "Кушодани якчанд ҷадвалҳо ташкили ҷаласаҳои фаъоли шуморо осонтар мекунад. Номи ҷадвалҳоро ба чизи фаромӯшнашаванда тағир диҳед, агар шумо ният доред, ки онҳоро дар гирду атроф нигоҳ доред.", + "help.tabs.home": + "Шумо тамоми сессияҳо ва лоиҳаҳои худро дар экрани нави асосӣ пайдо мекунед. Интихоби сессия онро дар ҷадвал мекушояд.", + "help.tabs.persistence": "Вақте ки шумо барномаро аз нав мекушоед, ҷадвалҳои шумо ҳанӯз кушода мешаванд.", + "help.tabs.worktrees": + "Тарҳи нав то ҳол Git Worktrees-ро дастгирӣ намекунад, он ба зудӣ меояд. Пас, агар шумо хоҳед, ки истифодаи тарҳи қаблиро идома диҳед, шумо метавонед дар Танзимот байни тарҳҳо гузаред. Танҳо дар хотир доред, ки тарҳи нав пас аз чанд ҳафта доимӣ хоҳад шуд.", + "server.row.noUsername": "номи корбар нест", + "dialog.project.edit.title": "Таҳрири лоиҳа", + "dialog.project.edit.name": "Ном", + "dialog.project.edit.icon": "Нишона", + "dialog.project.edit.icon.alt": "Нишонаи лоиҳа", + "dialog.project.edit.icon.hint": "Тасвирро клик кунед ё кашола кунед", + "dialog.project.edit.icon.recommended": "Тавсия дода мешавад: 128x128px", + "dialog.project.edit.color": "Ранги", + "dialog.project.edit.color.select": "Ранги {{color}}-ро интихоб кунед", + "dialog.project.edit.worktree.startup": "Скрипти оғозёбии фазои корӣ", + "dialog.project.edit.worktree.startup.description": "Пас аз эҷоди фазои нави корӣ (Git worktree) иҷро мешавад.", + "dialog.project.edit.worktree.startup.placeholder": "масалан. bun install", + "dialog.releaseNotes.action.getStarted": "Оғоз кунед", + "dialog.releaseNotes.action.next": "Баъдӣ", + "dialog.releaseNotes.action.hideFuture": "Инҳоро дар оянда нишон надиҳед", + "dialog.releaseNotes.media.alt": "Намоиши пешнамоиш", + "context.breakdown.title": "Тақсими контекст", + "context.breakdown.note": + 'Тақрибан тақсимоти аломатҳои воридотӣ. "Дигар" таърифҳои асбобҳо ва хароҷотро дар бар мегирад.', + "context.breakdown.system": "Система", + "context.breakdown.user": "Истифодабаранда", + "context.breakdown.assistant": "Ёрдамчӣ", + "context.breakdown.tool": "Зангҳои асбоб", + "context.breakdown.other": "Дигар", + "context.systemPrompt.title": "Промпти система", + "context.rawMessages.title": "Паёмҳои хом", + "context.export.session": "Сеанси содирот", + "context.stats.session": "Сессия", + "context.stats.messages": "Паёмҳо", + "context.stats.provider": "Таъминкунанда", + "context.stats.model": "Модели", + "context.stats.limit": "Маҳдудияти контекст", + "context.stats.totalTokens": "Токенҳои умумӣ", + "context.stats.usage": "Истифода", + "context.stats.inputTokens": "Токенҳои воридотӣ", + "context.stats.outputTokens": "Токенҳои баромад", + "context.stats.reasoningTokens": "Нишонаҳои мулоҳиза", + "context.stats.cacheTokens": "Токенҳои кэш (хондан/навиштан)", + "context.stats.userMessages": "Паёмҳои корбар", + "context.stats.assistantMessages": "Паёмҳои ассистент", + "context.stats.totalCost": "Арзиши умумӣ", + "context.stats.sessionCreated": "Сессия сохта шуд", + "context.stats.lastActivity": "Фаъолияти охирин", + "context.usage.tokens": "Токенҳо", + "context.usage.usage": "Истифода", + "context.usage.cost": "Арзиш", + "context.usage.clickToView": "Барои дидани контекст клик кунед", + "context.usage.view": "Истифодаи контекстро бинед", + "language.en": "англисӣ", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Германия", + "language.es": "Испания", + "language.fr": "Фаронса", + "language.da": "Данск", + "language.ja": "日本語", + "language.pl": "Польски", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Норск", + "language.br": "Português (Бразилия)", + "language.bs": "Босански", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Забон", + "toast.language.description": "Гузариш ба {{language}}", + "toast.theme.title": "Мавзӯъ иваз карда шуд", + "toast.scheme.title": "Схемаи ранг", + "toast.workspace.enabled.title": "Ҷойҳои корӣ фаъол карда шуданд", + "toast.workspace.enabled.description": "Ҳоло дар панели паҳлӯ якчанд Git worktree нишон дода шудаанд", + "toast.workspace.disabled.title": "Ҷойҳои корӣ ғайрифаъол", + "toast.workspace.disabled.description": "Дар панели паҳлӯ танҳо Git worktree-и асосӣ нишон дода мешавад", + "toast.permissions.autoaccept.on.title": "Қабули худкор иҷозатҳо", + "toast.permissions.autoaccept.on.description": "Дархостҳои иҷозат ба таври худкор тасдиқ карда мешаванд", + "toast.permissions.autoaccept.off.title": "Қабули худкори иҷозатҳоро қатъ кард", + "toast.permissions.autoaccept.off.description": "Дархостҳои иҷозат тасдиқро талаб мекунанд", + "toast.model.none.title": "Ягон модел интихоб нашудааст", + "toast.model.none.description": "Барои ҷамъбасти ин сессия провайдерро пайваст кунед", + "toast.file.loadFailed.title": "Файлро бор карда натавонист", + "toast.file.listFailed.title": "Рӯйхати файлҳо муяссар нашуд", + "toast.context.noLineSelection.title": "Интихоби сатр нест", + "toast.context.noLineSelection.description": "Аввал дар ҷадвали файл диапазони сатрро интихоб кунед.", + "toast.session.share.copyFailed.title": "URL ба буфер нусхабардорӣ карда нашуд", + "toast.session.share.success.title": "Сеанс муштарак", + "toast.session.share.success.description": "Мубодилаи URL ба буфер нусхабардорӣ карда шуд!", + "toast.session.share.failed.title": "Мубодилаи сеанс", + "toast.session.share.failed.description": "Ҳангоми мубодилаи сессия хатогӣ рух дод", + "toast.session.unshare.success.title": "Сеанс тақсим нашудааст", + "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": "Навсозии дастрас", + "toast.update.description": "Версияи нави OpenCode ({{version}}) ҳоло барои насб дастрас аст.", + "toast.update.action.installRestart": "Насб кунед ва аз нав оғоз кунед", + "toast.update.action.notYet": "Ҳанӯз не", + "error.page.title": "Чизе хато рафт", + "error.page.description": "Ҳангоми боркунии барнома хатогӣ рӯй дод.", + "error.page.description.localServerStartup": "Ҳангоми оғоз кардани сервери маҳаллӣ хатогӣ рӯй дод.", + "error.page.details.label": "Тафсилоти хато", + "error.page.action.restart": "Оғози дубора", + "error.page.action.report": "Гузориши хато", + "error.page.action.reported": "Хатогӣ гузориш дода шуд", + "error.page.action.exportLogs": "Содироти гузоришҳо", + "error.page.action.checking": "Санҷиш...", + "error.page.action.checkUpdates": "Барои навсозиҳо санҷед", + "error.page.action.updateTo": "Навсозӣ ба {{version}}", + "error.page.circular": "[Даврача]", + "error.page.report.prefix": "Лутфан дар бораи ин хато ба дастаи OpenCode хабар диҳед", + "error.page.report.discord": "дар Discord", + "error.page.version": "Версия: {{version}}", + "error.dev.rootNotFound": + "Элементи решавӣ ёфт нашуд. Оё шумо онро ба index.html илова карданро фаромӯш кардаед? Ё шояд атрибути id хато навишта шудааст?", + "error.serverSync.connectFailed": "Ба сервер пайваст шуда натавонист. Оё сервере дар `{{url}}` кор мекунад?", + "error.serverSDK.noServerAvailable": "Ҳеҷ сервер дастрас нест", + "error.serverSDK.serverNotAvailable": "Сервер дастрас нест", + "error.childStore.persistedCacheCreateFailed": "Эҷоди кэши доимӣ муяссар нашуд", + "error.childStore.persistedProjectMetadataCreateFailed": "Эҷоди метамаълумоти доимии лоиҳа натавонист", + "error.childStore.persistedProjectIconCreateFailed": "Эҷоди нишонаи лоиҳаи доимӣ ноком шуд", + "error.childStore.storeCreateFailed": "Эҷоди мағоза ноком шуд", + "directory.error.invalidUrl": "Феҳристи нодуруст дар URL.", + "error.chain.unknown": "Хатогии номаълум", + "error.server.invalidConfiguration": "Конфигуратсияи нодуруст", + "error.chain.causedBy": "Бо сабаби:", + "error.chain.apiError": "API хато", + "error.chain.status": "Статус: {{status}}", + "error.chain.retryable": "Кӯшиши такрорӣ: {{retryable}}", + "error.chain.responseBody": "Мақомоти вокуниш:\n{{body}}", + "error.chain.didYouMean": "Оё шумо дар назар доштед: {{suggestions}}", + "error.chain.modelNotFound": "Модели ёфт нашуд: {{provider}}/{{model}}", + "error.chain.checkConfig": "Номҳои провайдери конфигуратсияи худро (opencode.json) санҷед", + "error.chain.mcpFailed": + 'MCP сервери "{{name}}" ноком шуд. Дар хотир доред, ки OpenCode тасдиқи MCP-ро ҳанӯз дастгирӣ намекунад.', + "error.chain.providerAuthFailed": "Аутентификатсияи провайдер ноком шуд ({{provider}}): {{message}}", + "error.chain.providerInitFailed": 'Провайдери "{{provider}}" оғоз карда нашуд. Санҷиши эътимоднома ва конфигуратсия.', + "error.chain.configJsonInvalid": "Файли танзимот дар {{path}} эътибор надорад JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Файли танзимот дар {{path}} эътибор надорад JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Феҳристи "{{dir}}" дар {{path}} эътибор надорад. Номи директорияро ба "{{suggestion}}" иваз кунед ё онро хориҷ кунед. Ин як хатои маъмул аст.', + "error.chain.configFrontmatterError": "frontmatter дар {{path}} таҳлил карда нашуд:\n{{message}}", + "error.chain.configInvalid": "Файли танзимот дар {{path}} нодуруст аст", + "error.chain.configInvalidWithMessage": "Файли танзимот дар {{path}} нодуруст аст: {{message}}", + "notification.permission.title": "Иҷозат талаб карда мешавад", + "notification.permission.description": "{{sessionTitle}} дар {{projectName}} ичозат лозим аст", + "notification.question.title": "Савол", + "notification.question.description": "{{sessionTitle}} дар {{projectName}} савол дорад", + "notification.action.goToSession": "Ба сессия равед", + "notification.session.responseReady.title": "Ҷавоб омода", + "notification.session.error.title": "Хатогии сессия", + "notification.session.error.fallbackDescription": "Хатогӣ рӯй дод", + "home.recentProjects": "Лоиҳаҳои охирин", + "home.empty.title": "Лоиҳаҳои охирин нест", + "home.empty.description": "Бо кушодани лоиҳаи маҳаллӣ оғоз кунед", + "home.title": "Хона", + "home.projects": "Лоиҳаҳо", + "home.project.add": "Илова кардани лоиҳа", + "home.recentlyClosed": "Ба наздикӣ баста", + "home.server.collapse": "Лоиҳаҳои серверро вайрон кунед", + "home.server.expand": "Лоиҳаҳои серверро васеъ кунед", + "home.sessions.search.placeholder": "Сеансҳои ҷустуҷӯ", + "home.sessions.search.placeholder.scoped": "Ҷустуҷӯи сессияҳо дар {{scope}}", + "home.sessions.search.sessions": "Сеансхо", + "home.sessions.search.noResults": "Ягон сессия барои {{query}} ёфт нашуд", + "home.sessions.empty": "Дар ин ҷо ҳанӯз чизе нест", + "home.sessions.empty.description": "Барои оғоз кардани сессия сессия эҷод кунед", + "home.sessions.group.today": "Имруз", + "home.sessions.group.yesterday": "Дируз", + "home.sessions.group.older": "калонсолтар", + "home.providerTip": + "Барои истифодаи моделҳои дигар, аз ҷумла Claude, GPT, Gemini ва ғайра ба 75+ провайдерҳо пайваст шавед", + "session.tab.session": "Сессия", + "session.tab.review": "Барраси", + "session.tab.context": "Контекст", + "session.tab.unknown": "Сеанси номаълум", + "session.panel.reviewAndFiles": "Барраси ва файлҳо", + "session.error.notFound": "Ин сессияро ёфтан мумкин нест", + "session.error.notFound.description": "Ин ҷадвал ба сессияе ишора мекунад, ки дигар дар ин сервер вуҷуд надорад.", + "session.error.notFound.closeTab": "Варақаро пӯшед", + "session.error.serverConnection": "Ба ин сервер пайваст шудан мумкин нест", + "session.review.filesChanged": "Файлҳо тағир дода шуданд {{count}}", + "session.review.change.one": "Тағйир", + "session.review.change.other": "Тағйирот", + "session.review.loadingChanges": "Тағйирот бор карда мешавад...", + "session.review.empty": "Дар ин иҷлосия ҳанӯз тағирот нест", + "session.review.noVcs": "Не Git Системаи Идоракунии Версия ошкор нашудааст, тағирот нишон дода нашудааст", + "session.review.noVcs.createGit.title": "Анбори Git эҷод кунед", + "session.review.noVcs.createGit.description": "Тағиротро дар ин лоиҳа пайгирӣ кунед, баррасӣ кунед ва бекор кунед", + "session.review.noVcs.createGit.actionLoading": "Эҷоди Git анбор...", + "session.review.noVcs.createGit.action": "Эҷоди Git репозиторий", + "session.review.noSnapshot": + "Дар конфигуратсия пайгирии аксҳо ғайрифаъол аст, аз ин рӯ тағиротҳои сессия дастрас нестанд", + "session.review.noChanges": "Тағйирот нест", + "session.review.noUncommittedChanges": "Ҳанӯз ягон тағйироти беэътиборнашуда", + "session.review.noBranchChanges": "То ҳол ягон филиал тағир наёфтааст", + "session.files.selectToOpen": "Барои кушодан файлеро интихоб кунед", + "session.files.all": "Ҳама файлҳо", + "session.files.empty": "Файл нест", + "session.files.binaryContent": "Файли дуӣ (мӯҳтаво намоиш дода намешавад)", + "session.messages.renderEarlier": "Паёмҳои қаблиро пешниҳод кунед", + "session.messages.loadingEarlier": "Боркунии паёмҳои қаблӣ...", + "session.messages.loadEarlier": "Паёмҳои қаблиро бор кунед", + "session.messages.loading": "Паёмҳо бор карда мешаванд...", + "session.messages.jumpToLatest": "Ба охирин гузаред", + "session.context.addToContext": "Ба контекст {{selection}} илова кунед", + "session.todo.title": "Тодос", + "session.todo.collapse": "Фурӯпошӣ", + "session.todo.expand": "Васеъ кунед", + "session.todo.progress": "{{done}} аз {{total}} тодо анчом ёфт", + "session.question.progress": "{{current}} аз {{total}} саволҳо", + "session.question.minimize": "Саволро кам кунед", + "session.question.restore": "Саволро барқарор кунед", + "session.question.pending.one": "{{count}} саволи интизорӣ", + "session.question.pending.other": "{{count}} саволҳои интизорӣ", + "session.followupDock.summary.one": "{{count}} паёми навбатӣ", + "session.followupDock.summary.other": "{{count}} паёмҳои навбатӣ", + "session.followupDock.sendNow": "Ҳоло фиристед", + "session.followupDock.edit": "Таҳрир", + "session.followupDock.collapse": "Паёмҳои навбатдорро қатъ кунед", + "session.followupDock.expand": "Паёмҳои навбатдорро васеъ кунед", + "session.revertDock.summary.one": "{{count}} Паём баргашт", + "session.revertDock.summary.other": "{{count}} паёмҳоро баргардонд", + "session.revertDock.collapse": "Паёмҳои баргардонидашуда", + "session.revertDock.expand": "Паёмҳои баргардонидашударо васеъ кунед", + "session.revertDock.restore": "Барқарор кардани паём", + "session.new.title": "Ҳар чизеро созед", + "session.new.project.new": "Лоиҳаи нав", + "session.new.project.search": "Ҷустуҷӯи лоиҳаҳо", + "session.new.project.add": "Илова кардани лоиҳа", + "session.new.worktree.main": "Филиали асосӣ", + "session.new.worktree.mainWithBranch": "Филиали асосӣ ({{branch}})", + "session.new.worktree.create": "Git worktree-и нав эҷод кунед", + "session.new.workspace.runIn": "Иҷлосия дар", + "session.new.workspace.triggerLocal": "маҳаллӣ", + "session.new.workspace.local": "Анбори маҳаллӣ", + "session.new.workspace.existing": "Фазои корӣ…", + "session.new.git.none": "Не Git", + "session.new.lastModified": "Охирин тағир дода шудааст", + "session.header.search.placeholder": "Ҷустуҷӯ {{project}}", + "session.header.searchFiles": "Ҷустуҷӯи файлҳо", + "session.header.openIn": "Кушодан дар", + "session.header.open.action": "Кушодан {{app}}", + "session.header.open.ariaLabel": "Кушодан дар {{app}}", + "session.header.open.menu": "Вариантҳоро кушоед", + "session.header.open.copyPath": "Роҳи нусхабардорӣ", + "session.header.open.finder": "Ҷустуҷӯ", + "session.header.open.fileExplorer": "Роҳбалади файлҳо", + "session.header.open.fileManager": "Мудири файлҳо", + "session.header.reveal.finder": "Дар Finder ошкор кунед", + "session.header.reveal.fileExplorer": "Дар File Explorer ошкор кунед", + "session.header.reveal.containingFolder": "Папкаи дорои ҷузвдонро кушоед", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Терминал", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Статус", + "status.popover.ariaLabel": "Конфигуратсияҳои сервер", + "status.popover.tab.servers": "Серверҳо", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Плагинҳо", + "status.popover.action.manageServers": "Серверҳоро идора кунед", + "session.share.popover.title": "Дар веб нашр кунед", + "session.share.popover.description.shared": "Ин ҷаласа дар веб оммавӣ аст. Он барои ҳама бо истинод дастрас аст.", + "session.share.popover.description.unshared": + "Сеансро дар интернет ба таври оммавӣ мубодила кунед. Он барои ҳар касе, ки пайванд дорад, дастрас хоҳад буд.", + "session.share.action.share": "Мубодила", + "session.share.action.publish": "Нашр кунед", + "session.share.action.publishing": "Интишор...", + "session.share.action.unpublish": "Нашр кардан", + "session.share.action.unpublishing": "Нашр карда мешавад...", + "session.share.action.view": "Намоиш", + "session.share.copy.copied": "Нусхабардорӣ", + "session.share.copy.copyLink": "Истиноди нусхабардорӣ", + "lsp.tooltip.none": "Не LSP сервер", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Промпт бор шуда истодааст...", + "terminal.loading": "Терминал бор карда мешавад...", + "terminal.title": "Терминал", + "terminal.title.numbered": "Терминал {{number}}", + "terminal.close": "Терминалро пӯшед", + "terminal.connectionLost.title": "Пайвастшавӣ гум шуд", + "terminal.connectionLost.abnormalClose": "WebSocket ғайримуқаррарӣ баста шуд: {{code}}", + "terminal.connectionLost.description": + "Пайвасти терминал қатъ шуд. Ин метавонад ҳангоми аз нав оғоз шудани сервер рӯй диҳад.", + "terminal.connectTicket.csrfError": + "PTY пайвастшавӣ чиптаи аз рӯи пайдоиш рад ё CSRF чек. Танзимоти сервер CORS-ро санҷед.", + "terminal.connectTicket.statusError": "PTY чиптаи пайвастшавӣ бо {{status}} ноком шуд", + "titlebar.update": "Навсозӣ", + "titlebar.updateVersion": "Навсозӣ {{version}}", + "common.closeTab": "Варақаро пӯшед", + "common.dismiss": "Рад кардан", + "common.moreCountSuffix": "(+{{count}} бештар)", + "common.requestFailed": "Дархост иҷро нашуд", + "common.moreOptions": "Имконоти бештар", + "common.learnMore": "Бисёр омӯхтан", + "common.rename": "Номи дигар", + "common.export": "Содирот", + "common.reset": "Бозсозӣ", + "common.archive": "Архив", + "common.delete": "Нобуд кунед", + "common.close": "Пӯшед", + "common.edit": "Таҳрир", + "common.loadMore": "Бештар бор кунед", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "номаълум", + "common.time.justNow": "Ҳамин ҳоло", + "common.time.minutesAgo.short": "{{count}}дакика пеш", + "common.time.hoursAgo.short": "{{count}}соат пеш", + "common.time.daysAgo.short": "{{count}}д пеш", + "sidebar.menu.toggle": "Менюи гузариш", + "sidebar.nav.projectsAndSessions": "Лоиҳаҳо ва ҷаласаҳо", + "sidebar.settings": "Танзимотҳо", + "sidebar.help": "Кӯмак", + "sidebar.workspaces.enable": "Фазои кории фаъолро фаъол созед", + "sidebar.workspaces.disable": "Ҷойҳои кории худро ғайрифаъол кунед", + "sidebar.gettingStarted.title": "Сар кардани кор", + "sidebar.gettingStarted.line1": "OpenCode моделҳои ройгонро дар бар мегирад, то шумо метавонед фавран оғоз кунед.", + "sidebar.gettingStarted.line2": + "Ҳар як провайдерро барои истифодаи моделҳо пайваст кунед, Inc. Claude, GPT, Gemini ва гайра.", + "sidebar.project.recentSessions": "Сеансхои охирин", + "sidebar.project.viewAllSessions": "Ҳама сессияҳоро бинед", + "sidebar.project.clearNotifications": "Огоҳиҳоро тоза кунед", + "sidebar.empty.title": "Ягон лоиҳа кушода нест", + "sidebar.empty.description": "Барои оғоз кардани лоиҳа лоиҳа кушоед", + "debugBar.ariaLabel": "Ташхиси самаранокии рушд", + "debugBar.na": "не", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Гузариши охирини хатсайри анҷомёфта ба саҳифаи сеанс, аз оғози роутер то рангкунии аввал пас аз ҳал шудани он чен карда мешавад.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Гузаронидани кадрҳо дар як сония дар 5 сонияи охир.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Бадтарин вақти чаҳорчӯба дар 5 сонияи охир.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Чорчубахо зиёда аз 32ms дар 5 сонияи охир.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": + "Вақти басташуда ва ҳисоб кардани вазифаҳои тӯлонӣ дар 5 сонияи охир. Вазифаи максималӣ: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Бадтарин таъхири вуруд дар 5 сонияи охир.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "Давомнокии тахминии ҳамкорӣ дар 5 сонияи охир. Ин ба INP монанд аст, на ба Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Тағйирёбии тарҳбандии маҷмӯӣ барои умри замимаи ҷорӣ.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Тӯдаи истифодашудаи JS против маҳдудияти теппа. танҳо Chromium.", + "debugBar.mem.tip": "Тӯдаи истифодашудаи JS против маҳдудияти теппа. {{used}} аз {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Сабкҳои фокусро ба ҳама унсурҳои интерактивӣ маҷбур кунед", + "debugBar.focus.on": "ДАР БОРАИ", + "debugBar.focus.off": "ХОМӮШ", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Бе тағир додани забони интихобшуда самти тарҳбандии пурраи барномаро маҷбур кунед", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Мизи корӣ", + "settings.section.server": "Сервер", + "settings.tab.general": "Генерал", + "settings.tab.shortcuts": "Миёнабурҳо", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL интеграция", + "settings.desktop.wsl.description": "Сервери OpenCode-ро дар WSL дар Windows иҷро кунед.", + "settings.general.section.appearance": "Намуди зоҳирӣ", + "settings.general.section.advanced": "Мукаммал", + "settings.general.section.notifications": "Огоҳиҳои система", + "settings.general.section.updates": "Навсозиҳо", + "settings.general.section.sounds": "Таъсири садо", + "settings.general.section.feed": "Ғизо", + "settings.general.section.display": "Намоиш", + "settings.general.row.language.title": "Забон", + "settings.general.row.language.description": "Забони намоишро барои OpenCode иваз кунед", + "settings.general.row.shell.title": "Терминал shell", + "settings.general.row.shell.description": "Shell воситаи терминал ва агент истифода мешавад", + "settings.general.row.shell.autoDefault": "Авто (Пешфарз)", + "settings.general.row.shell.terminalOnly": "танҳо терминал", + "settings.general.row.appearance.title": "Намуди зоҳирӣ", + "settings.general.row.appearance.description": + "Фармоиш диҳед, ки чӣ гуна OpenCode дар дастгоҳи шумо намуди зоҳирӣ дорад", + "settings.general.row.colorScheme.title": "Схемаи ранг", + "settings.general.row.colorScheme.description": + "Интихоб кунед, ки OpenCode аз рӯи система, равшанӣ ё мавзӯи торик пайравӣ мекунад", + "settings.general.row.theme.title": "Мавзӯъ", + "settings.general.row.theme.description": "Чӣ тавр OpenCode мавзӯъро танзим кунед.", + "settings.general.row.font.title": "Шрифти код", + "settings.general.row.font.description": "Ҳарферо, ки дар блокҳои код истифода мешавад, танзим кунед", + "settings.general.row.terminalFont.title": "Шрифти терминал", + "settings.general.row.terminalFont.description": "Ҳарферо, ки дар терминал истифода мешавад, танзим кунед", + "settings.general.row.uiFont.title": "Шрифти UI", + "settings.general.row.uiFont.description": "Ҳарферо, ки дар тамоми интерфейс истифода мешавад, танзим кунед", + "settings.general.row.followup.title": "Рафтори пайгирӣ", + "settings.general.row.followup.description": + "Интихоб кунед, ки паёмҳои пайгирӣ фавран идора мешаванд ё дар навбат интизор мешаванд", + "settings.general.row.followup.option.queue": "Навбат", + "settings.general.row.followup.option.steer": "Роҳбарӣ кардан", + "settings.general.row.showFileTree.title": "Дарахти файл", + "settings.general.row.showFileTree.description": "Панели дарахти файлро дар сессияҳо нишон диҳед", + "settings.general.row.showNavigation.title": "Назорати навигатсионӣ", + "settings.general.row.showNavigation.description": "Тугмаҳои қафо ва пешро дар сатри унвони мизи корӣ нишон диҳед", + "settings.general.row.showSearch.title": "Палитраи фармон", + "settings.general.row.showSearch.description": "Тугмаи палитраи ҷустуҷӯ ва фармонро дар сатри унвон нишон диҳед", + "settings.general.row.showTerminal.title": "Терминал", + "settings.general.row.showTerminal.description": "Тугмаи терминалро дар сатри унвони мизи корӣ нишон диҳед", + "settings.general.row.showStatus.title": "Ҳолати сервер", + "settings.general.row.showStatus.description": "Тугмаи ҳолати серверро дар сатри унвон нишон диҳед", + "settings.general.row.mobileTitlebarBottom.title": "Навигатсия дар поён", + "settings.general.row.mobileTitlebarBottom.description": + "Сатри унвон ва ҷадвалҳои сессияро дар поёни экран дар мобилӣ ҷойгир кунед", + "settings.general.row.showCustomAgents.title": "Намоиши агент", + "settings.general.row.showCustomAgents.description": + "Гузариш байни агентҳо дар оҳангсоз. Ҳангоми пинҳон, пешфарз ба Сохтани агент.", + "settings.general.row.reasoningSummaries.title": "Ҷамъбасти далелҳоро нишон диҳед", + "settings.general.row.reasoningSummaries.description": "Дар ҷадвали вақт хулосаҳои далелҳои моделиро нишон диҳед", + "settings.general.row.shellToolPartsExpanded.title": "Қисмҳои shell асбобро васеъ кунед", + "settings.general.row.shellToolPartsExpanded.description": + "Дар ҷадвали вақт қисмҳои асбобҳои shell васеъшударо нишон диҳед", + "settings.general.row.editToolPartsExpanded.title": "Қисмҳои таҳрири асбобро васеъ кунед", + "settings.general.row.editToolPartsExpanded.description": + "Намоиши қисмҳои абзори таҳрир, навиштан ва часбкунӣ, ки ба таври нобаёнӣ дар ҷадвали вақт васеъ карда шудаанд", + "settings.general.row.newInterface.title": "Тарҳбандии нав", + "settings.general.row.newInterface.badge": "Нав", + "settings.general.row.newInterface.description": + "Варақаҳои нав ва тарҳбандии хонаро истифода баред. Дар муддати маҳдуд байни тарҳҳо гузаред.", + "settings.general.row.newInterfaceNotice.title": "Шумо ҳоло тарҳбандии навро истифода мебаред", + "settings.general.row.newInterfaceNotice.description": "Тарҳбандии қаблӣ дигар дастрас нест", + "settings.general.row.newInterfaceNotice.dismiss": "Рад кардан", + "settings.general.row.pinchZoom.title": "Барои калон кардан чимч кунед", + "settings.general.row.pinchZoom.description": + "Ба имову ишораи паймоиш ва Ctrl-и паймоиш иҷозат диҳед, ки калонтар кунед", + "settings.general.row.wayland.title": "Истифодаи аслӣ Wayland", + "settings.general.row.wayland.description": + "Хомӯш кардани X11 бозгашт дар Wayland. Аз нав оғоз карданро талаб мекунад.", + "settings.general.row.wayland.tooltip": + "Дар Linux бо мониторҳои омехтаи суръати навсозӣ, Wayland ватанӣ метавонад устувортар бошад.", + "settings.general.row.releaseNotes.title": "Қайдҳои нашр", + "settings.general.row.releaseNotes.description": 'Пас аз навсозиҳо поп-апҳои "Чӣ нав"-ро нишон диҳед', + "settings.updates.row.startup.title": "Барои навсозиҳо дар оғозёбӣ санҷед", + "settings.updates.row.startup.description": "Ҳангоми оғоз кардани OpenCode навсозиҳо ба таври худкор санҷед", + "settings.updates.row.check.title": "Барои навсозиҳо санҷед", + "settings.updates.row.check.description": "Навсозиро дастӣ тафтиш кунед ва агар дастрас бошад, насб кунед", + "settings.updates.action.checkNow": "Ҳоло санҷед", + "settings.updates.action.checking": "Санҷиш...", + "settings.updates.action.downloading": "Зеркашӣ карда мешавад...", + "settings.updates.action.installing": "Насб карда мешавад...", + "settings.updates.toast.latest.title": "Шумо навсозӣ доред", + "settings.updates.toast.latest.description": "Шумо версияи охирини OpenCode-ро иҷро карда истодаед.", + "sound.option.none": "Ҳеҷ", + "sound.option.alert01": "Огоҳӣ 01", + "sound.option.alert02": "Огоҳӣ 02", + "sound.option.alert03": "Огоҳӣ 03", + "sound.option.alert04": "Огоҳӣ 04", + "sound.option.alert05": "Огоҳӣ 05", + "sound.option.alert06": "Огоҳӣ 06", + "sound.option.alert07": "Огоҳӣ 07", + "sound.option.alert08": "Огоҳӣ 08", + "sound.option.alert09": "Огоҳӣ 09", + "sound.option.alert10": "Огоҳӣ 10", + "sound.option.bipbop01": "Бип-боп 01", + "sound.option.bipbop02": "Бип-боп 02", + "sound.option.bipbop03": "Бип-боп 03", + "sound.option.bipbop04": "Бип-боп 04", + "sound.option.bipbop05": "Бип-боп 05", + "sound.option.bipbop06": "Бип-боп 06", + "sound.option.bipbop07": "Бип-боп 07", + "sound.option.bipbop08": "Бип-боп 08", + "sound.option.bipbop09": "Бип-боп 09", + "sound.option.bipbop10": "Бип-боп 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Не 01", + "sound.option.nope02": "Не 02", + "sound.option.nope03": "Не 03", + "sound.option.nope04": "Не 04", + "sound.option.nope05": "Не 05", + "sound.option.nope06": "Не 06", + "sound.option.nope07": "Не 07", + "sound.option.nope08": "Не 08", + "sound.option.nope09": "Не 09", + "sound.option.nope10": "Не 10", + "sound.option.nope11": "Не 11", + "sound.option.nope12": "Не 12", + "sound.option.yup01": "Ҳа 01", + "sound.option.yup02": "Ҳа 02", + "sound.option.yup03": "Ҳа 03", + "sound.option.yup04": "Ҳа 04", + "sound.option.yup05": "Ҳа 05", + "sound.option.yup06": "Ҳа 06", + "settings.general.notifications.agent.title": "Агент", + "settings.general.notifications.agent.description": + "Вақте ки агент пурра аст ё ба таваҷҷӯҳ ниёз дорад, огоҳии системаро нишон диҳед", + "settings.general.notifications.permissions.title": "Иҷозатҳо", + "settings.general.notifications.permissions.description": "Вақте ки иҷозат лозим аст, огоҳии системаро нишон диҳед", + "settings.general.notifications.errors.title": "Хатогиҳо", + "settings.general.notifications.errors.description": "Ҳангоми рух додани хато огоҳии системаро нишон диҳед", + "settings.general.sounds.agent.title": "Агент", + "settings.general.sounds.agent.description": "Вақте ки агент пурра аст ё ба таваҷҷӯҳ ниёз дорад, садоро пахш кунед", + "settings.general.sounds.permissions.title": "Иҷозатҳо", + "settings.general.sounds.permissions.description": "Вақте ки иҷозат лозим аст, садо навозед", + "settings.general.sounds.errors.title": "Хатогиҳо", + "settings.general.sounds.errors.description": "Вақте ки хатогӣ рух медиҳад, садоро пахш кунед", + "settings.shortcuts.title": "Миёнабурҳои клавиатура", + "settings.shortcuts.reset.button": "Ба пешфарзҳо баргардонед", + "settings.shortcuts.reset.toast.title": "Миёнабурҳо аз нав барқарор карда мешаванд", + "settings.shortcuts.reset.toast.description": "Миёнабурҳои клавиатура ба пешфарзҳо барқарор карда шуданд.", + "settings.shortcuts.conflict.title": "Миёнабур аллакай истифода мешавад", + "settings.shortcuts.conflict.description": "{{keybind}} аллакай ба {{titles}} таъин шудааст.", + "settings.shortcuts.unassigned": "Таъиннашуда", + "settings.shortcuts.pressKeys": "Тугмаҳоро пахш кунед", + "settings.shortcuts.search.placeholder": "Ҷустуҷӯи миёнабурҳо", + "settings.shortcuts.search.empty": "Ягон миёнабур ёфт нашуд", + "settings.shortcuts.group.general": "Генерал", + "settings.shortcuts.group.session": "Сессия", + "settings.shortcuts.group.navigation": "Навигатсия", + "settings.shortcuts.group.modelAndAgent": "Модел ва агент", + "settings.shortcuts.group.terminal": "Терминал", + "settings.shortcuts.group.prompt": "Промпт", + "settings.providers.title": "Таъминкунандагон", + "settings.providers.description": "Танзимоти провайдер дар ин ҷо танзим карда мешавад.", + "settings.providers.section.connected": "Провайдерҳои пайвастшуда", + "settings.providers.connected.empty": "Провайдерҳои пайвастшуда нест", + "settings.providers.connected.environmentDescription": "Аз тағирёбандаҳои муҳити шумо пайваст карда шудааст", + "settings.providers.section.popular": "Провайдерҳои машҳур", + "settings.providers.custom.description": "Илова кардани провайдери OpenAI-мувофиқ аз рӯи асос URL.", + "settings.providers.tag.environment": "Муҳити зист", + "settings.providers.tag.config": "Конфигуратсия", + "settings.providers.tag.custom": "Фармоишгар", + "settings.providers.tag.other": "Дигар", + "settings.models.title": "Моделҳо", + "settings.models.description": "Танзимоти модел дар ин ҷо танзим карда мешавад.", + "settings.agents.title": "Агентҳо", + "settings.agents.description": "Танзимоти агент дар ин ҷо танзим карда мешавад.", + "settings.commands.title": "Фармонҳо", + "settings.commands.description": "Танзимоти фармон дар ин ҷо танзим карда мешавад.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP танзимот дар ин ҷо танзим карда мешаванд.", + "settings.permissions.title": "Иҷозатҳо", + "settings.permissions.description": "Назорат кунед, ки кадом асбобҳоро сервер бо нобаёнӣ истифода бурда метавонад.", + "settings.permissions.section.tools": "Воситаҳо", + "settings.permissions.toast.updateFailed.title": "Навсозии иҷозатҳо муяссар нашуд", + "settings.permissions.action.allow": "Иҷозат диҳед", + "settings.permissions.action.ask": "Пурсед", + "settings.permissions.action.deny": "Инкор кардан", + "settings.permissions.tool.read.title": "Хондан", + "settings.permissions.tool.read.description": "Хондани файл (бо роҳи файл мувофиқат мекунад)", + "settings.permissions.tool.edit.title": "Таҳрир", + "settings.permissions.tool.edit.description": "Тағир додани файлҳо, аз ҷумла таҳрир, навиштан ва часбҳо", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Бо истифода аз намунаҳои glob файлҳоро мувофиқ кунед", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Ҷустуҷӯи мундариҷаи файл бо истифода аз ибораҳои муқаррарӣ", + "settings.permissions.tool.list.title": "Рӯйхат", + "settings.permissions.tool.list.description": "Рӯйхати файлҳо дар дохили директория", + "settings.permissions.tool.bash.title": "Баш", + "settings.permissions.tool.bash.description": "Фармонҳои shell -ро иҷро кунед", + "settings.permissions.tool.task.title": "Вазифа", + "settings.permissions.tool.task.description": "Зер-агентҳоро оғоз кунед", + "settings.permissions.tool.skill.title": "Маҳорат", + "settings.permissions.tool.skill.description": "Маҳоратро бо ном бор кунед", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Дархостҳои сервери забонро иҷро кунед", + "settings.permissions.tool.todowrite.title": "Барои навиштан", + "settings.permissions.tool.todowrite.description": "Рӯйхати вазифаҳоро навсозӣ кунед", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Гирифтани мундариҷа аз URL", + "settings.permissions.tool.websearch.title": "Ҷустуҷӯи веб", + "settings.permissions.tool.websearch.description": "Дар веб ҷустуҷӯ кунед", + "settings.permissions.tool.external_directory.title": "Директорияи беруна", + "settings.permissions.tool.external_directory.description": "Дастрасӣ ба файлҳои берун аз феҳристи лоиҳа", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "Зангҳои такрории асбобро бо вуруди якхела ошкор кунед", + "session.delete.failed.title": "Сеансро нест карда натавонист", + "session.delete.title": "Сеансро нест кунед", + "session.delete.confirm": 'Сеанси "{{name}}" нест карда шавад?', + "session.delete.button": "Сеансро нест кунед", + "workspace.new": "Майдони нави корӣ", + "workspace.type.local": "маҳаллӣ", + "workspace.type.sandbox": "қуттии қум", + "workspace.create.failed.title": "Фазои корӣ эҷод карда нашуд", + "workspace.delete.failed.title": "Нест кардани фазои корӣ", + "workspace.resetting.title": "Барқарор кардани фазои корӣ", + "workspace.resetting.description": "Ин метавонад як дақиқа вақт гирад.", + "workspace.reset.failed.title": "Муваффақият барқарор кардани фазои корӣ", + "workspace.reset.success.title": "Бозсозии фазои корӣ", + "workspace.reset.success.description": "Фазои корӣ ҳоло ба филиали пешфарз мувофиқат мекунад.", + "workspace.error.stillPreparing": "Майдони корӣ ҳоло ҳам омода мешавад", + "workspace.status.checking": "Тафтиши тағироти муттаҳиднашуда...", + "workspace.status.error": "Ҳолати git-ро тафтиш кардан ғайриимкон аст.", + "workspace.status.clean": "Ягон тағироти муттаҳидшуда ошкор нашудааст.", + "workspace.status.dirty": "Тағироти муттаҳиднашуда дар ин фазои корӣ ошкор карда шуданд.", + "workspace.delete.title": "Нест кардани фазои корӣ", + "workspace.delete.confirm": 'Фазои кории "{{name}}" нест карда шавад?', + "workspace.delete.button": "Нест кардани фазои корӣ", + "workspace.reset.title": "Фазои корӣ аз нав танзим кунед", + "workspace.reset.confirm": 'Фазои кории "{{name}}" барқарор карда шавад?', + "workspace.reset.button": "Фазои корӣ аз нав танзим кунед", + "workspace.reset.archived.none": "Ягон сессияҳои фаъол бойгонӣ карда намешаванд.", + "workspace.reset.archived.one": "1 сессия архив карда мешавад.", + "workspace.reset.archived.many": "{{count}} сессия архив карда мешаванд.", + "workspace.reset.note": "Ин фазои кориро барои мувофиқ кардани филиали пешфарз барқарор мекунад.", + "dialog.usageExceeded.dontShowAgain": "Дигар нишон надиҳед", +} diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a0e9db72b1b04d1a99219689428c01b58778b4b --- /dev/null +++ b/packages/app/src/i18n/th.ts @@ -0,0 +1,1238 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "ไฟล์", + "desktop.menu.edit": "แก้ไข", + "desktop.menu.view": "มุมมอง", + "desktop.menu.go": "ไป", + "desktop.menu.window": "หน้าต่าง", + "desktop.menu.help": "วิธีใช้", + "desktop.menu.checkForUpdates": "ตรวจหาการอัปเดต...", + "desktop.menu.settings": "การตั้งค่า", + "desktop.menu.reloadWebview": "โหลด Webview ใหม่", + "desktop.menu.restart": "เริ่มการทำงานใหม่", + "desktop.menu.exportLogs": "ส่งออกบันทึก...", + "desktop.menu.newSession": "เซสชันใหม่", + "desktop.menu.openProject": "เปิดโปรเจกต์...", + "desktop.menu.newWindow": "หน้าต่างใหม่", + "desktop.menu.closeWindow": "ปิดหน้าต่าง", + "desktop.menu.undo": "เลิกทำ", + "desktop.menu.redo": "ทำซ้ำ", + "desktop.menu.cut": "ตัด", + "desktop.menu.copy": "คัดลอก", + "desktop.menu.paste": "วาง", + "desktop.menu.delete": "ลบ", + "desktop.menu.selectAll": "เลือกทั้งหมด", + "desktop.menu.toggleSidebar": "สลับแถบด้านข้าง", + "desktop.menu.toggleTerminal": "สลับเทอร์มินัล", + "desktop.menu.toggleFileTree": "สลับแผนผังไฟล์", + "desktop.menu.reload": "โหลดใหม่", + "desktop.menu.toggleDeveloperTools": "สลับเครื่องมือสำหรับนักพัฒนา", + "desktop.menu.actualSize": "ขนาดจริง", + "desktop.menu.zoomIn": "ขยาย", + "desktop.menu.zoomOut": "ย่อ", + "desktop.menu.toggleFullScreen": "สลับโหมดเต็มหน้าจอ", + "desktop.menu.back": "ย้อนกลับ", + "desktop.menu.forward": "ไปข้างหน้า", + "desktop.menu.previousSession": "เซสชันก่อนหน้า", + "desktop.menu.nextSession": "เซสชันถัดไป", + "desktop.menu.previousProject": "โปรเจกต์ก่อนหน้า", + "desktop.menu.nextProject": "โปรเจกต์ถัดไป", + "desktop.menu.minimize": "ย่อหน้าต่าง", + "desktop.menu.maximize": "ขยายหน้าต่าง", + "desktop.menu.documentation": "เอกสาร OpenCode", + "desktop.menu.supportForum": "ฟอรัมสนับสนุน", + "desktop.menu.shareFeedback": "ส่งข้อคิดเห็น", + "desktop.menu.reportBug": "รายงานข้อบกพร่อง", + "desktop.menu.ariaLabel": "เมนู OpenCode", + + "desktop.updater.dialog.checkFailed.message": "การตรวจหาการอัปเดตล้มเหลว", + "desktop.updater.dialog.checkFailed.title": "ข้อผิดพลาดในการอัปเดต", + "desktop.updater.dialog.upToDate.message": "คุณกำลังใช้เวอร์ชันล่าสุด", + "desktop.updater.dialog.upToDate.title": "ไม่มีการอัปเดต", + "desktop.updater.dialog.ready.message": "ดาวน์โหลดการอัปเดต {{version}} แล้ว เริ่มการทำงานใหม่ตอนนี้หรือไม่", + "desktop.updater.dialog.ready.title": "พร้อมอัปเดต", + "desktop.updater.dialog.restart": "เริ่มการทำงานใหม่", + "desktop.updater.dialog.later": "ภายหลัง", + + "desktop.recovery.action.relaunch": "เปิดอีกครั้ง", + "desktop.recovery.action.exportLogs": "ส่งออกบันทึก", + "desktop.recovery.action.keepWaiting": "รอต่อไป", + "desktop.recovery.action.quit": "ออก", + "desktop.recovery.loadFailed": "โหลด OpenCode ไม่สำเร็จ", + "desktop.recovery.terminated": "หน้าต่าง OpenCode หยุดทำงานโดยไม่คาดคิด", + "desktop.recovery.unresponsive": "OpenCode ไม่ตอบสนอง", + "desktop.recovery.unresponsive.detail": "คุณสามารถเปิดแอปอีกครั้ง เปิดบันทึก หรือรอต่อไป", + "desktop.recovery.loadFailed.detail": "หน้าต่าง: {{window}}\nURL: {{url}}\nข้อผิดพลาด: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "หน้าต่าง: {{window}}\nสาเหตุ: {{reason}}\nรหัส: {{code}}", + "desktop.recovery.unknown": "<ไม่ทราบ>", + + "desktop.dialog.chooseFolder": "เลือกโฟลเดอร์", + "desktop.dialog.chooseFile": "เลือกไฟล์", + "desktop.dialog.saveFile": "บันทึกไฟล์", + "desktop.dialog.files": "ไฟล์", + "desktop.server.local": "เซิร์ฟเวอร์ภายในเครื่อง", + + "desktop.wsl.error.windowsOnly": "WSL ใช้งานได้เฉพาะบน Windows เท่านั้น", + "desktop.wsl.error.unavailable": "WSL ไม่พร้อมใช้งาน", + "desktop.wsl.error.listInstalled": "ไม่สามารถแสดงรายการดิสโทร WSL ที่ติดตั้งแล้วได้", + "desktop.wsl.error.listOnline": "ไม่สามารถแสดงรายการดิสโทร WSL แบบออนไลน์ได้", + "desktop.wsl.error.executeDistro": "ไม่สามารถเรียกใช้คำสั่งในดิสโทรได้", + "desktop.wsl.error.installWsl": "การติดตั้ง WSL ล้มเหลว", + "desktop.wsl.error.installDistro": "ไม่สามารถติดตั้งดิสโทรได้: {{distro}}", + "desktop.wsl.error.installOpencode": "การติดตั้ง OpenCode ล้มเหลว", + "desktop.wsl.error.alreadyAdded": "เพิ่ม {{distro}} แล้ว", + "desktop.wsl.error.opencodeMissing": "ไม่ได้ติดตั้ง opencode ในดิสโทรนี้", + "desktop.wsl.error.opencodeCannotRun": "ติดตั้ง opencode แล้ว แต่ไม่สามารถเรียกใช้ได้", + "desktop.wsl.error.opencodeNotInstalled": "ไม่ได้ติดตั้ง OpenCode ใน {{distro}}", + "desktop.wsl.error.updateVersion": + "อัปเดต OpenCode เสร็จแล้ว แต่ {{distro}} ยังคงรายงานเวอร์ชัน {{installed}} ทั้งที่ควรเป็น {{expected}}", + "desktop.wsl.error.noVersion": "ไม่มีเวอร์ชัน", + "desktop.wsl.error.serverExited": "เซิร์ฟเวอร์ WSL หยุดทำงานหลังเริ่มต้น (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "เซิร์ฟเวอร์ WSL หยุดทำงานก่อนพร้อมใช้งาน (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "การตรวจสอบสถานะ Sidecar สำหรับ {{distro}} หมดเวลาหลังจาก {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} หมดเวลาหลังจาก {{timeout}}ms", + "desktop.wsl.error.failedPort": "ไม่สามารถรับพอร์ตได้", + + "desktop.picker.error.notSelected": "ไม่ได้เลือกไฟล์จากตัวเลือกไฟล์", + "desktop.picker.error.sizeLimit": "ไฟล์แนบที่เลือกมีขนาดเกินขีดจำกัด {{limit}} MB", + + "command.category.suggested": "แนะนำ", + "command.category.view": "มุมมอง", + "command.category.project": "โปรเจกต์", + "command.category.provider": "ผู้ให้บริการ", + "command.category.server": "เซิร์ฟเวอร์", + "command.category.session": "เซสชัน", + "command.category.theme": "ธีม", + "command.category.language": "ภาษา", + "command.category.file": "ไฟล์", + "command.category.context": "บริบท", + "command.category.terminal": "เทอร์มินัล", + "command.category.model": "โมเดล", + "command.category.mcp": "MCP", + "command.category.agent": "เอเจนต์", + "command.category.permissions": "สิทธิ์", + "command.category.workspace": "พื้นที่ทำงาน", + "command.category.settings": "การตั้งค่า", + "command.logs.export": "ส่งออกบันทึก", + + "theme.scheme.system": "ระบบ", + "theme.scheme.light": "สว่าง", + "theme.scheme.dark": "มืด", + + "command.sidebar.toggle": "สลับแถบข้าง", + "command.project.open": "เปิดโปรเจกต์", + "command.project.previous": "โปรเจกต์ก่อนหน้า", + "command.project.next": "โปรเจกต์ถัดไป", + "command.project.index": "สลับไปยังโปรเจกต์ {{index}}", + "command.provider.connect": "เชื่อมต่อผู้ให้บริการ", + "command.server.switch": "สลับเซิร์ฟเวอร์", + "command.settings.open": "เปิดการตั้งค่า", + "command.session.previous": "เซสชันก่อนหน้า", + "command.session.next": "เซสชันถัดไป", + "command.session.previous.unseen": "เซสชันที่ยังไม่ได้อ่านก่อนหน้า", + "command.session.next.unseen": "เซสชันที่ยังไม่ได้อ่านถัดไป", + "command.session.archive": "จัดเก็บเซสชัน", + + "command.palette": "ชุดคำสั่ง", + + "command.theme.cycle": "เปลี่ยนธีม", + "command.theme.set": "ใช้ธีม: {{theme}}", + "command.theme.scheme.cycle": "เปลี่ยนโทนสี", + "command.theme.scheme.set": "ใช้โทนสี: {{scheme}}", + + "command.language.cycle": "เปลี่ยนภาษา", + "command.language.set": "ใช้ภาษา: {{language}}", + + "command.session.new": "เซสชันใหม่", + "command.file.open": "เปิดไฟล์", + "command.tab.close": "ปิดแท็บ", + "command.tab.reopenClosed": "เปิดแท็บที่ปิดไปอีกครั้ง", + "command.context.addSelection": "เพิ่มส่วนที่เลือกไปยังบริบท", + "command.context.addSelection.description": "เพิ่มบรรทัดที่เลือกจากไฟล์ปัจจุบัน", + "command.input.focus": "โฟกัสช่องป้อนข้อมูล", + "command.terminal.toggle": "สลับเทอร์มินัล", + "command.fileTree.toggle": "สลับแผนผังไฟล์", + "command.review.toggle": "สลับการตรวจสอบ", + "command.terminal.new": "เทอร์มินัลใหม่", + "command.terminal.new.description": "สร้างแท็บเทอร์มินัลใหม่", + "command.steps.toggle": "สลับขั้นตอน", + "command.steps.toggle.description": "แสดงหรือซ่อนขั้นตอนสำหรับข้อความปัจจุบัน", + "command.message.previous": "ข้อความก่อนหน้า", + "command.message.previous.description": "ไปที่ข้อความผู้ใช้ก่อนหน้า", + "command.message.next": "ข้อความถัดไป", + "command.message.next.description": "ไปที่ข้อความผู้ใช้ถัดไป", + "command.model.choose": "เลือกโมเดล", + "command.model.choose.description": "เลือกโมเดลอื่น", + "command.mcp.toggle": "สลับ MCPs", + "command.mcp.toggle.description": "สลับ MCPs", + "command.agent.cycle": "เปลี่ยนเอเจนต์", + "command.agent.cycle.description": "สลับไปยังเอเจนต์ถัดไป", + "command.agent.cycle.reverse": "เปลี่ยนเอเจนต์ย้อนกลับ", + "command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า", + "command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด", + "command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป", + "command.prompt.mode.shell": "เชลล์", + "command.prompt.mode.normal": "พรอมต์", + "command.permissions.autoaccept.enable": "ยอมรับสิทธิ์โดยอัตโนมัติ", + "command.permissions.autoaccept.disable": "หยุดยอมรับสิทธิ์โดยอัตโนมัติ", + "command.workspace.toggle": "สลับพื้นที่ทำงาน", + "command.workspace.toggle.description": "เปิดหรือปิดใช้งานพื้นที่ทำงานหลายรายการในแถบด้านข้าง", + "command.session.undo": "เลิกทำ", + "command.session.undo.description": "เลิกทำข้อความล่าสุด", + "command.session.redo": "ทำซ้ำ", + "command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด", + "command.session.compact": "บีบอัดเซสชัน", + "command.session.compact.description": "สรุปเซสชันเพื่อลดขนาดบริบท", + "command.session.fork": "แตกแขนงจากข้อความ", + "command.session.fork.description": "สร้างเซสชันใหม่จากข้อความก่อนหน้า", + "command.session.share": "แชร์เซสชัน", + "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": "ไม่พบผลลัพธ์", + "palette.group.commands": "คำสั่ง", + "palette.group.files": "ไฟล์", + + "dialog.provider.search.placeholder": "ค้นหาผู้ให้บริการ", + "dialog.provider.empty": "ไม่พบผู้ให้บริการ", + "dialog.provider.group.popular": "ยอดนิยม", + "dialog.provider.group.other": "อื่น ๆ", + "dialog.provider.custom.label": "ผู้ให้บริการแบบกำหนดเองที่เข้ากันได้กับ OpenAI", + "dialog.provider.tag.recommended": "แนะนำ", + "dialog.provider.opencode.note": "โมเดลที่คัดสรร รวมถึง Claude, GPT, Gemini และอื่น ๆ", + "dialog.provider.opencode.tagline": "โมเดลที่เชื่อถือได้และปรับให้เหมาะสม", + "dialog.provider.opencodeGo.tagline": "การสมัครสมาชิกราคาประหยัดสำหรับทุกคน", + "dialog.provider.anthropic.note": "เข้าถึงโมเดล Claude โดยตรง รวมถึง Pro และ Max", + "dialog.provider.copilot.note": "โมเดล AI สำหรับการช่วยเหลือในการเขียนโค้ดผ่าน GitHub Copilot", + "dialog.provider.openai.note": "โมเดล GPT สำหรับงาน AI ทั่วไปที่รวดเร็วและมีความสามารถ", + "dialog.provider.google.note": "โมเดล Gemini สำหรับการตอบสนองที่รวดเร็วและมีโครงสร้าง", + "dialog.provider.openrouter.note": "เข้าถึงโมเดลที่รองรับทั้งหมดจากผู้ให้บริการเดียว", + "dialog.provider.vercel.note": "การเข้าถึงโมเดล AI แบบรวมด้วยการกำหนดเส้นทางอัจฉริยะ", + + "dialog.model.select.title": "เลือกโมเดล", + "dialog.model.search.placeholder": "ค้นหาโมเดล", + "dialog.model.empty": "ไม่พบผลลัพธ์โมเดล", + "dialog.model.manage": "จัดการโมเดล", + "dialog.model.manage.description": "ปรับแต่งโมเดลที่จะปรากฏในตัวเลือกโมเดล", + "dialog.model.manage.provider.toggle": "สลับโมเดลทั้งหมดของ {{provider}}", + + "dialog.model.unpaid.freeModels.title": "โมเดลฟรีที่จัดหาให้โดย OpenCode", + "dialog.model.unpaid.addMore.title": "เพิ่มโมเดลเพิ่มเติมจากผู้ให้บริการยอดนิยม", + "dialog.model.unpaid.viewMoreProviders": "ดูผู้ให้บริการเพิ่มเติมกว่า 70 ราย", + + "dialog.provider.viewAll": "แสดงผู้ให้บริการเพิ่มเติม", + + "provider.connect.title": "เชื่อมต่อ {{provider}}", + "provider.connect.title.anthropicProMax": "เข้าสู่ระบบด้วย Claude Pro/Max", + "provider.connect.selectMethod": "เลือกวิธีการเข้าสู่ระบบสำหรับ {{provider}}", + "provider.connect.method.apiKey": "คีย์ API", + "provider.connect.method.browser": "เบราว์เซอร์", + "provider.connect.method.headless": "ไม่ใช้เบราว์เซอร์", + "provider.connect.status.inProgress": "กำลังอนุญาต...", + "provider.connect.status.waiting": "รอการอนุญาต...", + "provider.connect.status.failed": "การอนุญาตล้มเหลว: {{error}}", + "provider.connect.apiKey.description": + "ป้อนคีย์ API ของ {{provider}} เพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode", + "provider.connect.apiKey.label": "คีย์ API ของ {{provider}}", + "provider.connect.apiKey.placeholder": "คีย์ API", + "provider.connect.apiKey.required": "ต้องใช้คีย์ API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen ให้คุณเข้าถึงชุดโมเดลที่เชื่อถือได้และปรับแต่งแล้วสำหรับเอเจนต์การเขียนโค้ด", + "provider.connect.opencodeZen.line2": + "ด้วยคีย์ API เดียวคุณจะได้รับการเข้าถึงโมเดล เช่น Claude, GPT, Gemini, GLM และอื่น ๆ", + "provider.connect.opencodeZen.visit.prefix": "เยี่ยมชม ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " เพื่อรับคีย์ API ของคุณ", + "provider.connect.oauth.code.visit.prefix": "เยี่ยมชม ", + "provider.connect.oauth.code.visit.link": "ลิงก์นี้", + "provider.connect.oauth.code.visit.suffix": + " เพื่อรับรหัสการอนุญาตสำหรับเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode", + "provider.connect.oauth.code.label": "รหัสการอนุญาต {{method}}", + "provider.connect.oauth.code.placeholder": "รหัสการอนุญาต", + "provider.connect.oauth.code.required": "ต้องใช้รหัสการอนุญาต", + "provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง", + "provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ", + "provider.connect.oauth.auto.visit.link": "ลิงก์นี้", + "provider.connect.oauth.auto.visit.suffix": + " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน OpenCode", + "provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน", + "provider.connect.toast.connected.title": "เชื่อมต่อ {{provider}} แล้ว", + "provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว", + + "provider.custom.title": "ผู้ให้บริการที่กำหนดเอง", + "provider.custom.unavailable": "ผู้ให้บริการที่กำหนดเองไม่พร้อมใช้งานบนเซิร์ฟเวอร์นี้", + "provider.custom.description.prefix": "กำหนดค่าผู้ให้บริการที่เข้ากันได้กับ OpenAI ดู ", + "provider.custom.description.link": "เอกสารการกำหนดค่าผู้ให้บริการ", + "provider.custom.description.suffix": "", + "provider.custom.field.providerID.label": "รหัสผู้ให้บริการ", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "ตัวอักษรพิมพ์เล็ก ตัวเลข ยัติภังค์ หรือขีดล่าง", + "provider.custom.field.name.label": "ชื่อที่แสดง", + "provider.custom.field.name.placeholder": "ผู้ให้บริการ AI ของฉัน", + "provider.custom.field.baseURL.label": "URL พื้นฐาน", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "คีย์ API", + "provider.custom.field.apiKey.placeholder": "คีย์ API", + "provider.custom.field.apiKey.description": "ไม่บังคับ เว้นว่างไว้หากคุณจัดการการยืนยันตัวตนผ่านส่วนหัว", + "provider.custom.models.label": "โมเดล", + "provider.custom.models.id.label": "รหัส", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "ชื่อ", + "provider.custom.models.name.placeholder": "ชื่อที่แสดง", + "provider.custom.models.remove": "ลบโมเดล", + "provider.custom.models.add": "เพิ่มโมเดล", + "provider.custom.headers.label": "ส่วนหัว (ไม่บังคับ)", + "provider.custom.headers.key.label": "ส่วนหัว", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "ค่า", + "provider.custom.headers.value.placeholder": "ค่า", + "provider.custom.headers.remove": "ลบส่วนหัว", + "provider.custom.headers.add": "เพิ่มส่วนหัว", + "provider.custom.error.providerID.required": "ต้องระบุรหัสผู้ให้บริการ", + "provider.custom.error.providerID.format": "ใช้ตัวอักษรพิมพ์เล็ก ตัวเลข ยัติภังค์ หรือขีดล่าง", + "provider.custom.error.providerID.exists": "รหัสผู้ให้บริการนั้นมีอยู่แล้ว", + "provider.custom.error.name.required": "ต้องระบุชื่อที่แสดง", + "provider.custom.error.baseURL.required": "ต้องระบุ URL พื้นฐาน", + "provider.custom.error.baseURL.format": "ต้องขึ้นต้นด้วย http:// หรือ https://", + "provider.custom.error.required": "จำเป็น", + "provider.custom.error.duplicate": "ซ้ำ", + + "provider.disconnect.toast.disconnected.title": "ยกเลิกการเชื่อมต่อ {{provider}} แล้ว", + "provider.disconnect.toast.disconnected.description": "โมเดล {{provider}} ไม่พร้อมใช้งานอีกต่อไป", + + "model.tag.free": "ฟรี", + "model.tag.latest": "ล่าสุด", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "ข้อความ", + "model.input.image": "รูปภาพ", + "model.input.audio": "เสียง", + "model.input.video": "วิดีโอ", + "model.input.pdf": "PDF", + "model.tooltip.allows": "รองรับ: {{inputs}}", + "model.tooltip.context.label": "บริบท", + "model.tooltip.inputs": "ข้อมูลนำเข้า", + "model.tooltip.model": "โมเดล", + "model.tooltip.provider": "ผู้ให้บริการ", + "model.tooltip.reasoning": "การใช้เหตุผล", + "model.tooltip.reasoning.allowed": "อนุญาตการใช้เหตุผล", + "model.tooltip.reasoning.none": "ไม่มีการใช้เหตุผล", + "model.tooltip.context": "ขีดจำกัดบริบท {{limit}}", + + "common.search.placeholder": "ค้นหา", + "common.goBack": "ย้อนกลับ", + "common.goForward": "ไปข้างหน้า", + "common.loading": "กำลังโหลด", + "common.loading.ellipsis": "...", + "common.cancel": "ยกเลิก", + "common.clear": "ล้าง", + "common.connect": "เชื่อมต่อ", + "common.disconnect": "ยกเลิกการเชื่อมต่อ", + "common.continue": "ดำเนินการต่อ", + "common.submit": "ส่ง", + "common.save": "บันทึก", + "common.saving": "กำลังบันทึก...", + "common.default": "ค่าเริ่มต้น", + "common.attachment": "ไฟล์แนบ", + + "prompt.placeholder.shell": "ป้อนคำสั่งเชลล์... {{example}}", + "prompt.placeholder.normal": 'ถามอะไรก็ได้... "{{example}}"', + "prompt.placeholder.simple": "ถามอะไรก็ได้...", + "prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…", + "prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…", + "prompt.mode.shell": "เชลล์", + "prompt.mode.normal": "พรอมต์", + "prompt.mode.shell.exit": "กด esc เพื่อออก", + "session.child.promptDisabled": "ไม่สามารถส่งพรอมต์ในเซสชันเอเจนต์ย่อยได้", + "session.child.backToParent": "กลับไปยังเซสชันหลัก", + + "prompt.example.1": "แก้ไข TODO ในโค้ดเบส", + "prompt.example.2": "เทคโนโลยีของโปรเจกต์นี้คืออะไร?", + "prompt.example.3": "แก้ไขการทดสอบที่เสีย", + "prompt.example.4": "อธิบายวิธีการทำงานของการตรวจสอบสิทธิ์", + "prompt.example.5": "ค้นหาและแก้ไขช่องโหว่ความปลอดภัย", + "prompt.example.6": "เพิ่มการทดสอบหน่วยสำหรับบริการผู้ใช้", + "prompt.example.7": "ปรับโครงสร้างฟังก์ชันนี้ให้อ่านง่ายขึ้น", + "prompt.example.8": "ข้อผิดพลาดนี้หมายความว่าอะไร?", + "prompt.example.9": "ช่วยฉันดีบักปัญหานี้", + "prompt.example.10": "สร้างเอกสาร API", + "prompt.example.11": "ปรับปรุงการสืบค้นฐานข้อมูล", + "prompt.example.12": "เพิ่มการตรวจสอบข้อมูลนำเข้า", + "prompt.example.13": "สร้างคอมโพเนนต์ใหม่สำหรับ...", + "prompt.example.14": "ฉันจะนำโปรเจกต์นี้ไปใช้งานได้อย่างไร?", + "prompt.example.15": "ตรวจสอบโค้ดของฉันเพื่อแนวทางปฏิบัติที่ดีที่สุด", + "prompt.example.16": "เพิ่มการจัดการข้อผิดพลาดในฟังก์ชันนี้", + "prompt.example.17": "อธิบายรูปแบบ regex นี้", + "prompt.example.18": "แปลงสิ่งนี้เป็น TypeScript", + "prompt.example.19": "เพิ่มการบันทึกทั่วทั้งโค้ดเบส", + "prompt.example.20": "มีการพึ่งพาอะไรที่ล้าสมัยอยู่?", + "prompt.example.21": "ช่วยฉันเขียนสคริปต์การย้ายข้อมูล", + "prompt.example.22": "ใช้แคชสำหรับเอนด์พอยต์นี้", + "prompt.example.23": "เพิ่มการแบ่งหน้าในรายการนี้", + "prompt.example.24": "สร้างคำสั่ง CLI สำหรับ...", + "prompt.example.25": "ตัวแปรสภาพแวดล้อมทำงานอย่างไรที่นี่?", + + "prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน", + "prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน", + "prompt.dropzone.label": "ลากรูปภาพ PDF หรือไฟล์ข้อความมาวางที่นี่", + "prompt.dropzone.file.label": "วางเพื่ออ้างถึงไฟล์ด้วย @", + "prompt.slash.badge.custom": "กำหนดเอง", + "prompt.slash.badge.skill": "ทักษะ", + "prompt.slash.badge.mcp": "MCP", + "prompt.context.active": "ใช้งานอยู่", + "prompt.context.includeActiveFile": "รวมไฟล์ที่ใช้งานอยู่", + "prompt.context.removeActiveFile": "เอาไฟล์ที่ใช้งานอยู่ออกจากบริบท", + "prompt.context.removeFile": "เอาไฟล์ออกจากบริบท", + "prompt.action.attachFile": "แนบไฟล์", + "prompt.menu.addImagesAndFiles": "เพิ่มไฟล์และอื่น ๆ", + "prompt.menu.imagesAndFiles": "รูปภาพและไฟล์", + "prompt.menu.commands": "คำสั่ง", + "prompt.menu.context": "บริบท", + "prompt.menu.shellCommand": "คำสั่งเชลล์", + "prompt.attachment.remove": "เอาไฟล์แนบออก", + "prompt.action.send": "ส่ง", + "prompt.action.stop": "หยุด", + + "prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ", + "prompt.toast.attachmentDuplicate.title": "ไฟล์นี้ถูกอัปโหลดแล้ว", + "prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ PDF หรือไฟล์ข้อความเท่านั้น", + "prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล", + "prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพรอมต์", + "prompt.toast.worktreeCreateFailed.title": "ไม่สามารถสร้าง worktree", + "prompt.toast.sessionCreateFailed.title": "ไม่สามารถสร้างเซสชัน", + "prompt.toast.shellSendFailed.title": "ไม่สามารถส่งคำสั่งเชลล์", + "prompt.toast.commandSendFailed.title": "ไม่สามารถส่งคำสั่ง", + "prompt.toast.promptSendFailed.title": "ไม่สามารถส่งพรอมต์", + "prompt.toast.promptSendFailed.description": "ไม่สามารถดึงเซสชันได้", + + "dialog.mcp.title": "MCPs", + "dialog.mcp.description": "{{enabled}} จาก {{total}} ที่เปิดใช้งาน", + "dialog.mcp.empty": "ไม่มี MCP ที่กำหนดค่า", + + "dialog.lsp.empty": "LSPs ตรวจจับอัตโนมัติจากประเภทไฟล์", + "dialog.plugins.empty": "ปลั๊กอินที่กำหนดค่าใน opencode.json", + + "mcp.status.connected": "เชื่อมต่อแล้ว", + "mcp.status.failed": "ล้มเหลว", + "mcp.status.needs_auth": "ต้องยืนยันตัวตน", + "mcp.auth.clickToAuthenticate": "คลิกเพื่อยืนยันตัวตน", + "mcp.status.disabled": "ปิดใช้งาน", + + "dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง", + + "dialog.directory.search.placeholder": "ค้นหาโฟลเดอร์", + "dialog.directory.empty": "ไม่พบโฟลเดอร์", + "dialog.directory.action.selectFile": "เลือกไฟล์", + "dialog.directory.action.selectFolder": "เลือกโฟลเดอร์", + "dialog.directory.root": "โฟลเดอร์ราก", + "dialog.directory.parent": "โฟลเดอร์หลัก", + "dialog.directory.readError": "ไม่สามารถอ่านโฟลเดอร์นี้ได้", + + "dialog.server.title": "เซิร์ฟเวอร์", + "dialog.server.description": "สลับเซิร์ฟเวอร์ OpenCode ที่แอปนี้เชื่อมต่อด้วย", + "dialog.server.search.placeholder": "ค้นหาเซิร์ฟเวอร์", + "dialog.server.empty": "ยังไม่มีเซิร์ฟเวอร์", + "dialog.server.add.title": "เพิ่มเซิร์ฟเวอร์", + "dialog.server.add.url": "URL เซิร์ฟเวอร์", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์", + "dialog.server.add.checking": "กำลังตรวจสอบ...", + "dialog.server.add.button": "เพิ่มเซิร์ฟเวอร์", + "dialog.server.add.name": "ชื่อเซิร์ฟเวอร์ (ไม่บังคับ)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "ชื่อผู้ใช้ (ไม่บังคับ)", + "dialog.server.add.password": "รหัสผ่าน (ไม่บังคับ)", + "dialog.server.edit.title": "แก้ไขเซิร์ฟเวอร์", + "dialog.server.default.title": "เซิร์ฟเวอร์เริ่มต้น", + "dialog.server.default.description": + "เชื่อมต่อกับเซิร์ฟเวอร์นี้เมื่อเปิดแอปแทนการเริ่มเซิร์ฟเวอร์ในเครื่อง ต้องรีสตาร์ท", + "dialog.server.default.none": "ไม่ได้เลือกเซิร์ฟเวอร์", + "dialog.server.default.set": "ตั้งเซิร์ฟเวอร์ปัจจุบันเป็นค่าเริ่มต้น", + "dialog.server.default.clear": "ล้าง", + "dialog.server.action.remove": "เอาเซิร์ฟเวอร์ออก", + + "dialog.server.menu.edit": "แก้ไข", + "dialog.server.menu.default": "ตั้งเป็นค่าเริ่มต้น", + "dialog.server.menu.defaultRemove": "เอาค่าเริ่มต้นออก", + "dialog.server.menu.delete": "ลบ", + "dialog.server.current": "เซิร์ฟเวอร์ปัจจุบัน", + "dialog.server.status.default": "ค่าเริ่มต้น", + "wsl.server.add": "เพิ่มเซิร์ฟเวอร์ WSL", + "wsl.server.addShort": "เพิ่ม WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "เซิร์ฟเวอร์ WSL", + "wsl.server.retryStart": "ลองเริ่มอีกครั้ง", + "wsl.server.updating": "กำลังอัปเดต...", + "wsl.onboarding.step.distro": "เลือกดิสโทร", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "กำลังตรวจสอบ WSL...", + "wsl.onboarding.restartRequired": "ต้องรีสตาร์ท Windows เพื่อให้การติดตั้ง WSL เสร็จสมบูรณ์", + "wsl.onboarding.ready": "WSL พร้อมใช้งานแล้ว", + "wsl.onboarding.required": "ต้องมี WSL เพื่อดำเนินการต่อ", + "wsl.onboarding.checkingDistros": "กำลังตรวจสอบดิสโทร...", + "wsl.onboarding.installingDistro": "กำลังติดตั้ง {{distro}}...", + "wsl.onboarding.checkingDistro": "กำลังตรวจสอบ {{distro}}...", + "wsl.onboarding.listingDistros": "กำลังแสดงรายการดิสโทร...", + "wsl.onboarding.distroReady": "{{distro}} พร้อมใช้งานแล้ว", + "wsl.onboarding.distroNotInstalled": "ยังไม่ได้ติดตั้ง {{distro}}", + "wsl.onboarding.openDistroOnce": "เปิด {{distro}} หนึ่งครั้งเพื่อให้การตั้งค่าเสร็จสมบูรณ์", + "wsl.onboarding.finishingDistro": "กำลังตั้งค่า {{distro}} ให้เสร็จสมบูรณ์", + "wsl.onboarding.pickDistro": "เลือกดิสโทรหรือติดตั้งจากรายการด้านล่าง", + "wsl.onboarding.checkingOpencode": "กำลังตรวจสอบ OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "กำลังตรวจสอบ OpenCode ใน {{distro}}...", + "wsl.onboarding.updatingOpencode": "กำลังอัปเดต OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "กำลังอัปเดต OpenCode ใน {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "อัปเดต OpenCode ใน {{distro}}", + "wsl.onboarding.updateOpencode": "อัปเดต OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode พร้อมใช้งานใน {{distro}} แล้ว", + "wsl.onboarding.opencodeReady": "OpenCode พร้อมใช้งานแล้ว", + "wsl.onboarding.installOpencodeIn": "ติดตั้ง OpenCode ใน {{distro}}", + "wsl.onboarding.installOpencode": "ติดตั้ง OpenCode", + "wsl.onboarding.chooseDistroFirst": "เลือกดิสโทรก่อน", + "wsl.onboarding.loadFailed": "ไม่สามารถโหลดสถานะ WSL ได้", + "wsl.onboarding.loading": "กำลังโหลด...", + "wsl.onboarding.installedDistros": "ดิสโทรที่ติดตั้งแล้ว", + "wsl.onboarding.checkAgain": "ตรวจสอบอีกครั้ง", + "wsl.onboarding.distroStatus.ready": "พร้อมใช้งาน", + "wsl.onboarding.distroStatus.checking": "กำลังตรวจสอบ...", + "wsl.onboarding.distroStatus.opencodeMissing": "ยังไม่ได้ติดตั้ง OpenCode", + "wsl.onboarding.distroStatus.missingTools": "ไม่มี bash, curl", + "wsl.onboarding.distroStatus.unsupported": "ไม่รองรับ · ใช้ WSL 2", + "wsl.onboarding.needAnotherDistro": "ต้องการดิสโทรอื่น?", + "wsl.onboarding.needAnotherDistroHint": "ติดตั้งดิสทริบิวชัน Linux จากแค็ตตาล็อก WSL", + "wsl.onboarding.wslNotInstalled.title": "ยังไม่ได้ติดตั้ง WSL", + "wsl.onboarding.wslNotInstalled.description": + "ต้องมี WSL (Windows Subsystem for Linux) ก่อนที่ OpenCode จะเพิ่มเซิร์ฟเวอร์ WSL ได้", + "wsl.onboarding.wslUnavailable.title": "WSL ไม่พร้อมใช้งาน", + "wsl.onboarding.wslUnavailable.description": "OpenCode ไม่สามารถตรวจสอบ WSL บนเครื่องนี้ได้", + "wsl.onboarding.installWsl": "ติดตั้ง WSL", + "wsl.onboarding.windowsRestartRequired": + "รีสตาร์ท Windows เพื่อให้การติดตั้ง WSL เสร็จสมบูรณ์ แล้วเปิด OpenCode อีกครั้ง", + "wsl.onboarding.next": "ถัดไป", + "wsl.onboarding.refresh": "รีเฟรช", + "wsl.onboarding.allDistrosAdded": "เพิ่มดิสโทรที่ติดตั้งไว้ทั้งหมดแล้ว", + "wsl.onboarding.noDistros": "ยังไม่พบดิสโทร", + "wsl.onboarding.install": "ติดตั้ง", + "wsl.onboarding.installing": "กำลังติดตั้ง...", + "wsl.onboarding.installDistro": "ติดตั้งดิสโทร", + "wsl.onboarding.searchDistros": "ค้นหาดิสโทร", + "wsl.onboarding.wsl2Required": "ต้องใช้ WSL 2", + "wsl.onboarding.toolsRequired": "ดิสโทรนี้ต้องมี bash และ curl", + "wsl.onboarding.openTerminal": "เปิดเทอร์มินัล", + "wsl.onboarding.path": "เส้นทาง: {{path}}", + "wsl.onboarding.notFound": "ไม่พบ", + "wsl.onboarding.version": "เวอร์ชัน: {{version}}", + "wsl.onboarding.unknown": "ไม่ทราบ", + "wsl.onboarding.desktopVersion": "เดสก์ท็อป {{version}}", + "wsl.onboarding.versionMismatch": "เวอร์ชันที่ติดตั้งไม่ตรงกับเวอร์ชันของแอปเดสก์ท็อป", + "wsl.onboarding.adding": "กำลังเพิ่ม...", + + "help.tabs.toast.ariaLabel": "ขอแนะนำแท็บ จัดระเบียบงานและเซสชันที่ใช้งานอยู่ด้วยแท็บ", + "help.tabs.toast.dismiss": "ปิดข้อมูลเกี่ยวกับแท็บ", + "help.tabs.title": "ขอแนะนำแท็บ", + "help.tabs.description": "จัดระเบียบงานและเซสชันที่ใช้งานอยู่ด้วยแท็บ", + "help.tabs.date": "14 กรกฎาคม", + "help.tabs.introduction": "ตอนนี้ OpenCode Desktop สร้างขึ้นโดยใช้แท็บเป็นหลัก", + "help.tabs.sessions": + "เริ่มเซสชันใหม่ในแท็บ หรือเปิดเซสชันที่มีอยู่จากโปรเจกต์ใดก็ได้ เปิดแท็บใหม่เมื่อเริ่มงานใหม่ และปิดเมื่อทำงานเสร็จ", + "help.tabs.organize": + "การเปิดแท็บไว้สองสามแท็บช่วยให้จัดระเบียบเซสชันที่ใช้งานอยู่ได้ง่ายขึ้น เปลี่ยนชื่อแท็บให้จดจำได้ง่ายหากต้องการเก็บแท็บไว้", + "help.tabs.home": "คุณจะพบเซสชันและโปรเจกต์ทั้งหมดในหน้าจอหน้าหลักแบบใหม่ เมื่อเลือกเซสชัน เซสชันนั้นจะเปิดในแท็บ", + "help.tabs.persistence": "เมื่อเปิดแอปอีกครั้ง แท็บของคุณจะยังคงเปิดอยู่", + "help.tabs.worktrees": + "การออกแบบใหม่ยังไม่รองรับ Git Worktrees แต่จะรองรับเร็ว ๆ นี้ ดังนั้น หากต้องการใช้เค้าโครงก่อนหน้าต่อ คุณสามารถสลับเค้าโครงได้ในการตั้งค่า โปรดทราบว่าเค้าโครงใหม่จะกลายเป็นเค้าโครงถาวรภายในอีกไม่กี่สัปดาห์", + + "dialog.project.edit.title": "แก้ไขโปรเจกต์", + "dialog.project.edit.name": "ชื่อ", + "dialog.project.edit.icon": "ไอคอน", + "dialog.project.edit.icon.alt": "ไอคอนโปรเจกต์", + "dialog.project.edit.icon.hint": "คลิกหรือลากรูปภาพ", + "dialog.project.edit.icon.recommended": "แนะนำ: 128x128px", + "dialog.project.edit.color": "สี", + "dialog.project.edit.color.select": "เลือกสี {{color}}", + "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": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด', + "context.breakdown.system": "ระบบ", + "context.breakdown.user": "ผู้ใช้", + "context.breakdown.assistant": "ผู้ช่วย", + "context.breakdown.tool": "การเรียกเครื่องมือ", + "context.breakdown.other": "อื่น ๆ", + + "context.systemPrompt.title": "พรอมต์ระบบ", + "context.rawMessages.title": "ข้อความดิบ", + + "context.export.session": "ส่งออกเซสชัน", + + "context.stats.session": "เซสชัน", + "context.stats.messages": "ข้อความ", + "context.stats.provider": "ผู้ให้บริการ", + "context.stats.model": "โมเดล", + "context.stats.limit": "ขีดจำกัดบริบท", + "context.stats.totalTokens": "โทเค็นทั้งหมด", + "context.stats.usage": "การใช้งาน", + "context.stats.inputTokens": "โทเค็นนำเข้า", + "context.stats.outputTokens": "โทเค็นส่งออก", + "context.stats.reasoningTokens": "โทเค็นการใช้เหตุผล", + "context.stats.cacheTokens": "โทเค็นแคช (อ่าน/เขียน)", + "context.stats.userMessages": "ข้อความผู้ใช้", + "context.stats.assistantMessages": "ข้อความผู้ช่วย", + "context.stats.totalCost": "ต้นทุนทั้งหมด", + "context.stats.sessionCreated": "สร้างเซสชันเมื่อ", + "context.stats.lastActivity": "กิจกรรมล่าสุด", + + "context.usage.tokens": "โทเค็น", + "context.usage.usage": "การใช้งาน", + "context.usage.cost": "ต้นทุน", + "context.usage.clickToView": "คลิกเพื่อดูบริบท", + "context.usage.view": "ดูการใช้บริบท", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "ภาษา", + "toast.language.description": "สลับไปที่ {{language}}", + + "toast.theme.title": "สลับธีมแล้ว", + "toast.scheme.title": "โทนสี", + + "toast.workspace.enabled.title": "เปิดใช้งานพื้นที่ทำงานแล้ว", + "toast.workspace.enabled.description": "ตอนนี้จะแสดง worktree หลายรายการในแถบด้านข้าง", + "toast.workspace.disabled.title": "ปิดใช้งานพื้นที่ทำงานแล้ว", + "toast.workspace.disabled.description": "จะแสดงเฉพาะ worktree หลักในแถบด้านข้าง", + + "toast.permissions.autoaccept.on.title": "กำลังยอมรับสิทธิ์โดยอัตโนมัติ", + "toast.permissions.autoaccept.on.description": "คำขอสิทธิ์จะได้รับการอนุมัติโดยอัตโนมัติ", + "toast.permissions.autoaccept.off.title": "หยุดยอมรับสิทธิ์โดยอัตโนมัติแล้ว", + "toast.permissions.autoaccept.off.description": "คำขอสิทธิ์จะต้องได้รับการอนุมัติ", + + "toast.model.none.title": "ไม่ได้เลือกโมเดล", + "toast.model.none.description": "เชื่อมต่อผู้ให้บริการเพื่อสรุปเซสชันนี้", + + "toast.file.loadFailed.title": "ไม่สามารถโหลดไฟล์", + "toast.file.listFailed.title": "ไม่สามารถแสดงรายการไฟล์", + + "toast.context.noLineSelection.title": "ไม่มีการเลือกบรรทัด", + "toast.context.noLineSelection.description": "เลือกช่วงบรรทัดในแท็บไฟล์ก่อน", + + "toast.session.share.copyFailed.title": "ไม่สามารถคัดลอก URL ไปยังคลิปบอร์ด", + "toast.session.share.success.title": "แชร์เซสชันแล้ว", + "toast.session.share.success.description": "คัดลอก URL แชร์ไปยังคลิปบอร์ดแล้ว!", + "toast.session.share.failed.title": "ไม่สามารถแชร์เซสชัน", + "toast.session.share.failed.description": "เกิดข้อผิดพลาดระหว่างการแชร์เซสชัน", + + "toast.session.unshare.success.title": "ยกเลิกการแชร์เซสชันแล้ว", + "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}}) พร้อมใช้งานสำหรับติดตั้ง", + "toast.update.action.installRestart": "ติดตั้งและรีสตาร์ท", + "toast.update.action.notYet": "ยังไม่", + + "error.page.title": "เกิดข้อผิดพลาด", + "error.page.description": "เกิดข้อผิดพลาดระหว่างการโหลดแอปพลิเคชัน", + "error.page.description.localServerStartup": "เกิดข้อผิดพลาดขณะเริ่มเซิร์ฟเวอร์ในเครื่อง", + "error.page.details.label": "รายละเอียดข้อผิดพลาด", + "error.page.action.restart": "รีสตาร์ท", + "error.page.action.report": "รายงานข้อผิดพลาด", + "error.page.action.reported": "รายงานข้อผิดพลาดแล้ว", + "error.page.action.exportLogs": "ส่งออกบันทึก", + "error.page.action.checking": "กำลังตรวจสอบ...", + "error.page.action.checkUpdates": "ตรวจสอบการอัปเดต", + "error.page.action.updateTo": "อัปเดตเป็น {{version}}", + "error.page.report.prefix": "โปรดรายงานข้อผิดพลาดนี้ให้ทีม OpenCode", + "error.page.report.discord": "บน Discord", + "error.page.version": "เวอร์ชัน: {{version}}", + + "error.dev.rootNotFound": "ไม่พบองค์ประกอบรูท คุณลืมเพิ่มใน index.html หรือบางทีแอตทริบิวต์ id อาจสะกดผิด?", + + "error.serverSync.connectFailed": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ มีเซิร์ฟเวอร์ทำงานอยู่ที่ `{{url}}` หรือไม่?", + "directory.error.invalidUrl": "ไดเรกทอรีใน URL ไม่ถูกต้อง", + + "error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก", + "error.chain.causedBy": "สาเหตุ:", + "error.chain.apiError": "ข้อผิดพลาด API", + "error.chain.status": "สถานะ: {{status}}", + "error.chain.retryable": "สามารถลองใหม่: {{retryable}}", + "error.chain.responseBody": "เนื้อหาการตอบสนอง:\n{{body}}", + "error.chain.didYouMean": "คุณหมายถึง: {{suggestions}}", + "error.chain.modelNotFound": "ไม่พบโมเดล: {{provider}}/{{model}}", + "error.chain.checkConfig": "ตรวจสอบการกำหนดค่าของคุณ (opencode.json) ชื่อผู้ให้บริการ/โมเดล", + "error.chain.mcpFailed": 'เซิร์ฟเวอร์ MCP "{{name}}" ล้มเหลว โปรดทราบว่า OpenCode ยังไม่รองรับการตรวจสอบสิทธิ์ MCP', + "error.chain.providerAuthFailed": "การตรวจสอบสิทธิ์ผู้ให้บริการล้มเหลว ({{provider}}): {{message}}", + "error.chain.providerInitFailed": 'ไม่สามารถเริ่มต้นผู้ให้บริการ "{{provider}}" ตรวจสอบข้อมูลรับรองและการกำหนดค่า', + "error.chain.configJsonInvalid": "ไฟล์กำหนดค่าที่ {{path}} ไม่ใช่ JSON(C) ที่ถูกต้อง", + "error.chain.configJsonInvalidWithMessage": "ไฟล์กำหนดค่าที่ {{path}} ไม่ใช่ JSON(C) ที่ถูกต้อง: {{message}}", + "error.chain.configDirectoryTypo": + 'ไดเรกทอรี "{{dir}}" ใน {{path}} ไม่ถูกต้อง เปลี่ยนชื่อไดเรกทอรีเป็น "{{suggestion}}" หรือเอาออก นี่เป็นการสะกดผิดทั่วไป', + "error.chain.configFrontmatterError": "ไม่สามารถแยกวิเคราะห์ frontmatter ใน {{path}}:\n{{message}}", + "error.chain.configInvalid": "ไฟล์กำหนดค่าที่ {{path}} ไม่ถูกต้อง", + "error.chain.configInvalidWithMessage": "ไฟล์กำหนดค่าที่ {{path}} ไม่ถูกต้อง: {{message}}", + + "notification.permission.title": "ต้องการสิทธิ์", + "notification.permission.description": "{{sessionTitle}} ใน {{projectName}} ต้องการสิทธิ์", + "notification.question.title": "คำถาม", + "notification.question.description": "{{sessionTitle}} ใน {{projectName}} มีคำถาม", + "notification.action.goToSession": "ไปที่เซสชัน", + + "notification.session.responseReady.title": "คำตอบพร้อมแล้ว", + "notification.session.error.title": "ข้อผิดพลาดเซสชัน", + "notification.session.error.fallbackDescription": "เกิดข้อผิดพลาด", + + "home.recentProjects": "โปรเจกต์ล่าสุด", + "home.empty.title": "ไม่มีโปรเจกต์ล่าสุด", + "home.empty.description": "เริ่มต้นโดยเปิดโปรเจกต์ในเครื่อง", + "home.title": "หน้าหลัก", + "home.projects": "โปรเจกต์", + "home.project.add": "เพิ่มโปรเจกต์", + "home.recentlyClosed": "ที่ปิดไปล่าสุด", + "home.server.collapse": "ย่อโปรเจกต์ของเซิร์ฟเวอร์", + "home.server.expand": "ขยายโปรเจกต์ของเซิร์ฟเวอร์", + "home.sessions.search.placeholder": "ค้นหาเซสชัน", + "home.sessions.search.placeholder.scoped": "ค้นหาเซสชันใน {{scope}}", + "home.sessions.search.sessions": "เซสชัน", + "home.sessions.search.noResults": "ไม่พบเซสชันสำหรับ {{query}}", + "home.sessions.empty": "ยังไม่มีอะไรที่นี่", + "home.sessions.empty.description": "สร้างเซสชันเพื่อเริ่มต้น", + "home.sessions.group.today": "วันนี้", + "home.sessions.group.yesterday": "เมื่อวาน", + "home.sessions.group.older": "ก่อนหน้านี้", + "home.providerTip": "เชื่อมต่อกับผู้ให้บริการกว่า 75 รายเพื่อใช้โมเดลอื่น ๆ รวมถึง Claude, GPT, Gemini และอีกมากมาย", + + "session.tab.session": "เซสชัน", + "session.tab.review": "ตรวจสอบ", + "session.tab.context": "บริบท", + "session.tab.unknown": "เซสชันที่ไม่รู้จัก", + "session.panel.reviewAndFiles": "ตรวจสอบและไฟล์", + "session.error.notFound": "ไม่พบเซสชันนี้", + "session.error.notFound.description": "แท็บนี้อ้างถึงเซสชันที่ไม่มีอยู่บนเซิร์ฟเวอร์นี้แล้ว", + "session.error.notFound.closeTab": "ปิดแท็บ", + "session.error.serverConnection": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์นี้", + "session.review.filesChanged": "{{count}} ไฟล์ที่เปลี่ยนแปลง", + "session.review.change.one": "การเปลี่ยนแปลง", + "session.review.change.other": "การเปลี่ยนแปลง", + "session.review.loadingChanges": "กำลังโหลดการเปลี่ยนแปลง...", + "session.review.empty": "ยังไม่มีการเปลี่ยนแปลงในเซสชันนี้", + "session.review.noVcs": "ไม่ตรวจพบระบบควบคุมเวอร์ชัน Git การเปลี่ยนแปลงจะไม่แสดง", + "session.review.noSnapshot": "การติดตามสแนปชอตถูกปิดใช้งานในการกำหนดค่า ดังนั้นการเปลี่ยนแปลงเซสชันจึงไม่พร้อมใช้งาน", + "session.review.noChanges": "ไม่มีการเปลี่ยนแปลง", + "session.review.noUncommittedChanges": "ยังไม่มีการเปลี่ยนแปลงที่รอคอมมิต", + "session.review.noBranchChanges": "ยังไม่มีการเปลี่ยนแปลงในสาขา", + + "session.files.selectToOpen": "เลือกไฟล์เพื่อเปิด", + "session.files.empty": "ไม่มีไฟล์", + "session.files.all": "ไฟล์ทั้งหมด", + "session.files.binaryContent": "ไฟล์ไบนารี (ไม่สามารถแสดงเนื้อหาได้)", + + "session.messages.renderEarlier": "แสดงข้อความก่อนหน้า", + "session.messages.loadingEarlier": "กำลังโหลดข้อความก่อนหน้า...", + "session.messages.loadEarlier": "โหลดข้อความก่อนหน้า", + "session.messages.loading": "กำลังโหลดข้อความ...", + "session.messages.jumpToLatest": "ไปที่ล่าสุด", + + "session.context.addToContext": "เพิ่ม {{selection}} ไปยังบริบท", + "session.todo.title": "สิ่งที่ต้องทำ", + "session.todo.collapse": "ย่อ", + "session.todo.expand": "ขยาย", + "session.question.minimize": "ย่อคำถาม", + "session.question.restore": "คืนค่าคำถาม", + "session.question.pending.one": "คำถามที่รอดำเนินการ {{count}} ข้อ", + "session.question.pending.other": "คำถามที่รอดำเนินการ {{count}} ข้อ", + "session.followupDock.summary.one": "{{count}} ข้อความในคิว", + "session.followupDock.summary.other": "{{count}} ข้อความในคิว", + "session.followupDock.sendNow": "ส่งทันที", + "session.followupDock.edit": "แก้ไข", + "session.followupDock.collapse": "ย่อข้อความในคิว", + "session.followupDock.expand": "ขยายข้อความในคิว", + "session.revertDock.summary.one": "{{count}} ข้อความที่ถูกย้อนกลับ", + "session.revertDock.summary.other": "{{count}} ข้อความที่ถูกย้อนกลับ", + "session.revertDock.collapse": "ย่อข้อความที่ถูกย้อนกลับ", + "session.revertDock.expand": "ขยายข้อความที่ถูกย้อนกลับ", + "session.revertDock.restore": "กู้คืนข้อความ", + + "session.new.title": "สร้างอะไรก็ได้", + "session.new.project.new": "โปรเจกต์ใหม่", + "session.new.project.search": "ค้นหาโปรเจกต์", + "session.new.project.add": "เพิ่มโปรเจกต์", + "session.new.worktree.main": "สาขาหลัก", + "session.new.worktree.mainWithBranch": "สาขาหลัก ({{branch}})", + "session.new.worktree.create": "สร้าง worktree ใหม่", + "session.new.workspace.runIn": "เรียกใช้เซสชันใน", + "session.new.workspace.triggerLocal": "ในเครื่อง", + "session.new.workspace.local": "รีโพซิทอรีในเครื่อง", + "session.new.workspace.existing": "พื้นที่ทำงาน…", + "session.new.git.none": "ไม่มี Git", + "session.new.lastModified": "แก้ไขล่าสุด", + + "session.header.search.placeholder": "ค้นหา {{project}}", + "session.header.searchFiles": "ค้นหาไฟล์", + "session.header.openIn": "เปิดใน", + "session.header.open.action": "เปิด {{app}}", + "session.header.open.ariaLabel": "เปิดใน {{app}}", + "session.header.open.menu": "ตัวเลือกการเปิด", + "session.header.open.copyPath": "คัดลอกเส้นทาง", + + "status.popover.trigger": "สถานะ", + "status.popover.ariaLabel": "การกำหนดค่าเซิร์ฟเวอร์", + "status.popover.tab.servers": "เซิร์ฟเวอร์", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "ปลั๊กอิน", + "status.popover.action.manageServers": "จัดการเซิร์ฟเวอร์", + + "session.share.popover.title": "เผยแพร่บนเว็บ", + "session.share.popover.description.shared": "เซสชันนี้เป็นสาธารณะบนเว็บ สามารถเข้าถึงได้โดยผู้ที่มีลิงก์", + "session.share.popover.description.unshared": "แชร์เซสชันสาธารณะบนเว็บ จะเข้าถึงได้โดยผู้ที่มีลิงก์", + "session.share.action.share": "แชร์", + "session.share.action.publish": "เผยแพร่", + "session.share.action.publishing": "กำลังเผยแพร่...", + "session.share.action.unpublish": "ยกเลิกการเผยแพร่", + "session.share.action.unpublishing": "กำลังยกเลิกการเผยแพร่...", + "session.share.action.view": "ดู", + "session.share.copy.copied": "คัดลอกแล้ว", + "session.share.copy.copyLink": "คัดลอกลิงก์", + + "lsp.tooltip.none": "ไม่มีเซิร์ฟเวอร์ LSP", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "กำลังโหลดพรอมต์...", + "terminal.loading": "กำลังโหลดเทอร์มินัล...", + "terminal.title": "เทอร์มินัล", + "terminal.title.numbered": "เทอร์มินัล {{number}}", + "terminal.close": "ปิดเทอร์มินัล", + "terminal.connectionLost.title": "การเชื่อมต่อขาดหาย", + "terminal.connectionLost.description": "การเชื่อมต่อเทอร์มินัลถูกขัดจังหวะ อาจเกิดขึ้นเมื่อเซิร์ฟเวอร์รีสตาร์ท", + "terminal.connectTicket.csrfError": + "ตั๋วเชื่อมต่อ PTY ถูกปฏิเสธโดยการตรวจสอบต้นทางหรือ CSRF โปรดตรวจสอบการกำหนดค่า CORS ของเซิร์ฟเวอร์", + "terminal.connectTicket.statusError": "ตั๋วเชื่อมต่อ PTY ล้มเหลวโดยมีสถานะ {{status}}", + + "titlebar.update": "อัปเดต", + "titlebar.updateVersion": "อัปเดต {{version}}", + + "common.closeTab": "ปิดแท็บ", + "common.dismiss": "ปิด", + "common.requestFailed": "คำขอล้มเหลว", + "common.moreOptions": "ตัวเลือกเพิ่มเติม", + "common.learnMore": "เรียนรู้เพิ่มเติม", + "common.rename": "เปลี่ยนชื่อ", + "common.export": "ส่งออก", + "common.reset": "รีเซ็ต", + "common.archive": "จัดเก็บ", + "common.delete": "ลบ", + "common.close": "ปิด", + "common.edit": "แก้ไข", + "common.loadMore": "โหลดเพิ่มเติม", + "common.key.esc": "ESC", + + "sidebar.menu.toggle": "สลับเมนู", + "sidebar.nav.projectsAndSessions": "โปรเจกต์และเซสชัน", + "sidebar.settings": "การตั้งค่า", + "sidebar.help": "ช่วยเหลือ", + "sidebar.workspaces.enable": "เปิดใช้งานพื้นที่ทำงาน", + "sidebar.workspaces.disable": "ปิดใช้งานพื้นที่ทำงาน", + "sidebar.gettingStarted.title": "เริ่มต้นใช้งาน", + "sidebar.gettingStarted.line1": "OpenCode รวมถึงโมเดลฟรีเพื่อให้คุณเริ่มต้นได้ทันที", + "sidebar.gettingStarted.line2": "เชื่อมต่อผู้ให้บริการใด ๆ เพื่อใช้โมเดล รวมถึง Claude, GPT, Gemini ฯลฯ", + "sidebar.project.recentSessions": "เซสชันล่าสุด", + "sidebar.project.viewAllSessions": "ดูเซสชันทั้งหมด", + "sidebar.project.clearNotifications": "ล้างการแจ้งเตือน", + "sidebar.empty.title": "ไม่มีโปรเจกต์ที่เปิดอยู่", + "sidebar.empty.description": "เปิดโปรเจกต์เพื่อเริ่มต้น", + + "app.name.desktop": "OpenCode Desktop", + + "settings.section.desktop": "เดสก์ท็อป", + "settings.section.server": "เซิร์ฟเวอร์", + "settings.tab.general": "ทั่วไป", + "settings.tab.shortcuts": "ทางลัด", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "การรวม WSL", + "settings.desktop.wsl.description": "เรียกใช้เซิร์ฟเวอร์ OpenCode ภายใน WSL บน Windows", + + "settings.general.section.appearance": "รูปลักษณ์", + "settings.general.section.advanced": "ขั้นสูง", + "settings.general.section.notifications": "การแจ้งเตือนระบบ", + "settings.general.section.updates": "การอัปเดต", + "settings.general.section.sounds": "เสียงเอฟเฟกต์", + "settings.general.section.feed": "ฟีด", + "settings.general.section.display": "การแสดงผล", + + "settings.general.row.language.title": "ภาษา", + "settings.general.row.language.description": "เปลี่ยนภาษาที่แสดงสำหรับ OpenCode", + "settings.general.row.shell.title": "เชลล์เทอร์มินัล", + "settings.general.row.shell.description": + "เลือกเชลล์ที่จะใช้กับเทอร์มินัล เชลล์ที่เข้ากันได้จะใช้สำหรับการเรียกเครื่องมือของเอเจนต์ด้วย", + "settings.general.row.shell.autoDefault": "อัตโนมัติ (ค่าเริ่มต้น)", + "settings.general.row.shell.terminalOnly": "เทอร์มินัลเท่านั้น", + "settings.general.row.appearance.title": "รูปลักษณ์", + "settings.general.row.appearance.description": "ปรับแต่งรูปลักษณ์ของ OpenCode บนอุปกรณ์ของคุณ", + "settings.general.row.colorScheme.title": "โทนสี", + "settings.general.row.colorScheme.description": "เลือกว่าจะให้ OpenCode ใช้ธีมตามระบบ สว่าง หรือมืด", + "settings.general.row.theme.title": "ธีม", + "settings.general.row.theme.description": "ปรับแต่งธีมของ OpenCode", + "settings.general.row.font.title": "แบบอักษรโค้ด", + "settings.general.row.font.description": "ปรับแต่งแบบอักษรที่ใช้ในบล็อกโค้ด", + "settings.general.row.terminalFont.title": "แบบอักษรเทอร์มินัล", + "settings.general.row.terminalFont.description": "ปรับแต่งแบบอักษรที่ใช้ในเทอร์มินัล", + "settings.general.row.uiFont.title": "แบบอักษร UI", + "settings.general.row.uiFont.description": "ปรับแต่งแบบอักษรที่ใช้ทั่วทั้งอินเทอร์เฟซ", + "settings.general.row.followup.title": "พฤติกรรมการติดตามผล", + "settings.general.row.followup.description": "เลือกว่าจะให้พรอมต์ติดตามผลทำงานทันทีหรือรอในคิว", + "settings.general.row.followup.option.queue": "คิว", + "settings.general.row.followup.option.steer": "นำทาง", + "settings.general.row.showFileTree.title": "แผนผังไฟล์", + "settings.general.row.showFileTree.description": "แสดงแผงแผนผังไฟล์ในเซสชัน", + "settings.general.row.showNavigation.title": "ตัวควบคุมการนำทาง", + "settings.general.row.showNavigation.description": "แสดงปุ่มย้อนกลับและไปข้างหน้าในแถบชื่อเรื่องเดสก์ท็อป", + "settings.general.row.showSearch.title": "ชุดคำสั่ง", + "settings.general.row.showSearch.description": "แสดงปุ่มค้นหาและชุดคำสั่งในแถบชื่อเรื่อง", + "settings.general.row.showTerminal.title": "เทอร์มินัล", + "settings.general.row.showTerminal.description": "แสดงปุ่มเทอร์มินัลในแถบชื่อเรื่องเดสก์ท็อป", + "settings.general.row.showStatus.title": "สถานะเซิร์ฟเวอร์", + "settings.general.row.showStatus.description": "แสดงปุ่มสถานะเซิร์ฟเวอร์ในแถบชื่อเรื่อง", + "settings.general.row.mobileTitlebarBottom.title": "การนำทางด้านล่าง", + "settings.general.row.mobileTitlebarBottom.description": + "วางแถบชื่อเรื่องและแท็บเซสชันไว้ด้านล่างของหน้าจอบนอุปกรณ์เคลื่อนที่", + "settings.general.row.showCustomAgents.title": "แสดงเอเจนต์", + "settings.general.row.showCustomAgents.description": + "สลับระหว่างเอเจนต์ในช่องเขียนข้อความ เมื่อซ่อน ระบบจะใช้เอเจนต์ Build เป็นค่าเริ่มต้น", + "settings.general.row.reasoningSummaries.title": "แสดงสรุปการใช้เหตุผล", + "settings.general.row.reasoningSummaries.description": "แสดงสรุปการใช้เหตุผลของโมเดลในไทม์ไลน์", + "settings.general.row.shellToolPartsExpanded.title": "ขยายส่วนเครื่องมือ shell", + "settings.general.row.shellToolPartsExpanded.description": "แสดงส่วนเครื่องมือ shell แบบขยายตามค่าเริ่มต้นในไทม์ไลน์", + "settings.general.row.editToolPartsExpanded.title": "ขยายส่วนเครื่องมือ edit", + "settings.general.row.editToolPartsExpanded.description": + "แสดงส่วนเครื่องมือ edit, write และ patch แบบขยายตามค่าเริ่มต้นในไทม์ไลน์", + "settings.general.row.newInterface.title": "เลย์เอาต์ใหม่", + "settings.general.row.newInterface.badge": "ใหม่", + "settings.general.row.newInterface.description": + "ใช้แท็บใหม่และเลย์เอาต์หน้าแรก คุณสามารถสลับระหว่างเลย์เอาต์ได้ในช่วงเวลาจำกัด", + "settings.general.row.newInterfaceNotice.title": "ขณะนี้คุณกำลังใช้เลย์เอาต์ใหม่", + "settings.general.row.newInterfaceNotice.description": "เลย์เอาต์ก่อนหน้าไม่พร้อมใช้งานอีกต่อไป", + "settings.general.row.newInterfaceNotice.dismiss": "ปิด", + "settings.general.row.pinchZoom.title": "บีบนิ้วเพื่อซูม", + "settings.general.row.pinchZoom.description": "อนุญาตให้ใช้ท่าบีบนิ้วบนแทร็คแพดและ Ctrl-scroll เพื่อซูม", + "settings.general.row.wayland.title": "ใช้ Wayland แบบเนทีฟ", + "settings.general.row.wayland.description": "ปิดใช้งาน X11 fallback บน Wayland ต้องรีสตาร์ท", + "settings.general.row.wayland.tooltip": "บน Linux ที่มีจอภาพรีเฟรชเรตแบบผสม Wayland แบบเนทีฟอาจเสถียรกว่า", + + "settings.general.row.releaseNotes.title": "บันทึกการอัปเดต", + "settings.general.row.releaseNotes.description": "แสดงป๊อปอัพ What's New หลังจากอัปเดต", + + "settings.updates.row.startup.title": "ตรวจสอบการอัปเดตเมื่อเริ่มต้น", + "settings.updates.row.startup.description": "ตรวจสอบการอัปเดตโดยอัตโนมัติเมื่อ OpenCode เปิดใช้งาน", + "settings.updates.row.check.title": "ตรวจสอบการอัปเดต", + "settings.updates.row.check.description": "ตรวจสอบการอัปเดตด้วยตนเองและติดตั้งหากมี", + "settings.updates.action.checkNow": "ตรวจสอบทันที", + "settings.updates.action.checking": "กำลังตรวจสอบ...", + "settings.updates.action.downloading": "กำลังดาวน์โหลด...", + "settings.updates.action.installing": "กำลังติดตั้ง...", + "settings.updates.toast.latest.title": "คุณเป็นเวอร์ชันล่าสุดแล้ว", + "settings.updates.toast.latest.description": "คุณกำลังใช้งาน OpenCode เวอร์ชันล่าสุด", + + "sound.option.none": "ไม่มี", + "sound.option.alert01": "เสียงเตือน 01", + "sound.option.alert02": "เสียงเตือน 02", + "sound.option.alert03": "เสียงเตือน 03", + "sound.option.alert04": "เสียงเตือน 04", + "sound.option.alert05": "เสียงเตือน 05", + "sound.option.alert06": "เสียงเตือน 06", + "sound.option.alert07": "เสียงเตือน 07", + "sound.option.alert08": "เสียงเตือน 08", + "sound.option.alert09": "เสียงเตือน 09", + "sound.option.alert10": "เสียงเตือน 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + + "settings.general.notifications.agent.title": "เอเจนต์", + "settings.general.notifications.agent.description": "แสดงการแจ้งเตือนระบบเมื่อเอเจนต์เสร็จสิ้นหรือต้องการความสนใจ", + "settings.general.notifications.permissions.title": "สิทธิ์", + "settings.general.notifications.permissions.description": "แสดงการแจ้งเตือนระบบเมื่อต้องการสิทธิ์", + "settings.general.notifications.errors.title": "ข้อผิดพลาด", + "settings.general.notifications.errors.description": "แสดงการแจ้งเตือนระบบเมื่อเกิดข้อผิดพลาด", + + "settings.general.sounds.agent.title": "เอเจนต์", + "settings.general.sounds.agent.description": "เล่นเสียงเมื่อเอเจนต์เสร็จสิ้นหรือต้องการความสนใจ", + "settings.general.sounds.permissions.title": "สิทธิ์", + "settings.general.sounds.permissions.description": "เล่นเสียงเมื่อต้องการสิทธิ์", + "settings.general.sounds.errors.title": "ข้อผิดพลาด", + "settings.general.sounds.errors.description": "เล่นเสียงเมื่อเกิดข้อผิดพลาด", + + "settings.shortcuts.title": "ทางลัดแป้นพิมพ์", + "settings.shortcuts.reset.button": "รีเซ็ตเป็นค่าเริ่มต้น", + "settings.shortcuts.reset.toast.title": "รีเซ็ตทางลัดแล้ว", + "settings.shortcuts.reset.toast.description": "รีเซ็ตทางลัดแป้นพิมพ์เป็นค่าเริ่มต้นแล้ว", + "settings.shortcuts.conflict.title": "ทางลัดใช้งานอยู่แล้ว", + "settings.shortcuts.conflict.description": "{{keybind}} ถูกกำหนดให้กับ {{titles}} แล้ว", + "settings.shortcuts.unassigned": "ไม่ได้กำหนด", + "settings.shortcuts.pressKeys": "กดปุ่ม", + "settings.shortcuts.search.placeholder": "ค้นหาทางลัด", + "settings.shortcuts.search.empty": "ไม่พบทางลัด", + + "settings.shortcuts.group.general": "ทั่วไป", + "settings.shortcuts.group.session": "เซสชัน", + "settings.shortcuts.group.navigation": "การนำทาง", + "settings.shortcuts.group.modelAndAgent": "โมเดลและเอเจนต์", + "settings.shortcuts.group.terminal": "เทอร์มินัล", + "settings.shortcuts.group.prompt": "พรอมต์", + + "settings.providers.title": "ผู้ให้บริการ", + "settings.providers.description": "การตั้งค่าผู้ให้บริการจะสามารถกำหนดค่าได้ที่นี่", + "settings.providers.section.connected": "ผู้ให้บริการที่เชื่อมต่อ", + "settings.providers.connected.empty": "ไม่มีผู้ให้บริการที่เชื่อมต่อ", + "settings.providers.section.popular": "ผู้ให้บริการยอดนิยม", + "settings.providers.tag.environment": "สภาพแวดล้อม", + "settings.providers.tag.config": "กำหนดค่า", + "settings.providers.tag.custom": "กำหนดเอง", + "settings.providers.tag.other": "อื่น ๆ", + "settings.models.title": "โมเดล", + "settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่", + "settings.agents.title": "เอเจนต์", + "settings.agents.description": "การตั้งค่าเอเจนต์จะสามารถกำหนดค่าได้ที่นี่", + "settings.commands.title": "คำสั่ง", + "settings.commands.description": "การตั้งค่าคำสั่งจะสามารถกำหนดค่าได้ที่นี่", + "settings.mcp.title": "MCP", + "settings.mcp.description": "การตั้งค่า MCP จะสามารถกำหนดค่าได้ที่นี่", + + "settings.permissions.title": "สิทธิ์", + "settings.permissions.description": "ควบคุมเครื่องมือที่เซิร์ฟเวอร์สามารถใช้โดยค่าเริ่มต้น", + "settings.permissions.section.tools": "เครื่องมือ", + "settings.permissions.toast.updateFailed.title": "ไม่สามารถอัปเดตสิทธิ์", + + "settings.permissions.action.allow": "อนุญาต", + "settings.permissions.action.ask": "ถาม", + "settings.permissions.action.deny": "ปฏิเสธ", + + "settings.permissions.tool.read.title": "อ่าน", + "settings.permissions.tool.read.description": "อ่านไฟล์ (ตรงกับเส้นทางไฟล์)", + "settings.permissions.tool.edit.title": "แก้ไข", + "settings.permissions.tool.edit.description": "แก้ไขไฟล์ รวมถึงการแก้ไข เขียน แพตช์ และแก้ไขหลายรายการ", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "จับคู่ไฟล์โดยใช้รูปแบบ glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "ค้นหาเนื้อหาไฟล์โดยใช้นิพจน์ทั่วไป", + "settings.permissions.tool.list.title": "แสดงรายการ", + "settings.permissions.tool.list.description": "แสดงรายการไฟล์ภายในไดเรกทอรี", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "เรียกใช้คำสั่งเชลล์", + "settings.permissions.tool.task.title": "งาน", + "settings.permissions.tool.task.description": "เปิดเอเจนต์ย่อย", + "settings.permissions.tool.skill.title": "ทักษะ", + "settings.permissions.tool.skill.description": "โหลดทักษะตามชื่อ", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "เรียกใช้การสืบค้นเซิร์ฟเวอร์ภาษา", + "settings.permissions.tool.todowrite.title": "เขียนรายการงาน", + "settings.permissions.tool.todowrite.description": "อัปเดตรายการงาน", + "settings.permissions.tool.webfetch.title": "ดึงข้อมูลจากเว็บ", + "settings.permissions.tool.webfetch.description": "ดึงเนื้อหาจาก URL", + "settings.permissions.tool.websearch.title": "ค้นหาเว็บ", + "settings.permissions.tool.websearch.description": "ค้นหาบนเว็บ", + "settings.permissions.tool.external_directory.title": "ไดเรกทอรีภายนอก", + "settings.permissions.tool.external_directory.description": "เข้าถึงไฟล์นอกไดเรกทอรีโปรเจกต์", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "ตรวจจับการเรียกเครื่องมือซ้ำด้วยข้อมูลนำเข้าเหมือนกัน", + + "session.delete.failed.title": "ไม่สามารถลบเซสชัน", + "session.delete.title": "ลบเซสชัน", + "session.delete.confirm": 'ลบเซสชัน "{{name}}" หรือไม่?', + "session.delete.button": "ลบเซสชัน", + + "workspace.new": "พื้นที่ทำงานใหม่", + "workspace.type.local": "ในเครื่อง", + "workspace.type.sandbox": "แซนด์บ็อกซ์", + "workspace.create.failed.title": "ไม่สามารถสร้างพื้นที่ทำงาน", + "workspace.delete.failed.title": "ไม่สามารถลบพื้นที่ทำงาน", + "workspace.resetting.title": "กำลังรีเซ็ตพื้นที่ทำงาน", + "workspace.resetting.description": "อาจใช้เวลาประมาณหนึ่งนาที", + "workspace.reset.failed.title": "ไม่สามารถรีเซ็ตพื้นที่ทำงาน", + "workspace.reset.success.title": "รีเซ็ตพื้นที่ทำงานแล้ว", + "workspace.reset.success.description": "พื้นที่ทำงานตรงกับสาขาเริ่มต้นแล้ว", + "workspace.error.stillPreparing": "พื้นที่ทำงานกำลังเตรียมอยู่", + "workspace.status.checking": "กำลังตรวจสอบการเปลี่ยนแปลงที่ไม่ได้ผสาน...", + "workspace.status.error": "ไม่สามารถตรวจสอบสถานะ git", + "workspace.status.clean": "ไม่ตรวจพบการเปลี่ยนแปลงที่ไม่ได้ผสาน", + "workspace.status.dirty": "ตรวจพบการเปลี่ยนแปลงที่ไม่ได้ผสานในพื้นที่ทำงานนี้", + "workspace.delete.title": "ลบพื้นที่ทำงาน", + "workspace.delete.confirm": 'ลบพื้นที่ทำงาน "{{name}}" หรือไม่?', + "workspace.delete.button": "ลบพื้นที่ทำงาน", + "workspace.reset.title": "รีเซ็ตพื้นที่ทำงาน", + "workspace.reset.confirm": 'รีเซ็ตพื้นที่ทำงาน "{{name}}" หรือไม่?', + "workspace.reset.button": "รีเซ็ตพื้นที่ทำงาน", + "workspace.reset.archived.none": "ไม่มีเซสชันที่ใช้งานอยู่จะถูกจัดเก็บ", + "workspace.reset.archived.one": "1 เซสชันจะถูกจัดเก็บ", + "workspace.reset.archived.many": "{{count}} เซสชันจะถูกจัดเก็บ", + "workspace.reset.note": "สิ่งนี้จะรีเซ็ตพื้นที่ทำงานให้ตรงกับสาขาเริ่มต้น", + "common.open": "เปิด", + "dialog.releaseNotes.action.getStarted": "เริ่มต้น", + "dialog.releaseNotes.action.next": "ถัดไป", + "dialog.releaseNotes.action.hideFuture": "ไม่ต้องแสดงสิ่งนี้อีกในอนาคต", + "dialog.releaseNotes.media.alt": "ตัวอย่างรุ่น", + "toast.project.reloadFailed.title": "ไม่สามารถโหลด {{project}} ใหม่ได้", + "error.server.invalidConfiguration": "การกำหนดค่าไม่ถูกต้อง", + "common.moreCountSuffix": " (เพิ่มอีก {{count}})", + "common.time.justNow": "เมื่อสักครู่นี้", + "common.time.minutesAgo.short": "{{count}} นาทีที่แล้ว", + "common.time.hoursAgo.short": "{{count}} ชม. ที่แล้ว", + "common.time.daysAgo.short": "{{count}} วันที่แล้ว", + "settings.providers.connected.environmentDescription": "เชื่อมต่อจากตัวแปรสภาพแวดล้อมของคุณ", + "settings.providers.custom.description": "เพิ่มผู้ให้บริการที่รองรับ OpenAI ด้วย URL หลัก", + + "app.server.unreachable": "ไม่สามารถติดต่อ {{server}}", + "app.server.retrying": "กำลังลองใหม่โดยอัตโนมัติ...", + "app.server.otherServers": "เซิร์ฟเวอร์อื่น ๆ", + "dialog.server.add.usernamePlaceholder": "ชื่อผู้ใช้", + "dialog.server.add.passwordPlaceholder": "รหัสผ่าน", + "server.row.noUsername": "ไม่มีชื่อผู้ใช้", + "session.review.noVcs.createGit.title": "สร้าง Git รีโพซิทอรี", + "session.review.noVcs.createGit.description": "ติดตาม ตรวจสอบ และเลิกทำการเปลี่ยนแปลงในโปรเจกต์นี้", + "session.review.noVcs.createGit.actionLoading": "กำลังสร้าง Git รีโพซิทอรี...", + "session.review.noVcs.createGit.action": "สร้าง Git รีโพซิทอรี", + "session.todo.progress": "เสร็จสิ้น {{done}} จาก {{total}} รายการ", + "session.question.progress": "{{current}} จาก {{total}} คำถาม", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.reveal.finder": "แสดงใน Finder", + "session.header.reveal.fileExplorer": "แสดงใน File Explorer", + "session.header.reveal.containingFolder": "เปิดโฟลเดอร์ที่มีรายการนี้", + "session.header.open.fileManager": "File Manager", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "การวินิจฉัยประสิทธิภาพการพัฒนา", + "debugBar.na": "n/a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "การเปลี่ยนเส้นทางที่เสร็จสมบูรณ์ล่าสุดที่สัมผัสหน้าเซสชัน วัดจากจุดเริ่มต้นเราเตอร์จนถึงการวาดครั้งแรกหลังจากที่นิ่ง", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "เฟรมต่อวินาทีแบบต่อเนื่องในช่วง 5 วินาทีที่ผ่านมา", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "เวลาเฟรมที่แย่ที่สุดในช่วง 5 วินาทีที่ผ่านมา", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "เฟรมที่เกิน 32ms ในช่วง 5 วินาทีที่ผ่านมา", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "เวลาที่ถูกบล็อกและจำนวนงานยาวในช่วง 5 วินาทีที่ผ่านมา งานสูงสุด: {{max}}", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "ความล่าช้าในการป้อนข้อมูลที่แย่ที่สุดที่สังเกตได้ในช่วง 5 วินาทีที่ผ่านมา", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "ระยะเวลาการโต้ตอบโดยประมาณในช่วง 5 วินาทีที่ผ่านมา นี่เป็นเหมือน INP ไม่ใช่ Web Vitals INP อย่างเป็นทางการ", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "การเลื่อนเลย์เอาต์สะสมสำหรับอายุการใช้งานของแอปปัจจุบัน", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "JS heap ที่ใช้เทียบกับขีดจำกัด heap เฉพาะ Chromium", + "debugBar.mem.tip": "JS heap ที่ใช้เทียบกับขีดจำกัด heap {{used}} จาก {{limit}}", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "บังคับใช้สไตล์โฟกัสกับองค์ประกอบแบบโต้ตอบทั้งหมด", + "debugBar.focus.on": "เปิด", + "debugBar.focus.off": "ปิด", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "บังคับกำหนดทิศทางเค้าโครงของทั้งแอปโดยไม่เปลี่ยนภาษาที่เลือก", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "ไม่ทราบ", + "error.page.circular": "[วงกลม]", + "error.serverSDK.noServerAvailable": "ไม่มีเซิร์ฟเวอร์", + "error.serverSDK.serverNotAvailable": "เซิร์ฟเวอร์ไม่พร้อมใช้งาน", + "error.childStore.persistedCacheCreateFailed": "ไม่สามารถสร้างแคชถาวร", + "error.childStore.persistedProjectMetadataCreateFailed": "ไม่สามารถสร้างเมตาดาต้าโปรเจกต์ถาวร", + "error.childStore.persistedProjectIconCreateFailed": "ไม่สามารถสร้างไอคอนโปรเจกต์ถาวร", + "error.childStore.storeCreateFailed": "ไม่สามารถสร้างที่เก็บ", + "terminal.connectionLost.abnormalClose": "WebSocket ปิดอย่างผิดปกติ: {{code}}", +} diff --git a/packages/app/src/i18n/tk.ts b/packages/app/src/i18n/tk.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b144c6a26c769f0b058a1008b1bc4842670ede6 --- /dev/null +++ b/packages/app/src/i18n/tk.ts @@ -0,0 +1,1159 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Faýl", + "desktop.menu.edit": "Redaktirläň", + "desktop.menu.view": "Gör", + "desktop.menu.go": "Bar", + "desktop.menu.window": "Penjire", + "desktop.menu.help": "Kömek", + "desktop.menu.checkForUpdates": "Täzelenmeleri barlaň ...", + "desktop.menu.settings": "Sazlamalar", + "desktop.menu.reloadWebview": "Web sahypasyny täzeden ýükläň", + "desktop.menu.restart": "Gaýtadan açyň", + "desktop.menu.exportLogs": "Eksport surnallary ...", + "desktop.menu.newSession": "Täze sessiýa", + "desktop.menu.openProject": "Açyk taslama ...", + "desktop.menu.newWindow": "Täze penjire", + "desktop.menu.closeWindow": "Penjiräni ýapyň", + "desktop.menu.undo": "Yza gaýtar", + "desktop.menu.redo": "Gaýtadan işlediň", + "desktop.menu.cut": "Kesiň", + "desktop.menu.copy": "Göçüriň", + "desktop.menu.paste": "Goýuň", + "desktop.menu.delete": "Poz", + "desktop.menu.selectAll": "Hemmesini saýlaň", + "desktop.menu.toggleSidebar": "Gapdal paneli çalyşyň", + "desktop.menu.toggleTerminal": "Terminaly açyň", + "desktop.menu.toggleFileTree": "Faýl agajyny çalyşyň", + "desktop.menu.reload": "Gaýtadan ýükläň", + "desktop.menu.toggleDeveloperTools": "Öndüriji gurallaryny çalyşyň", + "desktop.menu.actualSize": "Hakyky ululygy", + "desktop.menu.zoomIn": "Ulaltmak", + "desktop.menu.zoomOut": "Ulaltmak", + "desktop.menu.toggleFullScreen": "Doly ekrany açyň", + "desktop.menu.back": "Yzyna", + "desktop.menu.forward": "Öňe", + "desktop.menu.previousSession": "Öňki sessiýa", + "desktop.menu.nextSession": "Indiki sessiýa", + "desktop.menu.previousProject": "Öňki taslama", + "desktop.menu.nextProject": "Indiki taslama", + "desktop.menu.minimize": "Minimallaşdyryň", + "desktop.menu.maximize": "Ulaltmak", + "desktop.menu.documentation": "OpenCode Resminamalar", + "desktop.menu.supportForum": "Goldaw forumy", + "desktop.menu.shareFeedback": "Pikir alyşma", + "desktop.menu.reportBug": "Bug barada habar beriň", + "desktop.menu.ariaLabel": "OpenCode menýusy", + "desktop.updater.dialog.checkFailed.message": "Täzelenme şowsuz boldy.", + "desktop.updater.dialog.checkFailed.title": "Täzelenme säwligi", + "desktop.updater.dialog.upToDate.message": "Döwrebap", + "desktop.updater.dialog.upToDate.title": "Täzelenme ýok", + "desktop.updater.dialog.ready.message": "Göçürilen {{version}} täzeläň. Indi täzeden başlaň?", + "desktop.updater.dialog.ready.title": "Täzelenme taýýar", + "desktop.updater.dialog.restart": "Gaýtadan açyň", + "desktop.updater.dialog.later": "Soňrak", + "desktop.recovery.action.relaunch": "Gaýtadan işlediň", + "desktop.recovery.action.exportLogs": "Surnallary eksport ediň", + "desktop.recovery.action.keepWaiting": "Garaşmagy dowam ediň", + "desktop.recovery.action.quit": "Taşla", + "desktop.recovery.loadFailed": "OpenCode ýükläp bilmedi", + "desktop.recovery.terminated": "OpenCode penjiresi garaşylmadyk ýagdaýda bes edildi", + "desktop.recovery.unresponsive": "OpenCode jogap berenok", + "desktop.recovery.unresponsive.detail": + "Programmany täzeden açyp bilersiňiz, surnallary açyp ýa-da garaşyp bilersiňiz.", + "desktop.recovery.loadFailed.detail": "Penjire: {{window}}\nURL: {{url}}\nRoralňyşlyk: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Penjire: {{window}}\nSebäp: {{reason}}\nKod: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Papka saýlaň", + "desktop.dialog.chooseFile": "Faýl saýlaň", + "desktop.dialog.saveFile": "Faýly ýazdyryň", + "desktop.dialog.files": "Faýllar", + "desktop.server.local": "Serli serwer", + "desktop.wsl.error.windowsOnly": "WSL diňe Windows-de elýeterlidir", + "desktop.wsl.error.unavailable": "WSL elýeterli däl", + "desktop.wsl.error.listInstalled": "Gurlan WSL distroslaryny sanap bilmedi", + "desktop.wsl.error.listOnline": "Onlaýn WSL distroslaryny sanap bilmedi", + "desktop.wsl.error.executeDistro": "Distroda buýruklary ýerine ýetirip bolmaýar", + "desktop.wsl.error.installWsl": "WSL gurmak şowsuz", + "desktop.wsl.error.installDistro": "Distro gurup bilmedi: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode gurmak şowsuz", + "desktop.wsl.error.alreadyAdded": "{{distro}} eýýäm goşuldy", + "desktop.wsl.error.opencodeMissing": "açyk kod bu distroda gurlanok", + "desktop.wsl.error.opencodeCannotRun": "opencode guruldy, ýöne işledip bilmedi", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode {{distro}} gurulmady", + "desktop.wsl.error.updateVersion": + "OpenCode täzelenmesi gutardy, ýöne {{distro}} henizem {{installed}} habar berýär; garaşylýan {{expected}}", + "desktop.wsl.error.noVersion": "wersiýasy ýok", + "desktop.wsl.error.serverExited": "WSL serweri işe başlandan soň çykdy (kod = {{code}} signal = {{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL serweri sagdyn bolmanka çykdy (kod = {{code}} signal = {{signal}}) {{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} saglyk barlagy üçin Sidecar, {{timeout}}ms-den soň gutardy", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} {{timeout}}ms-den soň gutardy", + "desktop.wsl.error.failedPort": "Port alyp bilmedi", + "desktop.picker.error.notSelected": "Faýl saýlaýjy tarapyndan saýlanmady", + "desktop.picker.error.sizeLimit": "Saýlanan goşundylar {{limit}} MB çäginden ýokary", + "command.category.suggested": "Teklip edilýär", + "command.category.view": "Gör", + "command.category.project": "Taslama", + "command.category.provider": "Üpjün ediji", + "command.category.server": "Serwer", + "command.category.session": "Sessiýa", + "command.category.theme": "Mowzuk", + "command.category.language": "Dil", + "command.category.file": "Faýl", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Rugsatlar", + "command.category.workspace": "Workspace", + "command.category.settings": "Sazlamalar", + "command.logs.export": "Surnallary eksport ediň", + "theme.scheme.system": "Ulgam", + "theme.scheme.light": "Lightagtylyk", + "theme.scheme.dark": "Garaňky", + "command.sidebar.toggle": "Gapdal paneli çalyşyň", + "command.project.open": "Açyk taslama", + "command.project.previous": "Öňki taslama", + "command.project.next": "Indiki taslama", + "command.project.index": "{{index}} taslamasyna geçiň", + "command.provider.connect": "Prowaýderi birikdiriň", + "command.server.switch": "Serwer", + "command.settings.open": "Sazlamalary açyň", + "command.session.previous": "Öňki sessiýa", + "command.session.next": "Indiki sessiýa", + "command.session.previous.unseen": "Öňki okalmadyk sessiýa", + "command.session.next.unseen": "Indiki okalmadyk sessiýa", + "command.session.archive": "Arhiw sessiýasy", + "command.palette": "Buýruk palitrasy", + "command.theme.cycle": "Sikl mowzugy", + "command.theme.set": "Tema ulanyň: {{theme}}", + "command.theme.scheme.cycle": "Sikl reňk shemasy", + "command.theme.scheme.set": "Reňk shemasyny ulanyň: {{scheme}}", + "command.language.cycle": "Sikl dili", + "command.language.set": "Dil ulanyň: {{language}}", + "command.session.new": "Täze sessiýa", + "command.file.open": "Faýl açyň", + "command.tab.close": "Salgy ýapyň", + "command.tab.reopenClosed": "Closedapyk goýmany açyň", + "command.context.addSelection": "Kontekste saýlama goşuň", + "command.context.addSelection.description": "Häzirki faýldan saýlanan setirleri goşuň", + "command.input.focus": "Giriş fokus", + "command.terminal.toggle": "Terminaly açyň", + "command.fileTree.toggle": "Faýl agajyny çalyşyň", + "command.review.toggle": "Gözden geçiriň", + "command.terminal.new": "Täze terminal", + "command.terminal.new.description": "Täze terminal goýmasyny dörediň", + "command.steps.toggle": "Ädimleri aç ýa-da ýap", + "command.steps.toggle.description": "Häzirki habar üçin ädimleri görkeziň ýa-da gizläň", + "command.message.previous": "Öňki habar", + "command.message.previous.description": "Öňki ulanyjy habaryna geçiň", + "command.message.next": "Indiki habar", + "command.message.next.description": "Indiki ulanyjy habaryna geçiň", + "command.model.choose": "Model saýlaň", + "command.model.choose.description": "Başga modeli saýlaň", + "command.mcp.toggle": "MCP-leri çalyşyň", + "command.mcp.toggle.description": "MCP-leri çalyşyň", + "command.agent.cycle": "Indiki agent", + "command.agent.cycle.description": "Indiki agente geçiň", + "command.agent.cycle.reverse": "Öňki agent", + "command.agent.cycle.reverse.description": "Öňki agente geçiň", + "command.model.variant.cycle": "Indiki pikirleniş derejesi", + "command.model.variant.cycle.description": "Indiki synanyşyk derejesine geçiň", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "Rugsatlary awtomatiki kabul ediň", + "command.permissions.autoaccept.disable": "Awto-kabul ediş rugsatlaryny bes ediň", + "command.workspace.toggle": "Workspace-lary çalşyň", + "command.workspace.toggle.description": "Gapdal paneldäki birnäçe workspace-i işletmek ýa-da öçürmek", + "command.session.undo": "Yza gaýtar", + "command.session.undo.description": "Iň soňky habary yzyna al", + "command.session.redo": "Gaýtadan işlediň", + "command.session.redo.description": "Iň soňky yzyna gaýtarylan habary yzyna gaýtaryň", + "command.session.compact": "Ykjam sessiýa", + "command.session.compact.description": "Kontekstiň ululygyny azaltmak üçin sessiýany jemläň", + "command.session.fork": "Habardan şahalandyr", + "command.session.fork.description": "Öňki habardan täze sessiýa dörediň", + "command.session.share": "Sessiýa paýlaş", + "command.session.share.description": "Bu sessiýany paýlaşyň we URL buferine göçüriň", + "command.session.unshare": "Paýlaşma sessiýasy", + "command.session.unshare.description": "Bu sessiýany paýlaşmagy bes ediň", + "command.session.export": "Eksport sessiýasy", + "command.session.export.description": "Sessiýanyň doly ýazgysyny JSON görnüşinde eksport ediň", + "palette.search.placeholder": "Faýllary, buýruklary we sessiýalary gözläň", + "palette.search.placeholder.home": "Buýruklary we sessiýalary gözläň", + "palette.empty": "Netije tapylmady", + "palette.group.commands": "Buýruklar", + "palette.group.files": "Faýllar", + "dialog.provider.search.placeholder": "Gözleg üpjün edijileri", + "dialog.provider.empty": "Hiç hili prowaýder tapylmady", + "dialog.provider.group.popular": "Meşhur", + "dialog.provider.group.other": "Beýlekiler", + "dialog.provider.custom.label": "Xörite OpenAI gabat gelýän üpjün ediji", + "dialog.provider.tag.recommended": "Maslahat berilýär", + "dialog.provider.opencode.note": "Claude, GPT, Gemini we ş.m.", + "dialog.provider.opencode.tagline": "Ygtybarly optimal modeller", + "dialog.provider.opencodeGo.tagline": "Hemmeler üçin arzan bahaly abuna", + "dialog.provider.anthropic.note": "Pro we Max ýaly Claude modellerine göni giriş", + "dialog.provider.copilot.note": "GitHub Copilot arkaly kodlaşdyrmak üçin AI modelleri", + "dialog.provider.openai.note": "Çalt, başarnykly umumy AI meseleleri üçin GPT modelleri", + "dialog.provider.google.note": "Çalt, gurluşly jogaplar üçin Gemini modelleri", + "dialog.provider.openrouter.note": "Bir üpjün edijiden ähli goldanýan modellere giriň", + "dialog.provider.vercel.note": "Akylly marşrutizasiýa bilen AI modellerine bitewi giriş", + "dialog.model.select.title": "Model saýlaň", + "dialog.model.search.placeholder": "Gözleg modelleri", + "dialog.model.empty": "Hiç hili model netijesi ýok", + "dialog.model.manage": "Modelleri dolandyryň", + "dialog.model.manage.description": "Model saýlaýjysynda haýsy modelleriň peýda bolýandygyny düzüň.", + "dialog.model.manage.provider.toggle": "Zhli {{provider}} modellerini üýtgediň", + "dialog.model.unpaid.freeModels.title": "OpenCode tarapyndan mugt modeller", + "dialog.model.unpaid.addMore.title": "Meşhur üpjün edijilerden has köp model goşuň", + "dialog.model.unpaid.viewMoreProviders": "Moreene-de 70+ üpjün edijä serediň", + "dialog.provider.viewAll": "Has köp üpjün edijini görkeziň", + "provider.connect.title": "{{provider}} birikdiriň", + "provider.connect.title.anthropicProMax": "Claude Pro / Maks bilen giriň", + "provider.connect.selectMethod": "{{provider}} üçin giriş usulyny saýlaň.", + "provider.connect.method.apiKey": "API açary", + "provider.connect.method.browser": "Brauzer", + "provider.connect.method.headless": "Kellesiz", + "provider.connect.status.inProgress": "Ygtyýarnama dowam edýär ...", + "provider.connect.status.waiting": "Ygtyýarnama garaşýarys ...", + "provider.connect.status.failed": "Ygtyýarnama şowsuz: {{error}}", + "provider.connect.apiKey.description": + "Hasabyňyzy birikdirmek we OpenCode-da {{provider}} modellerini ulanmak üçin {{provider}} API açaryňyzy giriziň.", + "provider.connect.apiKey.label": "{{provider}} API açary", + "provider.connect.apiKey.placeholder": "API açary", + "provider.connect.apiKey.required": "API açary talap edilýär", + "provider.connect.opencodeZen.line1": + "OpenCode Zen kodlaşdyrmak serişdeleri üçin ygtybarly optimallaşdyrylan modelleriň toplumyna girmäge mümkinçilik berýär.", + "provider.connect.opencodeZen.line2": + "Zekeje API açary bilen Claude, GPT, Gemini, GLM we başgalar ýaly modellere girip bilersiňiz.", + "provider.connect.opencodeZen.visit.prefix": "Sapar ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " API açaryňyzy ýygnamak.", + "provider.connect.oauth.code.visit.prefix": "Sapar ", + "provider.connect.oauth.code.visit.link": "Bu baglanyşyk", + "provider.connect.oauth.code.visit.suffix": + " hasabyňyzy birikdirmek we {{provider}} modellerini OpenCode-de ulanmak üçin ygtyýarnama koduňyzy ýygnamak.", + "provider.connect.oauth.code.label": "{{method}} ygtyýarnama kody", + "provider.connect.oauth.code.placeholder": "Awtorizasiýa kody", + "provider.connect.oauth.code.required": "Awtorizasiýa kody talap edilýär", + "provider.connect.oauth.code.invalid": "Nädogry ygtyýar kody", + "provider.connect.oauth.auto.visit.prefix": "Sapar ", + "provider.connect.oauth.auto.visit.link": "Bu baglanyşyk", + "provider.connect.oauth.auto.visit.suffix": + " hasabyňyzy birikdirmek we {{provider}} modellerini OpenCode-de ulanmak üçin aşakdaky kody giriziň.", + "provider.connect.oauth.auto.confirmationCode": "Tassyklama kody", + "provider.connect.toast.connected.title": "{{provider}} birikdirildi", + "provider.connect.toast.connected.description": "{{provider}} modelleri indi ulanylyp bilner.", + "provider.custom.title": "Hususy üpjün ediji", + "provider.custom.unavailable": "Bu serwerde ýörite üpjün edijiler elýeterli däl", + "provider.custom.description.prefix": "OpenAI gabat gelýän üpjün edijini sazlaň. Serediň ", + "provider.custom.description.link": "üpjün ediji konfigurasiýa resminamalary", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Üpjün edijiniň şahsyýetnamasy", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Uly harplar, sanlar, çyzgylar ýa-da aşaky çyzyklar", + "provider.custom.field.name.label": "Adyny görkeziň", + "provider.custom.field.name.placeholder": "AI üpjün edijim", + "provider.custom.field.baseURL.label": "Esasy URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API açary", + "provider.custom.field.apiKey.placeholder": "API açary", + "provider.custom.field.apiKey.description": "Meýletin. Awtory sözbaşylar arkaly dolandyrsaňyz, boş goýuň.", + "provider.custom.models.label": "Modeller", + "provider.custom.models.id.label": "Şahsyýetnamasy", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Ady", + "provider.custom.models.name.placeholder": "Ekranyň ady", + "provider.custom.models.remove": "Modelini aýyryň", + "provider.custom.models.add": "Model goşuň", + "provider.custom.headers.label": "Sözbaşylar (islege görä)", + "provider.custom.headers.key.label": "Sözbaşy", + "provider.custom.headers.key.placeholder": "Adyň ady", + "provider.custom.headers.value.label": "Gymmatlyk", + "provider.custom.headers.value.placeholder": "bahasy", + "provider.custom.headers.remove": "Sözbaşy aýyryň", + "provider.custom.headers.add": "Sözbaşy goşuň", + "provider.custom.error.providerID.required": "Üpjün edijiniň şahsyýetnamasy talap edilýär", + "provider.custom.error.providerID.format": "Baş harplary, sanlary, defisleri ýa-da aşaky harplary ulanyň", + "provider.custom.error.providerID.exists": "Şol üpjün edijiniň ID-si eýýäm bar", + "provider.custom.error.name.required": "Ekranyň ady talap edilýär", + "provider.custom.error.baseURL.required": "Esasy URL hökmany", + "provider.custom.error.baseURL.format": "Http: // ýa-da https: // bilen başlamaly", + "provider.custom.error.required": "Gerekli", + "provider.custom.error.duplicate": "Göçürme", + "provider.disconnect.toast.disconnected.title": "{{provider}} kesildi", + "provider.disconnect.toast.disconnected.description": "{{provider}} modelleri indi ýok.", + "model.tag.free": "Mugt", + "model.tag.latest": "Iň soňky", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "tekst", + "model.input.image": "şekil", + "model.input.audio": "ses", + "model.input.video": "wideo", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Rugsat berýär: {{inputs}}", + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Girişler", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Üpjün ediji", + "model.tooltip.reasoning": "Sebäpler", + "model.tooltip.reasoning.allowed": "Pikirlenmäge rugsat berýär", + "model.tooltip.reasoning.none": "Sebäp ýok", + "model.tooltip.context": "Kontekstiň çäkleri {{limit}}", + "common.search.placeholder": "Gözlemek", + "common.goBack": "Yza gaýdyň", + "common.goForward": "Öňe gidiň", + "common.loading": "Adingüklemek", + "common.loading.ellipsis": "...", + "common.cancel": "Elatyr", + "common.clear": "Arassala", + "common.open": "Açyň", + "common.connect": "Birikdiriň", + "common.disconnect": "Aýyr", + "common.continue": "Dowam et", + "common.submit": "Iberiň", + "common.save": "Saklaň", + "common.saving": "Tygşytlamak ...", + "common.default": "Bellenen", + "common.attachment": "goşundy", + "prompt.placeholder.shell": "Shell buýrugyny giriziň ... {{example}}", + "prompt.placeholder.normal": 'Islän zadyňyzy soraň ... "{{example}}"', + "prompt.placeholder.simple": "Islän zadyňyzy soraň ...", + "prompt.placeholder.summarizeComments": "Teswirleri jemläň…", + "prompt.placeholder.summarizeComment": "Teswirleri jemläň…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "çykmak", + "session.child.promptDisabled": "Subagent sessiýalary soralyp bilinmez.", + "session.child.backToParent": "Esasy sessiýa dolan.", + "prompt.example.1": "Kod bazasynda TODO düzediň", + "prompt.example.2": "Bu taslamanyň tehnologiýa toplumy näme?", + "prompt.example.3": "Döwülen synaglary düzediň", + "prompt.example.4": "Autentifikasiýanyň nähili işleýändigini düşündiriň", + "prompt.example.5": "Howpsuzlyk gowşak taraplaryny tapyň we düzediň", + "prompt.example.6": "Ulanyjy hyzmaty üçin birlik synaglaryny goşuň", + "prompt.example.7": "Has köp okalmagy üçin bu funksiýany üýtgediň", + "prompt.example.8": "Bu ýalňyşlyk nämäni aňladýar?", + "prompt.example.9": "Bu meseläni çözmäge kömek ediň", + "prompt.example.10": "API resminamalaryny dörediň", + "prompt.example.11": "Maglumat bazasynyň talaplaryny optimizirläň", + "prompt.example.12": "Giriş tassyklamasyny goşuň", + "prompt.example.13": "Üçin täze komponent dörediň ...", + "prompt.example.14": "Bu taslamany nädip ýerleşdirmeli?", + "prompt.example.15": "Iň oňat tejribe üçin kodumy gözden geçiriň", + "prompt.example.16": "Bu funksiýa säwlik bilen işlemek goşuň", + "prompt.example.17": "Bu regex görnüşini düşündiriň", + "prompt.example.18": "Muny TypeScript-e öwüriň", + "prompt.example.19": "Kod bazasynyň hemme ýerinde hasaba giriň", + "prompt.example.20": "Haýsy baglylyklar köne?", + "prompt.example.21": "Migrasiýa skriptini ýazmaga kömek ediň", + "prompt.example.22": "Bu ahyrky nokat üçin keş keşini ýerine ýetiriň", + "prompt.example.23": "Bu sanawa sahypa goşuň", + "prompt.example.24": "Üçin CLI buýrugyny dörediň ...", + "prompt.example.25": "Daşky gurşaw üýtgeýjileri bu ýerde nähili işleýär?", + "prompt.popover.emptyResults": "Gabat gelýän netijeler ýok", + "prompt.popover.emptyCommands": "Gabat gelýän buýruklar ýok", + "prompt.dropzone.label": "Suratlary, PDF-leri ýa-da tekst faýllaryny şu ýere taşlaň", + "prompt.dropzone.file.label": "@mention faýlyna geçiň", + "prompt.slash.badge.custom": "adat", + "prompt.slash.badge.skill": "ussatlygy", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "işjeň", + "prompt.context.includeActiveFile": "Işjeň faýly goşuň", + "prompt.context.removeActiveFile": "Işjeň faýly kontekstden aýyryň", + "prompt.context.removeFile": "Faýly kontekstden aýyryň", + "prompt.action.attachFile": "Faýl goşuň", + "prompt.menu.addImagesAndFiles": "Faýl we ş.m. goşuň", + "prompt.menu.imagesAndFiles": "Suratlar we faýllar", + "prompt.menu.commands": "Buýruklar", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Shell buýrugy", + "prompt.attachment.remove": "Goşundyny aýyryň", + "prompt.action.send": "Iber", + "prompt.action.stop": "Dur", + "prompt.toast.pasteUnsupported.title": "Goldaw berilmeýän goşundy", + "prompt.toast.pasteUnsupported.description": "Bu ýerde diňe suratlar, PDF ýa-da tekst faýllary birikdirilip bilner.", + "prompt.toast.attachmentDuplicate.title": "Bu faýl eýýäm ýüklendi", + "prompt.toast.modelAgentRequired.title": "Agent we model saýlaň", + "prompt.toast.modelAgentRequired.description": "Sorag ibermezden ozal agent we model saýlaň.", + "prompt.toast.worktreeCreateFailed.title": "Git worktree döredip bolmady", + "prompt.toast.sessionCreateFailed.title": "Sessiýa döredip bilmedi", + "prompt.toast.shellSendFailed.title": "Shell buýrugy iberilmedi", + "prompt.toast.commandSendFailed.title": "Buýruk iberilmedi", + "prompt.toast.promptSendFailed.title": "Prompt iberilmedi", + "prompt.toast.promptSendFailed.description": "Sessiýany alyp bolmaýar", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{total}}-iň {{enabled}} açyk", + "dialog.mcp.empty": "Hiç hili MCP düzülmedi", + "dialog.lsp.empty": "LSP-ler faýl görnüşlerinden awtomatiki usulda tapyldy", + "dialog.plugins.empty": "opencode.json-de düzülen pluginler", + "mcp.status.connected": "birikdirildi", + "mcp.status.failed": "şowsuz", + "mcp.status.needs_auth": "auth gerek", + "mcp.status.disabled": "maýyp", + "mcp.auth.clickToAuthenticate": "Hakyky tassyklamak üçin basyň", + "dialog.fork.empty": "Şahalandyrmaly habar ýok", + "dialog.directory.search.placeholder": "Papkalary gözläň", + "dialog.directory.empty": "Papka tapylmady", + "dialog.directory.action.selectFile": "Faýly saýlaň", + "dialog.directory.action.selectFolder": "Papkany saýlaň", + "dialog.directory.root": "Kök", + "dialog.directory.parent": "Ene-ata", + "dialog.directory.readError": "Bu bukjany okap bolmaýar", + "app.server.unreachable": "{{server}} baryp bilmedi", + "app.server.retrying": "Awtomatiki usulda gaýtadan synanyşmak ...", + "app.server.otherServers": "Beýleki serwerler", + "dialog.server.title": "Serwerler", + "dialog.server.description": "Bu programmanyň haýsy OpenCode serwerine birikýändigini üýtgediň.", + "dialog.server.search.placeholder": "Serwerleri gözläň", + "dialog.server.empty": "Entek serwer ýok", + "dialog.server.add.title": "Serwer goşuň", + "dialog.server.add.url": "Serwer salgysy", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Serwere birigip bilmedim", + "dialog.server.add.checking": "Barlamak ...", + "dialog.server.add.button": "Serwer goşuň", + "dialog.server.add.name": "Serweriň ady (islege görä)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Ulanyjy ady (islege görä)", + "dialog.server.add.usernamePlaceholder": "ulanyjy ady", + "dialog.server.add.password": "Parol (islege görä)", + "dialog.server.add.passwordPlaceholder": "parol", + "dialog.server.edit.title": "Serweri redaktirläň", + "dialog.server.default.title": "Bellenen serwer", + "dialog.server.default.description": + "Servererli serweri başlamagyň ýerine programma goýberilende bu serwere birikdiriň. Gaýtadan açmagy talap edýär.", + "dialog.server.default.none": "Hiç hili serwer saýlanmady", + "dialog.server.default.set": "Häzirki serweri deslapky görnüşde düzüň", + "dialog.server.default.clear": "Arassala", + "dialog.server.action.remove": "Serweri aýyryň", + "dialog.server.menu.edit": "Redaktirläň", + "dialog.server.menu.default": "Dymmaklyk hökmünde düzüň", + "dialog.server.menu.defaultRemove": "Dymmaklygy aýyryň", + "dialog.server.menu.delete": "Poz", + "dialog.server.current": "Häzirki Serwer", + "dialog.server.status.default": "Bellenen", + "wsl.server.add": "WSL serwerini goşuň", + "wsl.server.addShort": "WSL goşuň", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL serweri", + "wsl.server.retryStart": "Başlaň", + "wsl.server.updating": "Täzelenmek ...", + "wsl.onboarding.step.distro": "Distro saýlaň", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL barlamak ...", + "wsl.onboarding.restartRequired": "Windows, WSL gurnamagy gutarmak üçin täzeden başlamaly.", + "wsl.onboarding.ready": "WSL taýýar.", + "wsl.onboarding.required": "Dowam etmek üçin WSL talap edilýär.", + "wsl.onboarding.checkingDistros": "Distroslary barlamak ...", + "wsl.onboarding.installingDistro": "{{distro}} gurmak ...", + "wsl.onboarding.checkingDistro": "{{distro}} barlamak ...", + "wsl.onboarding.listingDistros": "Distroslaryň sanawy ...", + "wsl.onboarding.distroReady": "{{distro}} taýýar.", + "wsl.onboarding.distroNotInstalled": "{{distro}} entek gurlanok.", + "wsl.onboarding.openDistroOnce": "Gurmak işini tamamlamak üçin {{distro}} bir gezek açyň.", + "wsl.onboarding.finishingDistro": "{{distro}} üçin gurnama.", + "wsl.onboarding.pickDistro": "Distro saýlaň ýa-da aşakda birini guruň.", + "wsl.onboarding.checkingOpencode": "OpenCode barlamak ...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}} içinde OpenCode barlanýar...", + "wsl.onboarding.updatingOpencode": "OpenCode täzelenmek ...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}}-de OpenCode täzelenmek ...", + "wsl.onboarding.updateOpencodeIn": "OpenCode-ni {{distro}}-de täzeläň.", + "wsl.onboarding.updateOpencode": "OpenCode täzeläň", + "wsl.onboarding.opencodeReadyIn": "OpenCode {{distro}}-de taýýar.", + "wsl.onboarding.opencodeReady": "OpenCode taýýar.", + "wsl.onboarding.installOpencodeIn": "{{distro}}-de OpenCode guruň.", + "wsl.onboarding.installOpencode": "OpenCode guruň", + "wsl.onboarding.chooseDistroFirst": "Ilki bilen distro saýlaň.", + "wsl.onboarding.loadFailed": "WSL ýagdaýyny ýükläp bilmedi.", + "wsl.onboarding.loading": "Adingüklenýär ...", + "wsl.onboarding.installedDistros": "Gurlan distroslar", + "wsl.onboarding.checkAgain": "Againene bir gezek barlaň", + "wsl.onboarding.distroStatus.ready": "Taýýar", + "wsl.onboarding.distroStatus.checking": "Barlamak ...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode gurulmady", + "wsl.onboarding.distroStatus.missingTools": "Ingitirim bolan baş, egri", + "wsl.onboarding.distroStatus.unsupported": "Goldaw berilmeýär · WSL 2 ulanyň", + "wsl.onboarding.needAnotherDistro": "Başga bir distro gerekmi?", + "wsl.onboarding.needAnotherDistroHint": "WSL katalogyndan Linux paýlanyşyny guruň", + "wsl.onboarding.wslNotInstalled.title": "WSL gurulmady", + "wsl.onboarding.wslNotInstalled.description": + "WSL (Linux üçin Windows kiçi ulgamy) OpenCode WSL serwerini goşmazdan ozal talap edilýär", + "wsl.onboarding.wslUnavailable.title": "WSL elýeterli däl", + "wsl.onboarding.wslUnavailable.description": "OpenCode bu enjamda WSL barlap bilmedi.", + "wsl.onboarding.installWsl": "WSL guruň", + "wsl.onboarding.windowsRestartRequired": "WSL gurnamagy gutarmak üçin Windows-i täzeden açyň, soňra OpenCode açyň.", + "wsl.onboarding.next": "Indiki", + "wsl.onboarding.refresh": "Täzele", + "wsl.onboarding.allDistrosAdded": "Gurlan distroslaryň hemmesi eýýäm goşuldy.", + "wsl.onboarding.noDistros": "Entek hiç hili distro tapylmady.", + "wsl.onboarding.install": "Gurnama", + "wsl.onboarding.installing": "Gurmak ...", + "wsl.onboarding.installDistro": "Distro guruň", + "wsl.onboarding.searchDistros": "Distroslary gözläň", + "wsl.onboarding.wsl2Required": "WSL 2 talap edilýär.", + "wsl.onboarding.toolsRequired": "Bu distro üçin bash we curl gerek.", + "wsl.onboarding.openTerminal": "Terminal açyň", + "wsl.onboarding.path": ": Ol: {{path}}", + "wsl.onboarding.notFound": "tapylmady", + "wsl.onboarding.version": "Wersiýa: {{version}}", + "wsl.onboarding.unknown": "näbelli", + "wsl.onboarding.desktopVersion": "iş stoly {{version}}", + "wsl.onboarding.versionMismatch": "Gurlan wersiýa iş stoly programma wersiýasyna gabat gelenok.", + "wsl.onboarding.adding": "Goşmak ...", + "help.tabs.toast.ariaLabel": + "Salgylar bilen tanyşdyrmak. Işleriňizi we işjeň sessiýalaryňyzy bellikler bilen tertipläň", + "help.tabs.toast.dismiss": "Salgylar baradaky maglumatlary aýyryň", + "help.tabs.title": "Salgylar bilen tanyşdyrmak", + "help.tabs.description": "Işleriňizi we işjeň sessiýalaryňyzy bellikler bilen tertipläň", + "help.tabs.date": "14-nji iýul", + "help.tabs.introduction": "OpenCode Desktop indi goýmalaryň töwereginde guruldy.", + "help.tabs.sessions": + "Salgyda täze sessiýa başlaň ýa-da islendik taslamaňyzdan bar bolan sessiýany açyň. Täze bir zat başlanyňyzda täze tab açyň we gutaranyňyzdan soň ýapyň.", + "help.tabs.organize": + "Birnäçe goýmany açyk saklamak, işjeň sessiýalaryňyzy tertipleşdirmegi aňsatlaşdyrýar. Tablisalary ýatda saklamagy meýilleşdirýän bolsaňyz, ýatdan çykmajak bir zadyň adyny üýtgediň.", + "help.tabs.home": + "Xhli sessiýalaryňyzy we taslamalaryňyzy täze Home ekranynda tapyp bilersiňiz. Sessiýany saýlamak ony goýmada açýar.", + "help.tabs.persistence": "Programmany açanyňyzda, goýmalar henizem açyk.", + "help.tabs.worktrees": + "Täze dizaýn entek Git Worktrees-i goldamaýar, ýakyn wagtda çykar. Şonuň üçin öňki tertibi ulanmagy dowam etdirmek isleseňiz, Sazlamalardaky düzülişleriň arasynda geçip bilersiňiz. Täze düzülişiň birnäçe hepdeden hemişelik boljakdygyny ýadyňyzdan çykarmaň.", + "server.row.noUsername": "ulanyjy ady ýok", + "dialog.project.edit.title": "Taslamany redaktirläň", + "dialog.project.edit.name": "Ady", + "dialog.project.edit.icon": "Nyşan", + "dialog.project.edit.icon.alt": "Taslamanyň nyşany", + "dialog.project.edit.icon.hint": "Surata basyň ýa-da süýräň", + "dialog.project.edit.icon.recommended": "Maslahat berilýär: 128x128px", + "dialog.project.edit.color": "Reňk", + "dialog.project.edit.color.select": "{{color}} reňkini saýlaň", + "dialog.project.edit.worktree.startup": "Workspace başlangyç skripti", + "dialog.project.edit.worktree.startup.description": "Täze workspace (Git worktree) döredilenden soň işleýär.", + "dialog.project.edit.worktree.startup.placeholder": "meselem bun install", + "dialog.releaseNotes.action.getStarted": "Başla", + "dialog.releaseNotes.action.next": "Indiki", + "dialog.releaseNotes.action.hideFuture": "Geljekde bulary görkezmäň", + "dialog.releaseNotes.media.alt": "Öňünden görüş", + "context.breakdown.title": "Kontekstiň bölünişi", + "context.breakdown.note": + 'Giriş bellikleriniň takmynan döwülmegi. "Beýlekiler" gural kesgitlemelerini we goşmaça sözleri öz içine alýar.', + "context.breakdown.system": "Ulgam", + "context.breakdown.user": "Ulanyjy", + "context.breakdown.assistant": "Kömekçi", + "context.breakdown.tool": "Gural jaňlary", + "context.breakdown.other": "Beýlekiler", + "context.systemPrompt.title": "Ulgam prompt-y", + "context.rawMessages.title": "Çig habarlar", + "context.export.session": "Eksport sessiýasy", + "context.stats.session": "Sessiýa", + "context.stats.messages": "Habarlar", + "context.stats.provider": "Üpjün ediji", + "context.stats.model": "Model", + "context.stats.limit": "Kontekstiň çäkleri", + "context.stats.totalTokens": "Jemi belgiler", + "context.stats.usage": "Ulanylyşy", + "context.stats.inputTokens": "Giriş belgileri", + "context.stats.outputTokens": "Çykyş belgileri", + "context.stats.reasoningTokens": "Tokenler", + "context.stats.cacheTokens": "Keş Tokens (oka / ýaz)", + "context.stats.userMessages": "Ulanyjy habarlary", + "context.stats.assistantMessages": "Kömekçi habarlar", + "context.stats.totalCost": "Jemi bahasy", + "context.stats.sessionCreated": "Sessiýa döredildi", + "context.stats.lastActivity": "Soňky işjeňlik", + "context.usage.tokens": "Belgiler", + "context.usage.usage": "Ulanylyşy", + "context.usage.cost": "Bahasy", + "context.usage.clickToView": "Konteksti görmek üçin basyň", + "context.usage.view": "Kontekstiň ulanylyşyny görüň", + "language.en": "Iňlis", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Doýçe", + "language.es": "Español", + "language.fr": "Fransuz", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Braziliýa)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Dil", + "toast.language.description": "{{language}}-e geçdi", + "toast.theme.title": "Tema açyldy", + "toast.scheme.title": "Reňk shemasy", + "toast.workspace.enabled.title": "Workspace-lar açyk", + "toast.workspace.enabled.description": "Indi gapdal panelde birnäçe Git worktree görkezilýär", + "toast.workspace.disabled.title": "Workspace-lar ýapyk", + "toast.workspace.disabled.description": "Gapdal panelde diňe esasy Git worktree görkezilýär", + "toast.permissions.autoaccept.on.title": "Awto-kabul ediş rugsatlary", + "toast.permissions.autoaccept.on.description": "Rugsat haýyşlary awtomatiki tassyklanar", + "toast.permissions.autoaccept.off.title": "Awto-kabul ediş rugsatlary bes edildi", + "toast.permissions.autoaccept.off.description": "Rugsat haýyşlary tassyklamagy talap eder", + "toast.model.none.title": "Hiç hili model saýlanmady", + "toast.model.none.description": "Bu sessiýany jemlemek üçin üpjün edijini birikdiriň", + "toast.file.loadFailed.title": "Faýl ýükläp bilmedi", + "toast.file.listFailed.title": "Faýllary sanap bilmedi", + "toast.context.noLineSelection.title": "Setir saýlamasy ýok", + "toast.context.noLineSelection.description": "Ilki bilen faýl goýmasynda setir aralygyny saýlaň.", + "toast.session.share.copyFailed.title": "URL buferine göçürip bilmedi", + "toast.session.share.success.title": "Sessiýa paýlaşyldy", + "toast.session.share.success.description": "Buferde göçürilen URL paýlaşyň!", + "toast.session.share.failed.title": "Sessiýany paýlaşyp bilmedi", + "toast.session.share.failed.description": "Sessiýa paýlaşylanda ýalňyşlyk ýüze çykdy", + "toast.session.unshare.success.title": "Sessiýa paýlaşylmady", + "toast.session.unshare.success.description": "Sessiýa üstünlikli paýlaşylmady!", + "toast.session.unshare.failed.title": "Sessiýa paýlaşylmady", + "toast.session.unshare.failed.description": "Sessiýa paýlaşylanda ýalňyşlyk ýüze çykdy", + "toast.session.export.success.title": "Sessiýa eksport edildi", + "toast.session.export.success.description": "Sessiýa {{filename}}-de saklandy", + "toast.session.export.failed.title": "Sessiýa eksport edip bilmedi", + "toast.session.export.failed.description": "Sessiýa eksport edilende säwlik ýüze çykdy", + "toast.session.listFailed.title": "{{project}} üçin sessiýalary ýükläp bilmedi", + "toast.project.reloadFailed.title": "{{project}} ýükläp bilmedi", + "toast.update.title": "Täzelenme elýeterli", + "toast.update.description": "Indi gurmak üçin OpenCode ({{version}}) -iň täze wersiýasy bar.", + "toast.update.action.installRestart": "Guruň we täzeden açyň", + "toast.update.action.notYet": "Entek däl", + "error.page.title": "Bir zat nädogry boldy", + "error.page.description": "Programmany ýükläniňizde säwlik ýüze çykdy.", + "error.page.description.localServerStartup": "Serli serweri işe girizeniňizde säwlik ýüze çykdy.", + "error.page.details.label": "Roralňyşlyk maglumatlary", + "error.page.action.restart": "Gaýtadan açyň", + "error.page.action.report": "Roralňyşlyk barada habar beriň", + "error.page.action.reported": "Roralňyşlyk habar berildi", + "error.page.action.exportLogs": "Surnallary eksport ediň", + "error.page.action.checking": "Barlamak ...", + "error.page.action.checkUpdates": "Täzelenmeleri barlaň", + "error.page.action.updateTo": "{{version}} täzeläň", + "error.page.circular": "[Tegelek]", + "error.page.report.prefix": "Bu ýalňyşlygy OpenCode toparyna habar bermegiňizi haýyş edýäris", + "error.page.report.discord": "Diskord", + "error.page.version": "Wersiýa: {{version}}", + "error.dev.rootNotFound": + "Kök elementi tapylmady index.html-e goşmagy ýatdan çykardyňyzmy? Ora-da id atributynyň ýalňyş ýazylan bolmagy mümkin?", + "error.serverSync.connectFailed": "Serwere birigip bilmedim. `{{url}}`-de işleýän serwer barmy?", + "error.serverSDK.noServerAvailable": "Serwer ýok", + "error.serverSDK.serverNotAvailable": "Serwer elýeterli däl", + "error.childStore.persistedCacheCreateFailed": "Dowamly keş ýasap bilmedi", + "error.childStore.persistedProjectMetadataCreateFailed": "Dowamly taslama meta-maglumatyny döredip bilmedi", + "error.childStore.persistedProjectIconCreateFailed": "Dowamly taslama nyşanyny döredip bilmedi", + "error.childStore.storeCreateFailed": "Dükan döredip bilmedi", + "directory.error.invalidUrl": "URL-de nädogry katalog.", + "error.chain.unknown": "Näbelli ýalňyşlyk", + "error.server.invalidConfiguration": "Nädogry konfigurasiýa", + "error.chain.causedBy": "Sebäp:", + "error.chain.apiError": "API ýalňyşlygy", + "error.chain.status": ": Agdaýy: {{status}}", + "error.chain.retryable": "Gaýtadan synanyşyp boljak: {{retryable}}", + "error.chain.responseBody": "Jogap beriji organ:\n{{body}}", + "error.chain.didYouMean": "Diýjek bolduňmy: {{suggestions}}", + "error.chain.modelNotFound": "Model tapylmady: {{provider}} / {{model}}", + "error.chain.checkConfig": "Konfigurasiýaňyzy (opencode.json) üpjün edijiniň / model atlaryny barlaň", + "error.chain.mcpFailed": 'MCP serweri "{{name}}" şowsuz. Üns beriň, OpenCode entek MCP tassyklamasyny goldamaýar.', + "error.chain.providerAuthFailed": "Prowaýderiň tassyklamasy şowsuz ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + '"{{provider}}" üpjün edijini işe girizip bilmedi. Şahsyýetnamalary we konfigurasiýany barlaň.', + "error.chain.configJsonInvalid": "{{path}} faýly sazlamak JSON (C)", + "error.chain.configJsonInvalidWithMessage": "{{path}} faýly sazlamak JSON (C): {{message}}", + "error.chain.configDirectoryTypo": + '{{path}}-daky "{{dir}}" katalogy dogry däl. Katalogyň adyny "{{suggestion}}" diýip üýtgediň ýa-da aýyryň. Bu umumy ýalňyşlyk.', + "error.chain.configFrontmatterError": "{{path}}-de frontmateri derňäp bilmedi:\n{{message}}", + "error.chain.configInvalid": "{{path}} faýly sazlamak nädogry", + "error.chain.configInvalidWithMessage": "{{path}} faýly sazlamak nädogry: {{message}}", + "notification.permission.title": "Rugsat gerek", + "notification.permission.description": "{{projectName}} taslamasyndaky {{sessionTitle}} rugsat talap edýär", + "notification.question.title": "Sorag", + "notification.question.description": "{{projectName}} taslamasyndaky {{sessionTitle}} sessiýasynyň soragy bar", + "notification.action.goToSession": "Sessiýa git", + "notification.session.responseReady.title": "Jogap taýýar", + "notification.session.error.title": "Sessiýa ýalňyşlygy", + "notification.session.error.fallbackDescription": "Ýalňyşlyk ýüze çykdy", + "home.recentProjects": "Soňky taslamalar", + "home.empty.title": "Soňky taslamalar ýok", + "home.empty.description": "Projecterli taslamany açmak bilen başlaň", + "home.title": "Baş sahypa", + "home.projects": "Taslamalar", + "home.project.add": "Taslama goşuň", + "home.recentlyClosed": "Recentlyakynda ýapyldy", + "home.server.collapse": "Serwer taslamalaryny ýykyň", + "home.server.expand": "Serwer taslamalaryny giňeltmek", + "home.sessions.search.placeholder": "Gözleg sessiýalary", + "home.sessions.search.placeholder.scoped": "{{scope}}-de gözleg sessiýalary", + "home.sessions.search.sessions": "Sessiýalar", + "home.sessions.search.noResults": "{{query}} üçin sessiýa tapylmady", + "home.sessions.empty": "Bu ýerde entek hiç zat ýok", + "home.sessions.empty.description": "Başlamak üçin sessiýa dörediň", + "home.sessions.group.today": "Bu gün", + "home.sessions.group.yesterday": "Düýn", + "home.sessions.group.older": "Köne", + "home.providerTip": "Claude, GPT, Gemini we ş.m. ýaly beýleki modelleri ulanmak üçin 75+ üpjün edijä birikdiriň.", + "session.tab.session": "Sessiýa", + "session.tab.review": "Syn", + "session.tab.context": "Kontekst", + "session.tab.unknown": "Näbelli sessiýa", + "session.panel.reviewAndFiles": "Gözden geçiriň we faýllary", + "session.error.notFound": "Bu sessiýany tapyp bolmaýar", + "session.error.notFound.description": "Bu goýma, bu serwerde ýok bolan sessiýany görkezýär.", + "session.error.notFound.closeTab": "Tab ýapyň", + "session.error.serverConnection": "Bu serwere birigip bilemok", + "session.review.filesChanged": "Faýllar üýtgedildi {{count}}", + "session.review.change.one": "Üýtget", + "session.review.change.other": "Üýtgeşmeler", + "session.review.loadingChanges": "Üýtgeşmeler ýüklenýär ...", + "session.review.empty": "Bu sessiýada entek üýtgeşme ýok", + "session.review.noVcs": "Git wersiýa dolandyryş ulgamy tapylmady, üýtgeşmeler görkezilmedi", + "session.review.noVcs.createGit.title": "Git ammary dörediň", + "session.review.noVcs.createGit.description": "Bu taslamadaky üýtgeşmeleri yzarlaň, gözden geçiriň we yzyna alyň", + "session.review.noVcs.createGit.actionLoading": "Git ammaryny döretmek ...", + "session.review.noVcs.createGit.action": "Git ammary dörediň", + "session.review.noSnapshot": + "Snapshot yzarlamasy konfigurasiýada ýapyk, şonuň üçin sessiýa üýtgeşmeleri elýeterli däl", + "session.review.noChanges": "Üýtgeşme ýok", + "session.review.noUncommittedChanges": "Entek rugsat berilmedik üýtgeşmeler ýok", + "session.review.noBranchChanges": "Entek hiç hili şahamça üýtgemeýär", + "session.files.selectToOpen": "Açmak üçin bir faýl saýlaň", + "session.files.all": "Fileshli faýllar", + "session.files.empty": "Faýl ýok", + "session.files.binaryContent": "Ikilik faýl (mazmuny görkezilip bilinmez)", + "session.messages.renderEarlier": "Öňki habarlary beriň", + "session.messages.loadingEarlier": "Öňki habarlary ýüklemek ...", + "session.messages.loadEarlier": "Öňki habarlary ýükläň", + "session.messages.loading": "Habarlary ýüklemek ...", + "session.messages.jumpToLatest": "Iň soňkusyna geçiň", + "session.context.addToContext": "Mazmuna {{selection}} goşuň", + "session.todo.title": "Edilmeli işler", + "session.todo.collapse": "Apseykylmak", + "session.todo.expand": "Giňeltmek", + "session.todo.progress": "{{total}} todosynyň {{done}} tamamlandy", + "session.question.progress": "{{total}} soraglarynyň {{current}}", + "session.question.minimize": "Soragy azaldyň", + "session.question.restore": "Soragy dikelt", + "session.question.pending.one": "Garaşylýan sorag {{count}}", + "session.question.pending.other": "Garaşylýan soraglar {{count}}", + "session.followupDock.summary.one": "{{count}} nobatdaky habar", + "session.followupDock.summary.other": "{{count}} nobatdaky habarlar", + "session.followupDock.sendNow": "Indi iber", + "session.followupDock.edit": "Redaktirläň", + "session.followupDock.collapse": "Nobatly habarlary ýykmak", + "session.followupDock.expand": "Nobatly habarlary giňeltmek", + "session.revertDock.summary.one": "{{count}} yzyna gaýdyp geldi", + "session.revertDock.summary.other": "{{count}} yzyna iberilen habarlar", + "session.revertDock.collapse": "Yzyna gaýdyp gelen habarlary ýykmak", + "session.revertDock.expand": "Yza gaýdýan habarlary giňeltmek", + "session.revertDock.restore": "Habary dikelt", + "session.new.title": "Islän zadyňyzy guruň", + "session.new.project.new": "Täze taslama", + "session.new.project.search": "Taslamalary gözläň", + "session.new.project.add": "Taslama goşuň", + "session.new.worktree.main": "Esasy şahasy", + "session.new.worktree.mainWithBranch": "Esasy şahasy ({{branch}})", + "session.new.worktree.create": "Täze Git worktree dörediň", + "session.new.workspace.runIn": "Sessiýany işlediň", + "session.new.workspace.triggerLocal": "Localerli", + "session.new.workspace.local": "Localerli ammar", + "session.new.workspace.existing": "Workspace…", + "session.new.git.none": "Git ýok", + "session.new.lastModified": "Soňky gezek üýtgedildi", + "session.header.search.placeholder": "{{project}} gözläň", + "session.header.searchFiles": "Faýllary gözläň", + "session.header.openIn": "Aç", + "session.header.open.action": "{{app}} açyň", + "session.header.open.ariaLabel": "{{app}}-de açyň", + "session.header.open.menu": "Açyk görnüşler", + "session.header.open.copyPath": "Pathol göçüriň", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Faýl gözlegçisi", + "session.header.open.fileManager": "Faýl dolandyryjysy", + "session.header.reveal.finder": "Finder-de açyň", + "session.header.reveal.fileExplorer": "File Explorer-de açyň", + "session.header.reveal.containingFolder": "Içindäki bukjany açyň", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": ".Agdaý", + "status.popover.ariaLabel": "Serwer sazlamalary", + "status.popover.tab.servers": "Serwerler", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Pluginler", + "status.popover.action.manageServers": "Serwerleri dolandyryň", + "session.share.popover.title": "Webde çap ediň", + "session.share.popover.description.shared": + "Bu sessiýa internetde açykdyr. Salgysy bolan islendik adam üçin elýeterlidir.", + "session.share.popover.description.unshared": + "Sessiýany internetde köpçülige paýlaşyň. Baglanyşygy bolan islendik adam üçin elýeterli bolar.", + "session.share.action.share": "Paýlaş", + "session.share.action.publish": "Neşir et", + "session.share.action.publishing": "Neşir etmek ...", + "session.share.action.unpublish": "Neşir et", + "session.share.action.unpublishing": "Neşir etmek ...", + "session.share.action.view": "Gör", + "session.share.copy.copied": "Göçürildi", + "session.share.copy.copyLink": "Baglanyşygy göçüriň", + "lsp.tooltip.none": "LSP serwerleri ýok", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Prompt ýüklenýär ...", + "terminal.loading": "Terminal ýüklenýär ...", + "terminal.title": "Terminal", + "terminal.title.numbered": "{{number}} terminaly", + "terminal.close": "Terminaly ýapyň", + "terminal.connectionLost.title": "Baglanyşyk ýitdi", + "terminal.connectionLost.abnormalClose": "WebSocket adatdan daşary ýapyldy: {{code}}", + "terminal.connectionLost.description": "Terminal birikmesi kesildi. Serwer täzeden başlanda bolup biler.", + "terminal.connectTicket.csrfError": + "PTY baglanyşyk bileti gelip çykyşy ýa-da CSRF barlaglary bilen ret edilýär. CORS serwerini barlaň.", + "terminal.connectTicket.statusError": "PTY birikme bileti {{status}} bilen şowsuz boldy", + "titlebar.update": "Täzelen", + "titlebar.updateVersion": "{{version}} täzeläň", + "common.closeTab": "Salgy ýapyň", + "common.dismiss": "Işden aýyrmak", + "common.moreCountSuffix": " (+ {{count}} has köp)", + "common.requestFailed": "Talap başa barmady", + "common.moreOptions": "Has köp wariant", + "common.learnMore": "Has giňişleýin öwreniň", + "common.rename": "Adyny üýtgetmek", + "common.export": "Eksport", + "common.reset": "Täzeden düzmek", + "common.archive": "Arhiw", + "common.delete": "Poz", + "common.close": ".Akyn", + "common.edit": "Redaktirläň", + "common.loadMore": "Has köp ýükläň", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "näbelli", + "common.time.justNow": "Edil häzir", + "common.time.minutesAgo.short": "{{count}}m öň", + "common.time.hoursAgo.short": "{{count}}h öň", + "common.time.daysAgo.short": "{{count}}d öň", + "sidebar.menu.toggle": "Menýu açyň", + "sidebar.nav.projectsAndSessions": "Taslamalar we sessiýalar", + "sidebar.settings": "Sazlamalar", + "sidebar.help": "Kömek", + "sidebar.workspaces.enable": "Workspace-lary açyň", + "sidebar.workspaces.disable": "Workspace-lary öçüriň", + "sidebar.gettingStarted.title": "Başlamak", + "sidebar.gettingStarted.line1": "OpenCode mugt modelleri öz içine alýar, derrew başlap bilersiňiz.", + "sidebar.gettingStarted.line2": + "Modelleri ulanmak üçin islendik üpjün edijini birikdiriň. Claude, GPT, Gemini we ş.m.", + "sidebar.project.recentSessions": "Soňky sessiýalar", + "sidebar.project.viewAllSessions": "Allhli sessiýalary görüň", + "sidebar.project.clearNotifications": "Duýduryşlary arassalaň", + "sidebar.empty.title": "Taslama açylmaýar", + "sidebar.empty.description": "Başlamak üçin taslama açyň", + "debugBar.ariaLabel": "Ösüş öndürijiligini anyklaýyş", + "debugBar.na": "n / a", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Iň soňky tamamlanan marşrut geçiş, sessiýa sahypasyna degip, marşrutizatoryň başyndan başlap, birinji boýag kesgitlenýänçä ölçelýär.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Soňky 5 sekuntda sekuntda aýlaw çarçuwalary.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Soňky 5 sekundyň içinde iň erbet kadr wagty.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Soňky 5 sekuntda 32 metrden gowrak çarçuwalar.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Blok edilen wagt we soňky 5 sekuntda uzak iş. Iň ýokary wezipe: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Iň erbet syn edilen soňky 5 sekuntda.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Soňky 5 sekundyň dowamynda takmynan täsirleşme dowamlylygy. Bu INP ýaly, resmi Web Vital INP däl.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Häzirki programmanyň ömri üçin jemleýji düzüliş çalşygy.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Ulanylan JS üýşmesi vs üýşmek çägi. Diňe Chromium.", + "debugBar.mem.tip": "Ulanylan JS üýşmesi vs üýşmek çägi. {{limit}}-iň {{used}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Intacthli interaktiw elementlere fokus stillerini mejbur ediň", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Saýlanan dili üýtgetmän doly programma düzüliş ugruny mejbur ediň", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Iş stoly", + "settings.section.server": "Serwer", + "settings.tab.general": "Umumy", + "settings.tab.shortcuts": "Salgylanmalar", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integrasiýa", + "settings.desktop.wsl.description": "OpenCode serwerini WSL-de Windows-de işlediň.", + "settings.general.section.appearance": "Daş görnüşi", + "settings.general.section.advanced": "Ösen", + "settings.general.section.notifications": "Ulgam habarnamalary", + "settings.general.section.updates": "Täzelenmeler", + "settings.general.section.sounds": "Ses effektleri", + "settings.general.section.feed": "Iýmit", + "settings.general.section.display": "Ekran", + "settings.general.row.language.title": "Dil", + "settings.general.row.language.description": "OpenCode üçin displeý dilini üýtgediň", + "settings.general.row.shell.title": "Terminal shell-i", + "settings.general.row.shell.description": "Terminal we agent gurallary tarapyndan ulanylýan shell", + "settings.general.row.shell.autoDefault": "Awto (Bellenilen)", + "settings.general.row.shell.terminalOnly": "Diňe terminal", + "settings.general.row.appearance.title": "Daş görnüşi", + "settings.general.row.appearance.description": "OpenCode enjamyňyzda nähili görünýändigini düzüň", + "settings.general.row.colorScheme.title": "Reňk shemasy", + "settings.general.row.colorScheme.description": + "OpenCode ulgamy, ýagtylygy ýa-da garaňky temany yzarlaýandygyny saýlaň", + "settings.general.row.theme.title": "Mowzuk", + "settings.general.row.theme.description": "OpenCode-iň mowzuklydygyny düzüň.", + "settings.general.row.font.title": "Kod şrifti", + "settings.general.row.font.description": "Kod bloklarynda ulanylýan şrifti düzüň", + "settings.general.row.terminalFont.title": "Terminal şrifti", + "settings.general.row.terminalFont.description": "Terminalda ulanylýan şrifti düzüň", + "settings.general.row.uiFont.title": "UI şrift", + "settings.general.row.uiFont.description": "Interfeýsiň hemme ýerinde ulanylýan şrifti düzüň", + "settings.general.row.followup.title": "Gözegçilik", + "settings.general.row.followup.description": + "Gözegçiligiň haýal etmän dolandyrjakdygyny ýa-da nobata garaşýandygyny saýlaň", + "settings.general.row.followup.option.queue": "Nobat", + "settings.general.row.followup.option.steer": "Rul", + "settings.general.row.showFileTree.title": "Faýl agajy", + "settings.general.row.showFileTree.description": "Faýl agajy panelini seanslarda görkeziň", + "settings.general.row.showNavigation.title": "Nawigasiýa dolandyryşlary", + "settings.general.row.showNavigation.description": "Iş stolunyň ady setirinde yzky we öňe düwmeleri görkeziň", + "settings.general.row.showSearch.title": "Buýruk palitrasy", + "settings.general.row.showSearch.description": "Gözleg we buýruk palitrasy düwmesini sözbaşy setirinde görkeziň", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Iş stolunyň ady setirinde terminal düwmesini görkeziň", + "settings.general.row.showStatus.title": "Serweriň ýagdaýy", + "settings.general.row.showStatus.description": "Serweriň ýagdaý düwmesini sözbaşy setirinde görkeziň", + "settings.general.row.mobileTitlebarBottom.title": "Aşakdaky nawigasiýa", + "settings.general.row.mobileTitlebarBottom.description": + "Adyň setirini we sessiýa belliklerini ekranyň aşagyna ykjam ýerleşdiriň", + "settings.general.row.showCustomAgents.title": "Agent görkez", + "settings.general.row.showCustomAgents.description": + "Kompozitordaky agentleriň arasynda geçiň. Gizlenende, agent gurmak üçin defolt.", + "settings.general.row.reasoningSummaries.title": "Pikirleriň gysgaça mazmunyny görkeziň", + "settings.general.row.reasoningSummaries.description": + "Wagt görkezijisinde model pikirlenişiň gysgaça mazmunyny görkeziň", + "settings.general.row.shellToolPartsExpanded.title": "Shell gural böleklerini giňeltmek", + "settings.general.row.shellToolPartsExpanded.description": + "Wagt görkezijisinde deslapky görnüşde giňeldilen shell gural böleklerini görkeziň", + "settings.general.row.editToolPartsExpanded.title": "Redaktirleme gurallarynyň böleklerini giňeltmek", + "settings.general.row.editToolPartsExpanded.description": + "Wagt tertibinde tertip boýunça giňeldilen redaktirlemegi, ýazmagy we patch gurallaryny görkeziň", + "settings.general.row.newInterface.title": "Täze düzüliş", + "settings.general.row.newInterface.badge": "Täze", + "settings.general.row.newInterface.description": + "Täze goýmalary we öý tertibini ulanyň. Çäkli wagt üçin düzülişleriň arasynda geçiň.", + "settings.general.row.newInterfaceNotice.title": "Indi täze düzülişi ulanýarsyňyz", + "settings.general.row.newInterfaceNotice.description": "Öňki düzüliş indi elýeterli däl", + "settings.general.row.newInterfaceNotice.dismiss": "Işden aýyrmak", + "settings.general.row.pinchZoom.title": "Ulaltmak üçin çümmük", + "settings.general.row.pinchZoom.description": "Trackpad çümmüklerine we Ctrl-aýlaw hereketlerine ýakynlaşdyryň", + "settings.general.row.wayland.title": "Nativeerli Wayland ulanyň", + "settings.general.row.wayland.description": "Wayland-de X11 yza gaýdyp gelmegi öçüriň. Gaýtadan açmagy talap edýär.", + "settings.general.row.wayland.tooltip": + "Garyşyk täzeleniş tizligi monitorlary bolan Linux-de ýerli Wayland has durnukly bolup biler.", + "settings.general.row.releaseNotes.title": "Bellikleri goýberiň", + "settings.general.row.releaseNotes.description": "Täzelenmelerden soň täze çykýanlary görkeziň", + "settings.updates.row.startup.title": "Başlangyçdaky täzelenmeleri barlaň", + "settings.updates.row.startup.description": "OpenCode işe başlanda täzelenmeleri awtomatiki barlaň", + "settings.updates.row.check.title": "Täzelenmeleri barlaň", + "settings.updates.row.check.description": "Täzelenmeleri el bilen barlaň we bar bolsa guruň", + "settings.updates.action.checkNow": "Indi barlaň", + "settings.updates.action.checking": "Barlamak ...", + "settings.updates.action.downloading": "Ingüklemek ...", + "settings.updates.action.installing": "Gurmak ...", + "settings.updates.toast.latest.title": "Döwrebap", + "settings.updates.toast.latest.description": "OpenCode-iň iň soňky wersiýasyny işleýärsiňiz.", + "sound.option.none": "Hiç", + "sound.option.alert01": "Duýduryş 01", + "sound.option.alert02": "Duýduryş 02", + "sound.option.alert03": "Duýduryş 03", + "sound.option.alert04": "Duýduryş 04", + "sound.option.alert05": "Duýduryş 05", + "sound.option.alert06": "Duýduryş 06", + "sound.option.alert07": "Duýduryş 07", + "sound.option.alert08": "Duýduryş 08", + "sound.option.alert09": "Duýduryş 09", + "sound.option.alert10": "Duýduryş 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "01ok 01", + "sound.option.nope02": "02ok", + "sound.option.nope03": "03ok", + "sound.option.nope04": "04ok 04", + "sound.option.nope05": "05ok 05", + "sound.option.nope06": "Opeok 06", + "sound.option.nope07": "07ok 07", + "sound.option.nope08": "08ok 08", + "sound.option.nope09": "0ok 09", + "sound.option.nope10": "10ok", + "sound.option.nope11": "11ok", + "sound.option.nope12": "Opeok 12", + "sound.option.yup01": "01ok 01", + "sound.option.yup02": "Upok 02", + "sound.option.yup03": "03ok 03", + "sound.option.yup04": "Upup 04", + "sound.option.yup05": "05ok 05", + "sound.option.yup06": "Upok 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Agent doly bolanda ýa-da ünsi talap edende ulgam habarnamasyny görkeziň", + "settings.general.notifications.permissions.title": "Rugsatlar", + "settings.general.notifications.permissions.description": "Rugsat talap edilende ulgam habarnamasyny görkeziň", + "settings.general.notifications.errors.title": "Rorsalňyşlyklar", + "settings.general.notifications.errors.description": "Ýalňyşlyk ýüze çykanda ulgam habarnamasyny görkeziň", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Agent doly bolanda ýa-da ünsi talap edende ses çalyň", + "settings.general.sounds.permissions.title": "Rugsatlar", + "settings.general.sounds.permissions.description": "Rugsat gerek bolsa ses çalyň", + "settings.general.sounds.errors.title": "Rorsalňyşlyklar", + "settings.general.sounds.errors.description": "Ýalňyşlyk ýüze çykanda ses çalyň", + "settings.shortcuts.title": "Klawiatura salgylanmalary", + "settings.shortcuts.reset.button": "Dymmaklyklara gaýtaryň", + "settings.shortcuts.reset.toast.title": "Salgylanmalar täzeden düzülýär", + "settings.shortcuts.reset.toast.description": "Klawiatura gysga ýollary deslapky görnüşde täzeden düzüldi.", + "settings.shortcuts.conflict.title": "Salgylanma eýýäm ulanylýar", + "settings.shortcuts.conflict.description": "{{keybind}} eýýäm {{titles}}-e bellendi.", + "settings.shortcuts.unassigned": "Bellenmedik", + "settings.shortcuts.pressKeys": "Düwmeleri basyň", + "settings.shortcuts.search.placeholder": "Salgylanmalary gözläň", + "settings.shortcuts.search.empty": "Salgylanmalar tapylmady", + "settings.shortcuts.group.general": "Umumy", + "settings.shortcuts.group.session": "Sessiýa", + "settings.shortcuts.group.navigation": "Nawigasiýa", + "settings.shortcuts.group.modelAndAgent": "Model we agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Prompt", + "settings.providers.title": "Üpjün edijiler", + "settings.providers.description": "Üpjün edijiniň sazlamalary bu ýerde düzülip bilner.", + "settings.providers.section.connected": "Birikdirilen üpjün edijiler", + "settings.providers.connected.empty": "Baglanan üpjün edijiler ýok", + "settings.providers.connected.environmentDescription": "Daşky gurşaw üýtgeýjilerinden birikdirildi", + "settings.providers.section.popular": "Meşhur üpjün edijiler", + "settings.providers.custom.description": "URL esasly OpenAI gabat gelýän üpjün edijini goşuň.", + "settings.providers.tag.environment": "Daşky gurşaw", + "settings.providers.tag.config": "Sazlamak", + "settings.providers.tag.custom": "Omörite", + "settings.providers.tag.other": "Beýlekiler", + "settings.models.title": "Modeller", + "settings.models.description": "Model sazlamalary bu ýerde düzülip bilner.", + "settings.agents.title": "Agentler", + "settings.agents.description": "Agent sazlamalary bu ýerde düzülip bilner.", + "settings.commands.title": "Buýruklar", + "settings.commands.description": "Buýruk sazlamalary bu ýerde düzülip bilner.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP sazlamalary bu ýerde düzülip bilner.", + "settings.permissions.title": "Rugsatlar", + "settings.permissions.description": "Serweriň haýsy gurallary ulanyp biljekdigini dolandyryň.", + "settings.permissions.section.tools": "Gurallar", + "settings.permissions.toast.updateFailed.title": "Rugsatlary täzeläp bilmedi", + "settings.permissions.action.allow": "Rugsat beriň", + "settings.permissions.action.ask": "Soraň", + "settings.permissions.action.deny": "Inkär et", + "settings.permissions.tool.read.title": "Oka", + "settings.permissions.tool.read.description": "Faýl okamak (faýl ýoluna gabat gelýär)", + "settings.permissions.tool.edit.title": "Redaktirläň", + "settings.permissions.tool.edit.description": "Faýllary, şol sanda redaktirlemegi, ýazmagy we ýamalary üýtgediň", + "settings.permissions.tool.glob.title": "Globus", + "settings.permissions.tool.glob.description": "Global faýllary ulanyp faýllary deňeşdiriň", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Faýl mazmunyny yzygiderli aňlatmalar arkaly gözläň", + "settings.permissions.tool.list.title": "Sanaw", + "settings.permissions.tool.list.description": "Faýllary bukjanyň içinde sanaň", + "settings.permissions.tool.bash.title": "Baş", + "settings.permissions.tool.bash.description": "Shell buýruklaryny işlediň", + "settings.permissions.tool.task.title": "Wezipe", + "settings.permissions.tool.task.description": "Sub-agentleri işe giriziň", + "settings.permissions.tool.skill.title": "Ussatlyk", + "settings.permissions.tool.skill.description": "Bir ussatlygy at bilen ýükläň", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Dil serweriniň talaplaryny işlediň", + "settings.permissions.tool.todowrite.title": "Todo ýaz", + "settings.permissions.tool.todowrite.description": "Todo sanawyny täzeläň", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Mazmuny URL-den alyň", + "settings.permissions.tool.websearch.title": "Web gözlegi", + "settings.permissions.tool.websearch.description": "Webde gözläň", + "settings.permissions.tool.external_directory.title": "Daşarky katalog", + "settings.permissions.tool.external_directory.description": "Taslama bukjasynyň daşyndaky faýllara giriň", + "settings.permissions.tool.doom_loop.title": "Doom aýlawy", + "settings.permissions.tool.doom_loop.description": "Birmeňzeş giriş bilen gaýtalanýan gural jaňlaryny kesgitläň", + "session.delete.failed.title": "Sessiýany pozup bilmedi", + "session.delete.title": "Sessiýany poz", + "session.delete.confirm": '"{{name}}" sessiýasyny pozmalymy?', + "session.delete.button": "Sessiýany poz", + "workspace.new": "Täze workspace", + "workspace.type.local": "ýerli", + "workspace.type.sandbox": "sandyk", + "workspace.create.failed.title": "Workspace döredip bolmady", + "workspace.delete.failed.title": "Workspace pozup bolmady", + "workspace.resetting.title": "Workspace täzeden düzülýär", + "workspace.resetting.description": "Bu bir minut alyp biler.", + "workspace.reset.failed.title": "Workspace täzeden düzüp bolmady", + "workspace.reset.success.title": "Workspace täzeden düzüldi", + "workspace.reset.success.description": "Workspace indi adaty şaha gabat gelýär.", + "workspace.error.stillPreparing": "Workspace henizem taýýarlanýar", + "workspace.status.checking": "Birleşdirilmedik üýtgeşmeleri barlamak ...", + "workspace.status.error": "Git ýagdaýyny barlap bolmaýar.", + "workspace.status.clean": "Düzülmedik üýtgeşmeler tapylmady.", + "workspace.status.dirty": "Bu workspace-de birleşdirilmedik üýtgeşmeler ýüze çykaryldy.", + "workspace.delete.title": "Workspace-i poz", + "workspace.delete.confirm": '"{{name}}" workspace-i pozmalymy?', + "workspace.delete.button": "Workspace-i poz", + "workspace.reset.title": "Workspace-i täzeden düzmek", + "workspace.reset.confirm": '"{{name}}" workspace-i täzeden düzmelimi?', + "workspace.reset.button": "Workspace-i täzeden düzmek", + "workspace.reset.archived.none": "Hiç bir işjeň sessiýa arhiwlenmez.", + "workspace.reset.archived.one": "1 sessiýa arhiwlener.", + "workspace.reset.archived.many": "{{count}} sessiýalary arhiwlener.", + "workspace.reset.note": "Bu workspace-i adaty şaha gabat getirmek üçin täzeden düzer.", + "dialog.usageExceeded.dontShowAgain": "Gaýtadan görkezme", +} diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts new file mode 100644 index 0000000000000000000000000000000000000000..85623036e429663689fb6c2bb61381c267152607 --- /dev/null +++ b/packages/app/src/i18n/tr.ts @@ -0,0 +1,1260 @@ +import { dict as en } from "./en" + +type Keys = keyof typeof en + +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Dosya", + "desktop.menu.edit": "Düzen", + "desktop.menu.view": "Görünüm", + "desktop.menu.go": "Git", + "desktop.menu.window": "Pencere", + "desktop.menu.help": "Yardım", + "desktop.menu.checkForUpdates": "Güncellemeleri kontrol et...", + "desktop.menu.settings": "Ayarlar", + "desktop.menu.reloadWebview": "Web görünümünü yeniden yükle", + "desktop.menu.restart": "Yeniden başlat", + "desktop.menu.exportLogs": "Günlükleri dışa aktar...", + "desktop.menu.newSession": "Yeni oturum", + "desktop.menu.openProject": "Proje aç...", + "desktop.menu.newWindow": "Yeni pencere", + "desktop.menu.closeWindow": "Pencereyi kapat", + "desktop.menu.undo": "Geri al", + "desktop.menu.redo": "Yinele", + "desktop.menu.cut": "Kes", + "desktop.menu.copy": "Kopyala", + "desktop.menu.paste": "Yapıştır", + "desktop.menu.delete": "Sil", + "desktop.menu.selectAll": "Tümünü seç", + "desktop.menu.toggleSidebar": "Kenar çubuğunu aç/kapat", + "desktop.menu.toggleTerminal": "Terminali aç/kapat", + "desktop.menu.toggleFileTree": "Dosya ağacını aç/kapat", + "desktop.menu.reload": "Yeniden yükle", + "desktop.menu.toggleDeveloperTools": "Geliştirici araçlarını aç/kapat", + "desktop.menu.actualSize": "Gerçek boyut", + "desktop.menu.zoomIn": "Yakınlaştır", + "desktop.menu.zoomOut": "Uzaklaştır", + "desktop.menu.toggleFullScreen": "Tam ekranı aç/kapat", + "desktop.menu.back": "Geri", + "desktop.menu.forward": "İleri", + "desktop.menu.previousSession": "Önceki oturum", + "desktop.menu.nextSession": "Sonraki oturum", + "desktop.menu.previousProject": "Önceki proje", + "desktop.menu.nextProject": "Sonraki proje", + "desktop.menu.minimize": "Simge durumuna küçült", + "desktop.menu.maximize": "Ekranı kapla", + "desktop.menu.documentation": "OpenCode belgeleri", + "desktop.menu.supportForum": "Destek forumu", + "desktop.menu.shareFeedback": "Geri bildirim paylaş", + "desktop.menu.reportBug": "Hata bildir", + "desktop.menu.ariaLabel": "OpenCode menüsü", + + "desktop.updater.dialog.checkFailed.message": "Güncellemeler kontrol edilemedi.", + "desktop.updater.dialog.checkFailed.title": "Güncelleme hatası", + "desktop.updater.dialog.upToDate.message": "En son sürümü kullanıyorsunuz.", + "desktop.updater.dialog.upToDate.title": "Güncelleme yok", + "desktop.updater.dialog.ready.message": "{{version}} güncellemesi indirildi. Şimdi yeniden başlatılsın mı?", + "desktop.updater.dialog.ready.title": "Güncelleme hazır", + "desktop.updater.dialog.restart": "Yeniden başlat", + "desktop.updater.dialog.later": "Daha sonra", + + "desktop.recovery.action.relaunch": "Uygulamayı yeniden başlat", + "desktop.recovery.action.exportLogs": "Günlükleri dışa aktar", + "desktop.recovery.action.keepWaiting": "Beklemeye devam et", + "desktop.recovery.action.quit": "Çık", + "desktop.recovery.loadFailed": "OpenCode yüklenemedi", + "desktop.recovery.terminated": "OpenCode penceresi beklenmedik şekilde sonlandırıldı", + "desktop.recovery.unresponsive": "OpenCode yanıt vermiyor", + "desktop.recovery.unresponsive.detail": + "Uygulamayı yeniden başlatabilir, günlükleri açabilir veya beklemeye devam edebilirsiniz.", + "desktop.recovery.loadFailed.detail": "Pencere: {{window}}\nURL: {{url}}\nHata: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Pencere: {{window}}\nNeden: {{reason}}\nKod: {{code}}", + "desktop.recovery.unknown": "", + + "desktop.dialog.chooseFolder": "Bir klasör seçin", + "desktop.dialog.chooseFile": "Bir dosya seçin", + "desktop.dialog.saveFile": "Dosyayı kaydedin", + "desktop.dialog.files": "Dosyalar", + "desktop.server.local": "Yerel sunucu", + + "desktop.wsl.error.windowsOnly": "WSL yalnızca Windows'ta kullanılabilir", + "desktop.wsl.error.unavailable": "WSL kullanılamıyor", + "desktop.wsl.error.listInstalled": "Kurulu WSL dağıtımları listelenemedi", + "desktop.wsl.error.listOnline": "Çevrimiçi WSL dağıtımları listelenemedi", + "desktop.wsl.error.executeDistro": "Dağıtımda komutlar çalıştırılamıyor", + "desktop.wsl.error.installWsl": "WSL kurulamadı", + "desktop.wsl.error.installDistro": "Dağıtım kurulamadı: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode kurulamadı", + "desktop.wsl.error.alreadyAdded": "{{distro}} zaten eklendi", + "desktop.wsl.error.opencodeMissing": "opencode bu dağıtımda kurulu değil", + "desktop.wsl.error.opencodeCannotRun": "opencode kurulu ancak çalıştırılamadı", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode, {{distro}} içinde kurulu değil", + "desktop.wsl.error.updateVersion": + "OpenCode güncellemesi tamamlandı ancak {{distro}} hâlâ {{installed}} sürümünü bildiriyor; beklenen sürüm: {{expected}}", + "desktop.wsl.error.noVersion": "sürüm yok", + "desktop.wsl.error.serverExited": "WSL sunucusu başlatıldıktan sonra kapandı (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL sunucusu kullanıma hazır duruma gelmeden kapandı (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "{{distro}} Sidecar sistem durumu denetimi {{timeout}} ms sonra zaman aşımına uğradı", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} komutu {{timeout}} ms sonra zaman aşımına uğradı", + "desktop.wsl.error.failedPort": "Bağlantı noktası alınamadı", + + "desktop.picker.error.notSelected": "Dosya seçici tarafından dosya seçilmedi", + "desktop.picker.error.sizeLimit": "Seçilen ekler {{limit}} MB sınırını aşıyor", + + "command.category.suggested": "Önerilen", + "command.category.view": "Görünüm", + "command.category.project": "Proje", + "command.category.provider": "Sağlayıcı", + "command.category.server": "Sunucu", + "command.category.session": "Oturum", + "command.category.theme": "Tema", + "command.category.language": "Dil", + "command.category.file": "Dosya", + "command.category.context": "Bağlam", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Ajan", + "command.category.permissions": "İzinler", + "command.category.workspace": "Çalışma Alanı", + "command.category.settings": "Ayarlar", + "command.logs.export": "Günlükleri dışa aktar", + + "theme.scheme.system": "Sistem", + "theme.scheme.light": "Açık", + "theme.scheme.dark": "Koyu", + + "command.sidebar.toggle": "Kenar çubuğunu aç/kapat", + "command.project.open": "Proje aç", + "command.project.previous": "Önceki proje", + "command.project.next": "Sonraki proje", + "command.project.index": "{{index}} numaralı projeye geç", + "command.provider.connect": "Sağlayıcı bağla", + "command.server.switch": "Sunucu değiştir", + "command.settings.open": "Ayarları aç", + "command.session.previous": "Önceki oturum", + "command.session.next": "Sonraki oturum", + "command.session.previous.unseen": "Önceki okunmamış oturum", + "command.session.next.unseen": "Sonraki okunmamış oturum", + "command.session.archive": "Oturumu arşivle", + + "command.palette": "Komut paleti", + + "command.theme.cycle": "Tema değiştir", + "command.theme.set": "Tema kullan: {{theme}}", + "command.theme.scheme.cycle": "Renk şemasını değiştir", + "command.theme.scheme.set": "Renk şeması kullan: {{scheme}}", + + "command.language.cycle": "Dil değiştir", + "command.language.set": "Dil kullan: {{language}}", + + "command.session.new": "Yeni oturum", + "command.file.open": "Dosya aç", + "command.tab.close": "Sekmeyi kapat", + "command.tab.reopenClosed": "Kapatılan sekmeyi yeniden aç", + "command.context.addSelection": "Seçimi bağlama ekle", + "command.context.addSelection.description": "Mevcut dosyadan seçili satırları ekle", + "command.input.focus": "Girişi odakla", + "command.terminal.toggle": "Terminali aç/kapat", + "command.fileTree.toggle": "Dosya ağacını aç/kapat", + "command.review.toggle": "İncelemeyi aç/kapat", + "command.terminal.new": "Yeni terminal", + "command.terminal.new.description": "Yeni bir terminal sekmesi oluştur", + "command.steps.toggle": "Adımları aç/kapat", + "command.steps.toggle.description": "Mevcut mesaj için adımları göster veya gizle", + "command.message.previous": "Önceki mesaj", + "command.message.previous.description": "Önceki kullanıcı mesajına git", + "command.message.next": "Sonraki mesaj", + "command.message.next.description": "Sonraki kullanıcı mesajına git", + "command.model.choose": "Model seç", + "command.model.choose.description": "Farklı bir model seç", + "command.mcp.toggle": "MCP'leri aç/kapat", + "command.mcp.toggle.description": "MCP'leri aç/kapat", + "command.agent.cycle": "Ajan değiştir", + "command.agent.cycle.description": "Sonraki ajana geç", + "command.agent.cycle.reverse": "Ajanı geri değiştir", + "command.agent.cycle.reverse.description": "Önceki ajana geç", + "command.model.variant.cycle": "Düşünme eforu değiştir", + "command.model.variant.cycle.description": "Sonraki efor seviyesine geç", + "command.prompt.mode.shell": "Kabuk", + "command.prompt.mode.normal": "İstem", + "command.permissions.autoaccept.enable": "İzinleri otomatik kabul et", + "command.permissions.autoaccept.disable": "İzinleri otomatik kabul etmeyi durdur", + "command.workspace.toggle": "Çalışma alanlarını aç/kapat", + "command.workspace.toggle.description": "Kenar çubuğunda birden fazla çalışma alanını göster veya gizle", + "command.session.undo": "Geri al", + "command.session.undo.description": "Son mesajı geri al", + "command.session.redo": "Yinele", + "command.session.redo.description": "Son geri alınan mesajı yinele", + "command.session.compact": "Oturumu sıkıştır", + "command.session.compact.description": "Bağlam boyutunu azaltmak için oturumu özetle", + "command.session.fork": "Mesajdan dallandır", + "command.session.fork.description": "Önceki bir mesajdan yeni oturum oluştur", + "command.session.share": "Oturumu paylaş", + "command.session.share.description": "Bu oturumu paylaş ve URL'yi panoya kopyala", + "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ı", + "palette.group.commands": "Komutlar", + "palette.group.files": "Dosyalar", + + "dialog.provider.search.placeholder": "Sağlayıcı ara", + "dialog.provider.empty": "Sağlayıcı bulunamadı", + "dialog.provider.group.popular": "Popüler", + "dialog.provider.group.other": "Diğer", + "dialog.provider.custom.label": "Özel OpenAI uyumlu sağlayıcı", + "dialog.provider.tag.recommended": "Önerilen", + "dialog.provider.opencode.note": "Claude, GPT, Gemini ve daha fazlasını içeren seçilmiş modeller", + "dialog.provider.opencode.tagline": "Güvenilir, optimize edilmiş modeller", + "dialog.provider.opencodeGo.tagline": "Herkes için düşük maliyetli abonelik", + "dialog.provider.anthropic.note": "Pro ve Max dahil Claude modellerine doğrudan erişim", + "dialog.provider.copilot.note": "GitHub Copilot üzerinden kodlama yardımı için yapay zekâ modelleri", + "dialog.provider.openai.note": "Hızlı ve yetenekli genel yapay zekâ görevleri için GPT modelleri", + "dialog.provider.google.note": "Hızlı ve yapılandırılmış yanıtlar için Gemini modelleri", + "dialog.provider.openrouter.note": "Tek bir sağlayıcıdan tüm desteklenen modellere eriş", + "dialog.provider.vercel.note": "Akıllı yönlendirme ile yapay zekâ modellerine birleşik erişim", + + "dialog.model.select.title": "Model seç", + "dialog.model.search.placeholder": "Model ara", + "dialog.model.empty": "Model sonucu yok", + "dialog.model.manage": "Modelleri yönet", + "dialog.model.manage.description": "Model seçicide hangi modellerin görüneceğini özelleştirin.", + "dialog.model.manage.provider.toggle": "Tüm {{provider}} modellerini aç/kapat", + + "dialog.model.unpaid.freeModels.title": "OpenCode tarafından sunulan ücretsiz modeller", + "dialog.model.unpaid.addMore.title": "Popüler sağlayıcılardan daha fazla model ekleyin", + "dialog.model.unpaid.viewMoreProviders": "70'ten fazla sağlayıcı daha görüntüle", + + "dialog.provider.viewAll": "Daha fazla sağlayıcı göster", + + "provider.connect.title": "{{provider}} bağla", + "provider.connect.title.anthropicProMax": "Claude Pro/Max ile giriş yap", + "provider.connect.selectMethod": "{{provider}} için giriş yöntemini seçin.", + "provider.connect.method.apiKey": "API anahtarı", + "provider.connect.method.browser": "Tarayıcı", + "provider.connect.method.headless": "Tarayıcısız", + "provider.connect.status.inProgress": "Yetkilendirme devam ediyor...", + "provider.connect.status.waiting": "Yetkilendirme bekleniyor...", + "provider.connect.status.failed": "Yetkilendirme başarısız: {{error}}", + "provider.connect.apiKey.description": + "{{provider}} hesabınızı bağlamak ve OpenCode'da {{provider}} modellerini kullanmak için API anahtarınızı girin.", + "provider.connect.apiKey.label": "{{provider}} API anahtarı", + "provider.connect.apiKey.placeholder": "API anahtarı", + "provider.connect.apiKey.required": "API anahtarı gerekli", + "provider.connect.opencodeZen.line1": + "OpenCode Zen, kodlama ajanları için seçilmiş güvenilir optimize edilmiş modellere erişim sağlar.", + "provider.connect.opencodeZen.line2": + "Tek bir API anahtarıyla Claude, GPT, Gemini, GLM ve daha fazlası gibi modellere erişebilirsiniz.", + "provider.connect.opencodeZen.visit.prefix": "", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " adresini ziyaret ederek API anahtarınızı alın.", + "provider.connect.oauth.code.visit.prefix": "Yetkilendirme kodunuzu almak için ", + "provider.connect.oauth.code.visit.link": "bu bağlantıya", + "provider.connect.oauth.code.visit.suffix": + " gidin; ardından hesabınızı bağlayarak OpenCode'da {{provider}} modellerini kullanın.", + "provider.connect.oauth.code.label": "{{method}} yetkilendirme kodu", + "provider.connect.oauth.code.placeholder": "Yetkilendirme kodu", + "provider.connect.oauth.code.required": "Yetkilendirme kodu gerekli", + "provider.connect.oauth.code.invalid": "Geçersiz yetkilendirme kodu", + "provider.connect.oauth.auto.visit.prefix": "", + "provider.connect.oauth.auto.visit.link": "Bu bağlantıya", + "provider.connect.oauth.auto.visit.suffix": + " tıklayarak aşağıdaki kodu girin ve hesabınızı bağlayarak OpenCode'da {{provider}} modellerini kullanın.", + "provider.connect.oauth.auto.confirmationCode": "Onay kodu", + "provider.connect.toast.connected.title": "{{provider}} bağlandı", + "provider.connect.toast.connected.description": "{{provider}} modelleri artık kullanımda.", + + "provider.custom.title": "Özel sağlayıcı", + "provider.custom.unavailable": "Özel sağlayıcılar bu sunucuda kullanılamıyor", + "provider.custom.description.prefix": "OpenAI uyumlu bir sağlayıcı yapılandırın. ", + "provider.custom.description.link": "sağlayıcı yapılandırma dokümanları", + "provider.custom.description.suffix": " sayfasına bakın.", + "provider.custom.field.providerID.label": "Sağlayıcı kimliği", + "provider.custom.field.providerID.placeholder": "saglayicim", + "provider.custom.field.providerID.description": "Küçük harfler, rakamlar, tire veya alt çizgi", + "provider.custom.field.name.label": "Görünen ad", + "provider.custom.field.name.placeholder": "Yapay Zekâ Sağlayıcım", + "provider.custom.field.baseURL.label": "Temel URL", + "provider.custom.field.baseURL.placeholder": "https://api.saglayicim.com/v1", + "provider.custom.field.apiKey.label": "API anahtarı", + "provider.custom.field.apiKey.placeholder": "API anahtarı", + "provider.custom.field.apiKey.description": + "İsteğe bağlı. Kimlik doğrulamayı başlıklarla yönetiyorsanız boş bırakın.", + "provider.custom.models.label": "Modeller", + "provider.custom.models.id.label": "Kimlik", + "provider.custom.models.id.placeholder": "model-kimlik", + "provider.custom.models.name.label": "Ad", + "provider.custom.models.name.placeholder": "Görünen Ad", + "provider.custom.models.remove": "Modeli kaldır", + "provider.custom.models.add": "Model ekle", + "provider.custom.headers.label": "Başlıklar (isteğe bağlı)", + "provider.custom.headers.key.label": "Başlık", + "provider.custom.headers.key.placeholder": "Başlık-Adı", + "provider.custom.headers.value.label": "Değer", + "provider.custom.headers.value.placeholder": "değer", + "provider.custom.headers.remove": "Başlığı kaldır", + "provider.custom.headers.add": "Başlık ekle", + "provider.custom.error.providerID.required": "Sağlayıcı kimliği gerekli", + "provider.custom.error.providerID.format": "Küçük harf, rakam, tire veya alt çizgi kullanın", + "provider.custom.error.providerID.exists": "Bu sağlayıcı kimliği zaten mevcut", + "provider.custom.error.name.required": "Görünen ad gerekli", + "provider.custom.error.baseURL.required": "Temel URL gerekli", + "provider.custom.error.baseURL.format": "http:// veya https:// ile başlamalı", + "provider.custom.error.required": "Gerekli", + "provider.custom.error.duplicate": "Tekrar", + + "provider.disconnect.toast.disconnected.title": "{{provider}} bağlantısı kesildi", + "provider.disconnect.toast.disconnected.description": "{{provider}} modelleri artık kullanılamıyor.", + + "model.tag.free": "Ücretsiz", + "model.tag.latest": "En yeni", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "metin", + "model.input.image": "görsel", + "model.input.audio": "ses", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Kabul eder: {{inputs}}", + "model.tooltip.context.label": "Bağlam", + "model.tooltip.inputs": "Girdiler", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Sağlayıcı", + "model.tooltip.reasoning": "Akıl yürütme", + "model.tooltip.reasoning.allowed": "Akıl yürütmeyi destekler", + "model.tooltip.reasoning.none": "Akıl yürütme yok", + "model.tooltip.context": "Bağlam sınırı {{limit}}", + + "common.search.placeholder": "Ara", + "common.goBack": "Geri git", + "common.goForward": "İleri git", + "common.loading": "Yükleniyor", + "common.loading.ellipsis": "...", + "common.cancel": "İptal", + "common.clear": "Temizle", + "common.connect": "Bağlan", + "common.disconnect": "Bağlantıyı kes", + "common.continue": "Devam et", + "common.submit": "Gönder", + "common.save": "Kaydet", + "common.saving": "Kaydediliyor...", + "common.default": "Varsayılan", + "common.attachment": "ek", + + "prompt.placeholder.shell": "Kabuk komutu girin... {{example}}", + "prompt.placeholder.normal": 'Bir şeyler sorun... "{{example}}"', + "prompt.placeholder.simple": "Bir şeyler sorun...", + "prompt.placeholder.summarizeComments": "Yorumları özetle…", + "prompt.placeholder.summarizeComment": "Yorumu özetle…", + "prompt.mode.shell": "Kabuk", + "prompt.mode.normal": "İstem", + "prompt.mode.shell.exit": "çıkmak için esc", + "session.child.promptDisabled": "Alt ajan oturumlarına istem gönderilemez.", + "session.child.backToParent": "Ana oturuma dön.", + + "prompt.example.1": "Kod tabanındaki bir TODO'yu düzelt", + "prompt.example.2": "Bu projenin teknoloji yığını nedir?", + "prompt.example.3": "Bozuk testleri düzelt", + "prompt.example.4": "Kimlik doğrulamanın nasıl çalıştığını açıkla", + "prompt.example.5": "Güvenlik açıkları bul ve düzelt", + "prompt.example.6": "Kullanıcı servisi için birim testleri ekle", + "prompt.example.7": "Bu fonksiyonu daha okunabilir hale getir", + "prompt.example.8": "Bu hata ne anlama geliyor?", + "prompt.example.9": "Bu sorunu ayıklamama yardım et", + "prompt.example.10": "API dokümantasyonu oluştur", + "prompt.example.11": "Veritabanı sorgularını optimize et", + "prompt.example.12": "Girdi doğrulama ekle", + "prompt.example.13": "... için yeni bir bileşen oluştur", + "prompt.example.14": "Bu projeyi nasıl dağıtabilirim?", + "prompt.example.15": "Kodumu en iyi uygulamalar için incele", + "prompt.example.16": "Bu fonksiyona hata yönetimi ekle", + "prompt.example.17": "Bu regex kalıbını açıkla", + "prompt.example.18": "Bunu TypeScript'e dönüştür", + "prompt.example.19": "Kod tabanının tamamına günlük kaydı ekle", + "prompt.example.20": "Hangi bağımlılıklar güncellenmemiş?", + "prompt.example.21": "Bir geçiş betiği yazmama yardım et", + "prompt.example.22": "Bu uç nokta için önbellekleme uygula", + "prompt.example.23": "Bu listeye sayfalama ekle", + "prompt.example.24": "... için bir CLI komutu oluştur", + "prompt.example.25": "Ortam değişkenleri burada nasıl çalışıyor?", + + "prompt.popover.emptyResults": "Eşleşen sonuç yok", + "prompt.popover.emptyCommands": "Eşleşen komut yok", + "prompt.dropzone.label": "Resimleri, PDF'leri veya metin dosyalarını buraya bırakın", + "prompt.dropzone.file.label": "Dosyayı @ ile belirtmek için buraya bırakın", + "prompt.slash.badge.custom": "özel", + "prompt.slash.badge.skill": "beceri", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "etkin", + "prompt.context.includeActiveFile": "Etkin dosyayı dahil et", + "prompt.context.removeActiveFile": "Etkin dosyayı bağlamdan çıkar", + "prompt.context.removeFile": "Dosyayı bağlamdan çıkar", + "prompt.action.attachFile": "Dosya ekle", + "prompt.menu.addImagesAndFiles": "Dosya ve daha fazlasını ekle", + "prompt.menu.imagesAndFiles": "Görseller ve dosyalar", + "prompt.menu.commands": "Komutlar", + "prompt.menu.context": "Bağlam", + "prompt.menu.shellCommand": "Kabuk komutu", + "prompt.attachment.remove": "Eki kaldır", + "prompt.action.send": "Gönder", + "prompt.action.stop": "Durdur", + + "prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek", + "prompt.toast.attachmentDuplicate.title": "Bu dosya zaten yüklendi", + "prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.", + "prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin", + "prompt.toast.modelAgentRequired.description": "İstem göndermeden önce bir ajan ve model seçin.", + "prompt.toast.worktreeCreateFailed.title": "Çalışma ağacı oluşturulamadı", + "prompt.toast.sessionCreateFailed.title": "Oturum oluşturulamadı", + "prompt.toast.shellSendFailed.title": "Kabuk komutu gönderilemedi", + "prompt.toast.commandSendFailed.title": "Komut gönderilemedi", + "prompt.toast.promptSendFailed.title": "İstem gönderilemedi", + "prompt.toast.promptSendFailed.description": "Oturum alınamadı", + + "dialog.mcp.title": "MCP'ler", + "dialog.mcp.description": "Toplam {{total}} öğeden {{enabled}} tanesi etkin", + "dialog.mcp.empty": "Yapılandırılmış MCP yok", + + "dialog.lsp.empty": "LSP'ler dosya türlerinden otomatik algılanır", + "dialog.plugins.empty": "Eklentiler opencode.json içinde yapılandırılır", + + "mcp.status.connected": "bağlı", + "mcp.status.failed": "başarısız", + "mcp.status.needs_auth": "kimlik doğrulama gerekli", + "mcp.auth.clickToAuthenticate": "Kimlik doğrulamak için tıklayın", + "mcp.status.disabled": "devre dışı", + + "dialog.fork.empty": "Dallandırılacak mesaj yok", + + "dialog.directory.search.placeholder": "Klasör ara", + "dialog.directory.empty": "Klasör bulunamadı", + "dialog.directory.action.selectFile": "Dosya seç", + "dialog.directory.action.selectFolder": "Klasör seç", + "dialog.directory.root": "Kök", + "dialog.directory.parent": "Üst klasör", + "dialog.directory.readError": "Bu klasör okunamıyor", + + "dialog.server.title": "Sunucular", + "dialog.server.description": "Bu uygulamanın hangi OpenCode sunucusuna bağlanacağını değiştirin.", + "dialog.server.search.placeholder": "Sunucu ara", + "dialog.server.empty": "Henüz sunucu yok", + "dialog.server.add.title": "Sunucu ekle", + "dialog.server.add.url": "Sunucu URL'si", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Sunucuya bağlanılamadı", + "dialog.server.add.checking": "Kontrol ediliyor...", + "dialog.server.add.button": "Sunucu ekle", + "dialog.server.add.name": "Sunucu adı (isteğe bağlı)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Kullanıcı adı (isteğe bağlı)", + "dialog.server.add.password": "Parola (isteğe bağlı)", + "dialog.server.edit.title": "Sunucuyu düzenle", + "dialog.server.default.title": "Varsayılan sunucu", + "dialog.server.default.description": + "Uygulama başlatıldığında yerel sunucu başlatmak yerine bu sunucuya bağlan. Yeniden başlatma gerektirir.", + "dialog.server.default.none": "Sunucu seçilmedi", + "dialog.server.default.set": "Mevcut sunucuyu varsayılan olarak ayarla", + "dialog.server.default.clear": "Temizle", + "dialog.server.action.remove": "Sunucuyu kaldır", + + "dialog.server.menu.edit": "Düzenle", + "dialog.server.menu.default": "Varsayılan olarak ayarla", + "dialog.server.menu.defaultRemove": "Varsayılanı kaldır", + "dialog.server.menu.delete": "Sil", + "dialog.server.current": "Mevcut Sunucu", + "dialog.server.status.default": "Varsayılan", + "wsl.server.add": "WSL sunucusu ekle", + "wsl.server.addShort": "WSL ekle", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL sunucusu", + "wsl.server.retryStart": "Başlatmayı yeniden dene", + "wsl.server.updating": "Güncelleniyor...", + "wsl.onboarding.step.distro": "Dağıtım seç", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL kontrol ediliyor...", + "wsl.onboarding.restartRequired": "WSL kurulumunu tamamlamak için Windows'un yeniden başlatılması gerekiyor.", + "wsl.onboarding.ready": "WSL hazır.", + "wsl.onboarding.required": "Devam etmek için WSL gerekli.", + "wsl.onboarding.checkingDistros": "Dağıtımlar kontrol ediliyor...", + "wsl.onboarding.installingDistro": "{{distro}} kuruluyor...", + "wsl.onboarding.checkingDistro": "{{distro}} kontrol ediliyor...", + "wsl.onboarding.listingDistros": "Dağıtımlar listeleniyor...", + "wsl.onboarding.distroReady": "{{distro}} hazır.", + "wsl.onboarding.distroNotInstalled": "{{distro}} henüz kurulu değil.", + "wsl.onboarding.openDistroOnce": "Kurulumu tamamlamak için {{distro}} dağıtımını bir kez açın.", + "wsl.onboarding.finishingDistro": "{{distro}} kurulumu tamamlanıyor.", + "wsl.onboarding.pickDistro": "Bir dağıtım seçin veya aşağıdan bir dağıtım kurun.", + "wsl.onboarding.checkingOpencode": "OpenCode kontrol ediliyor...", + "wsl.onboarding.checkingOpencodeIn": "{{distro}} içinde OpenCode kontrol ediliyor...", + "wsl.onboarding.updatingOpencode": "OpenCode güncelleniyor...", + "wsl.onboarding.updatingOpencodeIn": "{{distro}} içinde OpenCode güncelleniyor...", + "wsl.onboarding.updateOpencodeIn": "{{distro}} içindeki OpenCode'u güncelleyin.", + "wsl.onboarding.updateOpencode": "OpenCode'u güncelle", + "wsl.onboarding.opencodeReadyIn": "{{distro}} içinde OpenCode hazır.", + "wsl.onboarding.opencodeReady": "OpenCode hazır.", + "wsl.onboarding.installOpencodeIn": "OpenCode'u {{distro}} içine kurun.", + "wsl.onboarding.installOpencode": "OpenCode'u kur", + "wsl.onboarding.chooseDistroFirst": "Önce bir dağıtım seçin.", + "wsl.onboarding.loadFailed": "WSL durumu yüklenemedi.", + "wsl.onboarding.loading": "Yükleniyor...", + "wsl.onboarding.installedDistros": "Kurulu dağıtımlar", + "wsl.onboarding.checkAgain": "Tekrar kontrol et", + "wsl.onboarding.distroStatus.ready": "Hazır", + "wsl.onboarding.distroStatus.checking": "Kontrol ediliyor...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode kurulu değil", + "wsl.onboarding.distroStatus.missingTools": "bash ve curl eksik", + "wsl.onboarding.distroStatus.unsupported": "Desteklenmiyor · WSL 2 kullanın", + "wsl.onboarding.needAnotherDistro": "Başka bir dağıtım mı gerekiyor?", + "wsl.onboarding.needAnotherDistroHint": "WSL kataloğundan bir Linux dağıtımı kurun", + "wsl.onboarding.wslNotInstalled.title": "WSL kurulu değil", + "wsl.onboarding.wslNotInstalled.description": + "OpenCode'un WSL sunucusu ekleyebilmesi için önce WSL (Linux için Windows Alt Sistemi) kurulmalıdır", + "wsl.onboarding.wslUnavailable.title": "WSL kullanılamıyor", + "wsl.onboarding.wslUnavailable.description": "OpenCode bu makinede WSL'yi doğrulayamadı.", + "wsl.onboarding.installWsl": "WSL'yi kur", + "wsl.onboarding.windowsRestartRequired": + "WSL kurulumunu tamamlamak için Windows'u yeniden başlatın, ardından OpenCode'u tekrar açın.", + "wsl.onboarding.next": "İleri", + "wsl.onboarding.refresh": "Yenile", + "wsl.onboarding.allDistrosAdded": "Kurulu tüm dağıtımlar zaten eklenmiş.", + "wsl.onboarding.noDistros": "Henüz dağıtım algılanmadı.", + "wsl.onboarding.install": "Kur", + "wsl.onboarding.installing": "Kuruluyor...", + "wsl.onboarding.installDistro": "Dağıtım kur", + "wsl.onboarding.searchDistros": "Dağıtım ara", + "wsl.onboarding.wsl2Required": "WSL 2 gerekli.", + "wsl.onboarding.toolsRequired": "Bu dağıtım için bash ve curl gerekli.", + "wsl.onboarding.openTerminal": "Terminali aç", + "wsl.onboarding.path": "Yol: {{path}}", + "wsl.onboarding.notFound": "bulunamadı", + "wsl.onboarding.version": "Sürüm: {{version}}", + "wsl.onboarding.unknown": "bilinmiyor", + "wsl.onboarding.desktopVersion": "masaüstü {{version}}", + "wsl.onboarding.versionMismatch": "Kurulu sürüm masaüstü uygulamasının sürümüyle eşleşmiyor.", + "wsl.onboarding.adding": "Ekleniyor...", + + "help.tabs.toast.ariaLabel": "Sekmelerle tanışın. Çalışmalarınızı ve etkin oturumlarınızı sekmelerle düzenleyin", + "help.tabs.toast.dismiss": "Sekmeler bilgilerini kapat", + "help.tabs.title": "Sekmelerle tanışın", + "help.tabs.description": "Çalışmalarınızı ve etkin oturumlarınızı sekmelerle düzenleyin", + "help.tabs.date": "14 Temmuz", + "help.tabs.introduction": "OpenCode Desktop artık sekmeler üzerine kurulu.", + "help.tabs.sessions": + "Bir sekmede yeni oturum başlatın veya projelerinizden mevcut bir oturumu açın. Yeni bir işe başlarken yeni sekme açın, işiniz bittiğinde sekmeyi kapatın.", + "help.tabs.organize": + "Birkaç sekmeyi açık tutmak, etkin oturumlarınızı düzenlemenizi kolaylaştırır. Sekmeleri açık tutmayı planlıyorsanız onlara akılda kalıcı adlar verin.", + "help.tabs.home": + "Tüm oturumlarınızı ve projelerinizi yeni Ana Sayfa ekranında bulabilirsiniz. Bir oturum seçtiğinizde oturum bir sekmede açılır.", + "help.tabs.persistence": "Uygulamayı yeniden açtığınızda sekmeleriniz açık kalır.", + "help.tabs.worktrees": + "Yeni tasarım henüz Git Worktrees'i desteklemiyor; bu özellik yakında kullanıma sunulacak. Önceki düzeni kullanmaya devam etmek isterseniz Ayarlar'da düzenler arasında geçiş yapabilirsiniz. Yeni düzenin birkaç hafta içinde kalıcı olacağını unutmayın.", + + "dialog.project.edit.title": "Projeyi düzenle", + "dialog.project.edit.name": "Ad", + "dialog.project.edit.icon": "Simge", + "dialog.project.edit.icon.alt": "Proje simgesi", + "dialog.project.edit.icon.hint": "Tıkla veya bir görsel sürükle", + "dialog.project.edit.icon.recommended": "Önerilen: 128x128px", + "dialog.project.edit.color": "Renk", + "dialog.project.edit.color.select": "{{color}} rengini seç", + "dialog.project.edit.worktree.startup": "Çalışma alanı başlatma betiği", + "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", + "context.breakdown.user": "Kullanıcı", + "context.breakdown.assistant": "Asistan", + "context.breakdown.tool": "Araç Çağrıları", + "context.breakdown.other": "Diğer", + + "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ı", + "context.stats.model": "Model", + "context.stats.limit": "Bağlam Sınırı", + "context.stats.totalTokens": "Toplam Token", + "context.stats.usage": "Kullanım", + "context.stats.inputTokens": "Girdi Tokenleri", + "context.stats.outputTokens": "Çıktı Tokenleri", + "context.stats.reasoningTokens": "Akıl Yürütme Tokenleri", + "context.stats.cacheTokens": "Önbellek Tokenleri (okuma/yazma)", + "context.stats.userMessages": "Kullanıcı Mesajları", + "context.stats.assistantMessages": "Asistan Mesajları", + "context.stats.totalCost": "Toplam Maliyet", + "context.stats.sessionCreated": "Oturum Oluşturma", + "context.stats.lastActivity": "Son Etkinlik", + + "context.usage.tokens": "Tokenler", + "context.usage.usage": "Kullanım", + "context.usage.cost": "Maliyet", + "context.usage.clickToView": "Bağlamı görüntüle", + "context.usage.view": "Bağlam kullanımını görüntüle", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "Dil", + "toast.language.description": "{{language}} diline geçildi", + + "toast.theme.title": "Tema değiştirildi", + "toast.scheme.title": "Renk şeması", + + "toast.workspace.enabled.title": "Çalışma alanları etkinleştirildi", + "toast.workspace.enabled.description": "Kenar çubuğunda birden fazla çalışma ağacı gösterilecek", + "toast.workspace.disabled.title": "Çalışma alanları devre dışı bırakıldı", + "toast.workspace.disabled.description": "Kenar çubuğunda yalnızca ana çalışma ağacı gösterilecek", + + "toast.permissions.autoaccept.on.title": "İzinler otomatik kabul ediliyor", + "toast.permissions.autoaccept.on.description": "İzin istekleri otomatik olarak onaylanacak", + "toast.permissions.autoaccept.off.title": "İzinleri otomatik kabul etme durduruldu", + "toast.permissions.autoaccept.off.description": "İzin istekleri onay gerektirecek", + + "toast.model.none.title": "Model seçilmedi", + "toast.model.none.description": "Bu oturumu özetlemek için bir sağlayıcı bağlayın", + + "toast.file.loadFailed.title": "Dosya yüklenemedi", + "toast.file.listFailed.title": "Dosyalar listelenemedi", + + "toast.context.noLineSelection.title": "Satır seçimi yok", + "toast.context.noLineSelection.description": "Önce bir dosya sekmesinde satır aralığı seçin.", + + "toast.session.share.copyFailed.title": "URL panoya kopyalanamadı", + "toast.session.share.success.title": "Oturum paylaşıldı", + "toast.session.share.success.description": "Paylaşım URL'si panoya kopyalandı!", + "toast.session.share.failed.title": "Oturum paylaşılamadı", + "toast.session.share.failed.description": "Oturum paylaşılırken bir hata oluştu", + + "toast.session.unshare.success.title": "Oturum paylaşımı kaldırıldı", + "toast.session.unshare.success.description": "Oturum paylaşımı başarıyla kaldırıldı!", + "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", + "toast.update.description": "OpenCode'un yeni bir sürümü ({{version}}) yüklemeye hazır.", + "toast.update.action.installRestart": "Yükle ve yeniden başlat", + "toast.update.action.notYet": "Şimdi değil", + + "error.page.title": "Bir şeyler yanlış gitti", + "error.page.description": "Uygulama yüklenirken bir hata oluştu.", + "error.page.description.localServerStartup": "Yerel sunucu başlatılırken bir hata oluştu.", + "error.page.details.label": "Hata Detayları", + "error.page.action.restart": "Yeniden Başlat", + "error.page.action.report": "Hatayı Bildir", + "error.page.action.reported": "Hata Bildirildi", + "error.page.action.exportLogs": "Günlükleri Dışa Aktar", + "error.page.action.checking": "Kontrol ediliyor...", + "error.page.action.checkUpdates": "Güncellemeleri kontrol et", + "error.page.action.updateTo": "{{version}} sürümüne güncelle", + "error.page.report.prefix": "Lütfen bu hatayı OpenCode ekibine", + "error.page.report.discord": "Discord üzerinden bildirin", + "error.page.version": "Sürüm: {{version}}", + + "error.dev.rootNotFound": + "Kök eleman bulunamadı. index.html dosyanıza eklemeyi unuttunuz mu? Ya da id özelliği yanlış mı yazıldı?", + + "error.serverSync.connectFailed": "Sunucuya bağlanılamadı. `{{url}}` adresinde çalışan bir sunucu var mı?", + "directory.error.invalidUrl": "URL'de geçersiz dizin.", + + "error.chain.unknown": "Bilinmeyen hata", + "error.chain.causedBy": "Nedeni:", + "error.chain.apiError": "API hatası", + "error.chain.status": "Durum: {{status}}", + "error.chain.retryable": "Yeniden denenebilir: {{retryable}}", + "error.chain.responseBody": "Yanıt gövdesi:\n{{body}}", + "error.chain.didYouMean": "Bunu mu demek istediniz: {{suggestions}}", + "error.chain.modelNotFound": "Model bulunamadı: {{provider}}/{{model}}", + "error.chain.checkConfig": "Yapılandırma dosyanızdaki (opencode.json) sağlayıcı/model adlarını kontrol edin", + "error.chain.mcpFailed": + 'MCP sunucusu "{{name}}" çalıştırılamadı. Not: OpenCode henüz MCP kimlik doğrulamasını desteklemiyor.', + "error.chain.providerAuthFailed": "Sağlayıcı kimlik doğrulaması başarısız ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + '"{{provider}}" sağlayıcısı başlatılamadı. Kimlik bilgilerini ve yapılandırmayı kontrol edin.', + "error.chain.configJsonInvalid": "{{path}} adresindeki yapılandırma dosyası geçerli JSON(C) değil", + "error.chain.configJsonInvalidWithMessage": + "{{path}} adresindeki yapılandırma dosyası geçerli JSON(C) değil: {{message}}", + "error.chain.configDirectoryTypo": + '"{{dir}}" dizini {{path}} içinde geçerli değil. Dizini "{{suggestion}}" olarak yeniden adlandırın veya kaldırın. Bu yaygın bir yazım hatasıdır.', + "error.chain.configFrontmatterError": "{{path}} içindeki ön bilgi ayrıştırılamadı:\n{{message}}", + "error.chain.configInvalid": "{{path}} adresindeki yapılandırma dosyası geçersiz", + "error.chain.configInvalidWithMessage": "{{path}} adresindeki yapılandırma dosyası geçersiz: {{message}}", + + "notification.permission.title": "İzin gerekli", + "notification.permission.description": "{{projectName}} içindeki {{sessionTitle}} izin gerektiriyor", + "notification.question.title": "Soru", + "notification.question.description": "{{projectName}} içindeki {{sessionTitle}} bir soru soruyor", + "notification.action.goToSession": "Oturuma git", + + "notification.session.responseReady.title": "Yanıt hazır", + "notification.session.error.title": "Oturum hatası", + "notification.session.error.fallbackDescription": "Bir hata oluştu", + + "home.recentProjects": "Son projeler", + "home.empty.title": "Son proje yok", + "home.empty.description": "Yerel bir proje açarak başlayın", + "home.title": "Ana Sayfa", + "home.projects": "Projeler", + "home.project.add": "Proje ekle", + "home.recentlyClosed": "Yakın zamanda kapatılanlar", + "home.server.collapse": "Sunucu projelerini daralt", + "home.server.expand": "Sunucu projelerini genişlet", + "home.sessions.search.placeholder": "Oturum ara", + "home.sessions.search.placeholder.scoped": "{{scope}} kapsamında oturum ara", + "home.sessions.search.sessions": "Oturumlar", + "home.sessions.search.noResults": "{{query}} için oturum bulunamadı", + "home.sessions.empty": "Henüz burada bir şey yok", + "home.sessions.empty.description": "Başlamak için bir oturum oluşturun", + "home.sessions.group.today": "Bugün", + "home.sessions.group.yesterday": "Dün", + "home.sessions.group.older": "Daha eski", + "home.providerTip": "Claude, GPT, Gemini ve diğer modelleri kullanmak için 75'ten fazla sağlayıcıya bağlanın", + + "session.tab.session": "Oturum", + "session.tab.review": "İnceleme", + "session.tab.context": "Bağlam", + "session.tab.unknown": "Bilinmeyen Oturum", + "session.panel.reviewAndFiles": "İnceleme ve dosyalar", + "session.error.notFound": "Bu oturum bulunamıyor", + "session.error.notFound.description": "Bu sekme, artık bu sunucuda bulunmayan bir oturuma yönlendiriyor.", + "session.error.notFound.closeTab": "Sekmeyi kapat", + "session.error.serverConnection": "Bu sunucuya bağlanılamıyor", + "session.review.filesChanged": "{{count}} dosya değişti", + "session.review.change.one": "Değişiklik", + "session.review.change.other": "Değişiklik", + "session.review.loadingChanges": "Değişiklikler yükleniyor...", + "session.review.empty": "Bu oturumda henüz değişiklik yok", + "session.review.noVcs": "Git VCS algılanamadı, oturum değişiklikleri tespit edilemeyecek", + "session.review.noSnapshot": + "Yapılandırmada anlık görüntü takibi devre dışı bırakıldı, bu nedenle oturum değişiklikleri kullanılamıyor", + "session.review.noChanges": "Değişiklik yok", + "session.review.noUncommittedChanges": "Henüz işlenmemiş değişiklik yok", + "session.review.noBranchChanges": "Henüz dal değişikliği yok", + + "session.files.selectToOpen": "Açmak için bir dosya seçin", + "session.files.all": "Tüm dosyalar", + "session.files.empty": "Dosya yok", + "session.files.binaryContent": "İkili dosya (içerik görüntülenemiyor)", + + "session.messages.renderEarlier": "Önceki mesajları göster", + "session.messages.loadingEarlier": "Önceki mesajlar yükleniyor...", + "session.messages.loadEarlier": "Önceki mesajları yükle", + "session.messages.loading": "Mesajlar yükleniyor...", + "session.messages.jumpToLatest": "En sona atla", + + "session.context.addToContext": "{{selection}} bağlama ekle", + "session.todo.title": "Görevler", + "session.todo.collapse": "Daralt", + "session.todo.expand": "Genişlet", + "session.question.minimize": "Soruyu küçült", + "session.question.restore": "Soruyu geri yükle", + "session.question.pending.one": "{{count}} bekleyen soru", + "session.question.pending.other": "{{count}} bekleyen soru", + "session.followupDock.summary.one": "{{count}} sıradaki mesaj", + "session.followupDock.summary.other": "{{count}} sıradaki mesaj", + "session.followupDock.sendNow": "Şimdi gönder", + "session.followupDock.edit": "Düzenle", + "session.followupDock.collapse": "Sıradaki mesajları daralt", + "session.followupDock.expand": "Sıradaki mesajları genişlet", + "session.revertDock.summary.one": "{{count}} geri alınan mesaj", + "session.revertDock.summary.other": "{{count}} geri alınan mesaj", + "session.revertDock.collapse": "Geri alınan mesajları daralt", + "session.revertDock.expand": "Geri alınan mesajları genişlet", + "session.revertDock.restore": "Mesajı geri yükle", + + "session.new.title": "Her şeyi geliştirin", + "session.new.project.new": "Yeni proje", + "session.new.project.search": "Proje ara", + "session.new.project.add": "Proje ekle", + "session.new.worktree.main": "Ana dal", + "session.new.worktree.mainWithBranch": "Ana dal ({{branch}})", + "session.new.worktree.create": "Yeni çalışma ağacı oluştur", + "session.new.workspace.runIn": "Oturumu şurada çalıştır", + "session.new.workspace.triggerLocal": "Yerel", + "session.new.workspace.local": "Yerel depo", + "session.new.workspace.existing": "Çalışma alanı…", + "session.new.git.none": "Git yok", + "session.new.lastModified": "Son değişiklik", + + "session.header.search.placeholder": "{{project}} ara", + "session.header.searchFiles": "Dosya ara", + "session.header.openIn": "Aç", + "session.header.open.action": "{{app}} ile aç", + "session.header.open.ariaLabel": "{{app}} ile aç", + "session.header.open.menu": "Açma seçenekleri", + "session.header.open.copyPath": "Yolu kopyala", + "session.header.reveal.finder": "Finder'da göster", + "session.header.reveal.fileExplorer": "Dosya Gezgini'nde göster", + "session.header.reveal.containingFolder": "İçeren klasörü aç", + + "status.popover.trigger": "Durum", + "status.popover.ariaLabel": "Sunucu yapılandırmaları", + "status.popover.tab.servers": "Sunucular", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Eklentiler", + "status.popover.action.manageServers": "Sunucuları yönet", + + "session.share.popover.title": "Web'de yayınla", + "session.share.popover.description.shared": "Bu oturum web'de herkese açıktır. Bağlantıya sahip herkes erişebilir.", + "session.share.popover.description.unshared": + "Oturumu web'de herkese açık olarak paylaşın. Bağlantıya sahip herkes erişebilecek.", + "session.share.action.share": "Paylaş", + "session.share.action.publish": "Yayınla", + "session.share.action.publishing": "Yayınlanıyor...", + "session.share.action.unpublish": "Yayından kaldır", + "session.share.action.unpublishing": "Yayından kaldırılıyor...", + "session.share.action.view": "Görüntüle", + "session.share.copy.copied": "Kopyalandı", + "session.share.copy.copyLink": "Bağlantıyı kopyala", + + "lsp.tooltip.none": "LSP sunucusu yok", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "İstem yükleniyor...", + "terminal.loading": "Terminal yükleniyor...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Terminali kapat", + "terminal.connectionLost.title": "Bağlantı kesildi", + "terminal.connectionLost.description": + "Terminal bağlantısı kesildi. Bu durum sunucu yeniden başladığında oluşabilir.", + "terminal.connectTicket.csrfError": + "PTY bağlantı bileti, kaynak veya CSRF denetimleri tarafından reddedildi. Sunucunun CORS yapılandırmasını kontrol edin.", + "terminal.connectTicket.statusError": "PTY bağlantı bileti {{status}} durumuyla başarısız oldu", + + "titlebar.update": "Güncelle", + "titlebar.updateVersion": "Güncelle {{version}}", + + "common.closeTab": "Sekmeyi kapat", + "common.dismiss": "Kapat", + "common.requestFailed": "İstek başarısız", + "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", + "common.close": "Kapat", + "common.edit": "Düzenle", + "common.loadMore": "Daha fazla yükle", + "common.key.esc": "ESC", + + "sidebar.menu.toggle": "Menüyü aç/kapat", + "sidebar.nav.projectsAndSessions": "Projeler ve oturumlar", + "sidebar.settings": "Ayarlar", + "sidebar.help": "Yardım", + "sidebar.workspaces.enable": "Çalışma alanlarını etkinleştir", + "sidebar.workspaces.disable": "Çalışma alanlarını devre dışı bırak", + "sidebar.gettingStarted.title": "Başlarken", + "sidebar.gettingStarted.line1": "OpenCode ücretsiz modeller içerir, böylece hemen başlayabilirsiniz.", + "sidebar.gettingStarted.line2": "Claude, GPT, Gemini vb. modelleri kullanmak için herhangi bir sağlayıcı bağlayın.", + "sidebar.project.recentSessions": "Son oturumlar", + "sidebar.project.viewAllSessions": "Tüm oturumları görüntüle", + "sidebar.project.clearNotifications": "Bildirimleri temizle", + "sidebar.empty.title": "Açık proje yok", + "sidebar.empty.description": "Başlamak için bir proje açın", + + "app.name.desktop": "OpenCode Masaüstü", + + "settings.section.desktop": "Masaüstü", + "settings.section.server": "Sunucu", + "settings.tab.general": "Genel", + "settings.tab.shortcuts": "Kısayollar", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL entegrasyonu", + "settings.desktop.wsl.description": "OpenCode sunucusunu Windows'ta WSL içinde çalıştırın.", + + "settings.general.section.appearance": "Görünüm", + "settings.general.section.advanced": "Gelişmiş", + "settings.general.section.notifications": "Sistem bildirimleri", + "settings.general.section.updates": "Güncellemeler", + "settings.general.section.sounds": "Ses efektleri", + "settings.general.section.feed": "Akış", + "settings.general.section.display": "Ekran", + + "settings.general.row.language.title": "Dil", + "settings.general.row.language.description": "OpenCode'un görünüm dilini değiştirin", + "settings.general.row.shell.title": "Terminal Kabuğu", + "settings.general.row.shell.description": + "Terminalinizde kullanılacak kabuğu seçin. Uyumlu kabuklar, ajan araç çağrılarında da kullanılır.", + "settings.general.row.shell.autoDefault": "Otomatik (Varsayılan)", + "settings.general.row.shell.terminalOnly": "yalnızca terminal", + "settings.general.row.appearance.title": "Görünüm", + "settings.general.row.appearance.description": "OpenCode'un cihazınızdaki görünümünü özelleştirin", + "settings.general.row.colorScheme.title": "Renk şeması", + "settings.general.row.colorScheme.description": + "OpenCode'un sistem, açık veya koyu temayı takip etip etmeyeceğini seçin", + "settings.general.row.theme.title": "Tema", + "settings.general.row.theme.description": "OpenCode'un temasını özelleştirin.", + "settings.general.row.font.title": "Kod yazı tipi", + "settings.general.row.font.description": "Kod bloklarında kullanılan yazı tipini özelleştirin", + "settings.general.row.terminalFont.title": "Terminal yazı tipi", + "settings.general.row.terminalFont.description": "Terminalde kullanılan yazı tipini özelleştirin", + "settings.general.row.uiFont.title": "Arayüz yazı tipi", + "settings.general.row.uiFont.description": "Arayüz genelinde kullanılan yazı tipini özelleştirin", + "settings.general.row.followup.title": "Takip davranışı", + "settings.general.row.followup.description": + "Takip istemlerinin hemen yönlendirilmesini mi yoksa sırada beklemesini mi istediğinizi seçin", + "settings.general.row.followup.option.queue": "Sıra", + "settings.general.row.followup.option.steer": "Yönlendir", + "settings.general.row.showFileTree.title": "Dosya ağacı", + "settings.general.row.showFileTree.description": "Oturumlarda dosya ağacı panelini göster", + "settings.general.row.showNavigation.title": "Gezinme denetimleri", + "settings.general.row.showNavigation.description": "Masaüstü başlık çubuğunda geri ve ileri düğmelerini göster", + "settings.general.row.showSearch.title": "Komut paleti", + "settings.general.row.showSearch.description": "Başlık çubuğunda arama ve komut paleti düğmesini göster", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Masaüstü başlık çubuğunda terminal düğmesini göster", + "settings.general.row.showStatus.title": "Sunucu durumu", + "settings.general.row.showStatus.description": "Başlık çubuğunda sunucu durumu düğmesini göster", + "settings.general.row.mobileTitlebarBottom.title": "Alt gezinme", + "settings.general.row.mobileTitlebarBottom.description": + "Mobil cihazlarda başlık çubuğunu ve oturum sekmelerini ekranın altına yerleştir", + "settings.general.row.showCustomAgents.title": "Ajanı göster", + "settings.general.row.showCustomAgents.description": + "Düzenleyicide ajanlar arasında geçiş yapın. Gizlendiğinde varsayılan olarak Build ajanı kullanılır.", + "settings.general.row.reasoningSummaries.title": "Akıl yürütme özetlerini göster", + "settings.general.row.reasoningSummaries.description": "Zaman çizelgesinde model akıl yürütme özetlerini görüntüle", + "settings.general.row.shellToolPartsExpanded.title": "Kabuk araç bileşenlerini genişlet", + "settings.general.row.shellToolPartsExpanded.description": + "Zaman çizelgesinde kabuk araç bileşenlerini varsayılan olarak genişletilmiş göster", + "settings.general.row.editToolPartsExpanded.title": "Düzenleme araç bileşenlerini genişlet", + "settings.general.row.editToolPartsExpanded.description": + "Zaman çizelgesinde düzenleme, yazma ve yama araç bileşenlerini varsayılan olarak genişletilmiş göster", + "settings.general.row.newInterface.title": "Yeni düzen", + "settings.general.row.newInterface.badge": "Yeni", + "settings.general.row.newInterface.description": + "Yeni sekmeleri ve ana sayfa düzenini kullanın. Sınırlı bir süre boyunca düzenler arasında geçiş yapabilirsiniz.", + "settings.general.row.newInterfaceNotice.title": "Artık yeni düzeni kullanıyorsunuz", + "settings.general.row.newInterfaceNotice.description": "Önceki düzen artık kullanılamıyor", + "settings.general.row.newInterfaceNotice.dismiss": "Kapat", + "settings.general.row.pinchZoom.title": "Sıkıştırarak yakınlaştır", + "settings.general.row.pinchZoom.description": + "İzleme dörtgeninde sıkıştırma ve Ctrl-kaydırma hareketleriyle yakınlaştırmaya izin ver", + + "settings.general.row.wayland.title": "Yerel Wayland kullan", + "settings.general.row.wayland.description": + "Wayland'da X11 geri dönüşünü devre dışı bırak. Yeniden başlatma gerektirir.", + "settings.general.row.wayland.tooltip": + "Farklı yenileme hızlarına sahip monitörlerin kullanıldığı Linux sistemlerinde yerel Wayland daha kararlı olabilir.", + + "settings.general.row.releaseNotes.title": "Sürüm notları", + "settings.general.row.releaseNotes.description": "Güncellemelerden sonra Yenilikler bildirimlerini göster", + + "settings.updates.row.startup.title": "Başlangıçta güncellemeleri kontrol et", + "settings.updates.row.startup.description": "OpenCode başladığında otomatik güncelleme kontrolü yap", + "settings.updates.row.check.title": "Güncellemeleri kontrol et", + "settings.updates.row.check.description": "Elle güncelleme kontrolü yap ve varsa yükle", + "settings.updates.action.checkNow": "Şimdi kontrol et", + "settings.updates.action.checking": "Kontrol ediliyor...", + "settings.updates.action.downloading": "İndiriliyor...", + "settings.updates.action.installing": "Kuruluyor...", + "settings.updates.toast.latest.title": "Güncelsiniz", + "settings.updates.toast.latest.description": "OpenCode'un en son sürümünü kullanıyorsunuz.", + + "sound.option.none": "Yok", + "sound.option.alert01": "Uyarı 01", + "sound.option.alert02": "Uyarı 02", + "sound.option.alert03": "Uyarı 03", + "sound.option.alert04": "Uyarı 04", + "sound.option.alert05": "Uyarı 05", + "sound.option.alert06": "Uyarı 06", + "sound.option.alert07": "Uyarı 07", + "sound.option.alert08": "Uyarı 08", + "sound.option.alert09": "Uyarı 09", + "sound.option.alert10": "Uyarı 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Hayır 01", + "sound.option.nope02": "Hayır 02", + "sound.option.nope03": "Hayır 03", + "sound.option.nope04": "Hayır 04", + "sound.option.nope05": "Hayır 05", + "sound.option.nope06": "Hayır 06", + "sound.option.nope07": "Hayır 07", + "sound.option.nope08": "Hayır 08", + "sound.option.nope09": "Hayır 09", + "sound.option.nope10": "Hayır 10", + "sound.option.nope11": "Hayır 11", + "sound.option.nope12": "Hayır 12", + "sound.option.yup01": "Evet 01", + "sound.option.yup02": "Evet 02", + "sound.option.yup03": "Evet 03", + "sound.option.yup04": "Evet 04", + "sound.option.yup05": "Evet 05", + "sound.option.yup06": "Evet 06", + + "settings.general.notifications.agent.title": "Ajan", + "settings.general.notifications.agent.description": + "Ajan tamamlandığında veya dikkat gerektirdiğinde sistem bildirimi göster", + "settings.general.notifications.permissions.title": "İzinler", + "settings.general.notifications.permissions.description": "İzin gerektiğinde sistem bildirimi göster", + "settings.general.notifications.errors.title": "Hatalar", + "settings.general.notifications.errors.description": "Hata oluştuğunda sistem bildirimi göster", + + "settings.general.sounds.agent.title": "Ajan", + "settings.general.sounds.agent.description": "Ajan tamamlandığında veya dikkat gerektirdiğinde ses çal", + "settings.general.sounds.permissions.title": "İzinler", + "settings.general.sounds.permissions.description": "İzin gerektiğinde ses çal", + "settings.general.sounds.errors.title": "Hatalar", + "settings.general.sounds.errors.description": "Hata oluştuğunda ses çal", + + "settings.shortcuts.title": "Klavye kısayolları", + "settings.shortcuts.reset.button": "Varsayılanlara sıfırla", + "settings.shortcuts.reset.toast.title": "Kısayollar sıfırlandı", + "settings.shortcuts.reset.toast.description": "Klavye kısayolları varsayılanlara sıfırlandı.", + "settings.shortcuts.conflict.title": "Kısayol zaten kullanılıyor", + "settings.shortcuts.conflict.description": "{{keybind}} zaten {{titles}} için atanmış.", + "settings.shortcuts.unassigned": "Atanmamış", + "settings.shortcuts.pressKeys": "Tuşlara basın", + "settings.shortcuts.search.placeholder": "Kısayol ara", + "settings.shortcuts.search.empty": "Kısayol bulunamadı", + + "settings.shortcuts.group.general": "Genel", + "settings.shortcuts.group.session": "Oturum", + "settings.shortcuts.group.navigation": "Gezinme", + "settings.shortcuts.group.modelAndAgent": "Model ve ajan", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "İstem", + + "settings.providers.title": "Sağlayıcılar", + "settings.providers.description": "Sağlayıcı ayarları burada yapılandırılabilecek.", + "settings.providers.section.connected": "Bağlı sağlayıcılar", + "settings.providers.connected.empty": "Bağlı sağlayıcı yok", + "settings.providers.section.popular": "Popüler sağlayıcılar", + "settings.providers.tag.environment": "Ortam", + "settings.providers.tag.config": "Yapılandırma", + "settings.providers.tag.custom": "Özel", + "settings.providers.tag.other": "Diğer", + "settings.models.title": "Modeller", + "settings.models.description": "Model ayarları burada yapılandırılabilecek.", + "settings.agents.title": "Ajanlar", + "settings.agents.description": "Ajan ayarları burada yapılandırılabilecek.", + "settings.commands.title": "Komutlar", + "settings.commands.description": "Komut ayarları burada yapılandırılabilecek.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP ayarları burada yapılandırılabilecek.", + + "settings.permissions.title": "İzinler", + "settings.permissions.description": "Sunucunun varsayılan olarak hangi araçları kullanabileceğini kontrol edin.", + "settings.permissions.section.tools": "Araçlar", + "settings.permissions.toast.updateFailed.title": "İzinler güncellenemedi", + + "settings.permissions.action.allow": "İzin ver", + "settings.permissions.action.ask": "Sor", + "settings.permissions.action.deny": "Reddet", + + "settings.permissions.tool.read.title": "Oku", + "settings.permissions.tool.read.description": "Bir dosyayı okuma (dosya yoluyla eşleşir)", + "settings.permissions.tool.edit.title": "Düzenle", + "settings.permissions.tool.edit.description": "Düzenleme, yazma, yama ve çoklu düzenleme dahil dosyaları değiştir", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Glob kalıpları kullanarak dosyaları eşle", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Düzenli ifadeler kullanarak dosya içerikleri ara", + "settings.permissions.tool.list.title": "Listele", + "settings.permissions.tool.list.description": "Bir dizindeki dosyaları listele", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Kabuk komutları çalıştır", + "settings.permissions.tool.task.title": "Görev", + "settings.permissions.tool.task.description": "Alt ajanlar başlat", + "settings.permissions.tool.skill.title": "Beceri", + "settings.permissions.tool.skill.description": "Ada göre bir beceri yükle", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Dil sunucusu sorguları çalıştır", + "settings.permissions.tool.todowrite.title": "Görev Yaz", + "settings.permissions.tool.todowrite.description": "Görev listesini güncelle", + "settings.permissions.tool.webfetch.title": "Web Getir", + "settings.permissions.tool.webfetch.description": "Bir URL'den içerik getir", + "settings.permissions.tool.websearch.title": "Web Ara", + "settings.permissions.tool.websearch.description": "Web'de ara", + "settings.permissions.tool.external_directory.title": "Harici Dizin", + "settings.permissions.tool.external_directory.description": "Proje dizini dışındaki dosyalara eriş", + "settings.permissions.tool.doom_loop.title": "Sonsuz Döngü", + "settings.permissions.tool.doom_loop.description": "Aynı girdiyle tekrarlanan araç çağrılarını algıla", + + "session.delete.failed.title": "Oturum silinemedi", + "session.delete.title": "Oturumu sil", + "session.delete.confirm": '"{{name}}" oturumu silinsin mi?', + "session.delete.button": "Oturumu sil", + + "workspace.new": "Yeni çalışma alanı", + "workspace.type.local": "yerel", + "workspace.type.sandbox": "korumalı alan", + "workspace.create.failed.title": "Çalışma alanı oluşturulamadı", + "workspace.delete.failed.title": "Çalışma alanı silinemedi", + "workspace.resetting.title": "Çalışma alanı sıfırlanıyor", + "workspace.resetting.description": "Bu bir dakika sürebilir.", + "workspace.reset.failed.title": "Çalışma alanı sıfırlanamadı", + "workspace.reset.success.title": "Çalışma alanı sıfırlandı", + "workspace.reset.success.description": "Çalışma alanı artık varsayılan dalla eşleşiyor.", + "workspace.error.stillPreparing": "Çalışma alanı hâlâ hazırlanıyor", + "workspace.status.checking": "Birleşmemiş değişiklikler kontrol ediliyor...", + "workspace.status.error": "Git durumu doğrulanamadı.", + "workspace.status.clean": "Birleşmemiş değişiklik algılanmadı.", + "workspace.status.dirty": "Bu çalışma alanında birleşmemiş değişiklikler algılandı.", + "workspace.delete.title": "Çalışma alanını sil", + "workspace.delete.confirm": '"{{name}}" çalışma alanı silinsin mi?', + "workspace.delete.button": "Çalışma alanını sil", + "workspace.reset.title": "Çalışma alanını sıfırla", + "workspace.reset.confirm": '"{{name}}" çalışma alanı sıfırlansın mı?', + "workspace.reset.button": "Çalışma alanını sıfırla", + "workspace.reset.archived.none": "Arşivlenecek aktif oturum yok.", + "workspace.reset.archived.one": "1 oturum arşivlenecek.", + "workspace.reset.archived.many": "{{count}} oturum arşivlenecek.", + "workspace.reset.note": "Bu işlem çalışma alanını varsayılan dalla eşleşecek şekilde sıfırlayacak.", + "common.open": "Aç", + "dialog.releaseNotes.action.getStarted": "Başla", + "dialog.releaseNotes.action.next": "İleri", + "dialog.releaseNotes.action.hideFuture": "Bunu gelecekte bir daha gösterme", + "dialog.releaseNotes.media.alt": "Sürüm önizlemesi", + "toast.project.reloadFailed.title": "{{project}} yeniden yüklenemedi", + "error.server.invalidConfiguration": "Geçersiz yapılandırma", + "common.moreCountSuffix": " (+{{count}} daha)", + "common.time.justNow": "Şimdi", + "common.time.minutesAgo.short": "{{count}} dk önce", + "common.time.hoursAgo.short": "{{count}} sa önce", + "common.time.daysAgo.short": "{{count}} gün önce", + "settings.providers.connected.environmentDescription": "Ortam değişkenlerinizden bağlandı", + "settings.providers.custom.description": "Temel URL üzerinden OpenAI uyumlu bir sağlayıcı ekleyin.", + + "app.server.unreachable": "{{server}} sunucusuna ulaşılamadı", + "app.server.retrying": "Otomatik olarak tekrar deneniyor...", + "app.server.otherServers": "Diğer sunucular", + "dialog.server.add.usernamePlaceholder": "kullanıcı adı", + "dialog.server.add.passwordPlaceholder": "parola", + "server.row.noUsername": "kullanıcı adı yok", + "session.review.noVcs.createGit.title": "Git deposu oluştur", + "session.review.noVcs.createGit.description": "Bu projedeki değişiklikleri takip et, incele ve geri al", + "session.review.noVcs.createGit.actionLoading": "Git deposu oluşturuluyor...", + "session.review.noVcs.createGit.action": "Git deposu oluştur", + "session.todo.progress": "{{total}} görevin {{done}} tanesi tamamlandı", + "session.question.progress": "{{total}} sorunun {{current}} tanesi", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Dosya Gezgini", + "session.header.open.fileManager": "Dosya Yöneticisi", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "Geliştirme performansı teşhisi", + "debugBar.na": "yok", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Yönlendirici başlangıcından yerleşme sonrası ilk boyamaya kadar ölçülen, bir oturum sayfasına dokunan son tamamlanmış rota geçişi.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Son 5 saniyedeki hareketli saniye başına kare ortalaması.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Son 5 saniyedeki en kötü kare süresi.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Son 5 saniyede 32ms üzerindeki kareler.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Son 5 saniyedeki engellenen süre ve uzun görev sayısı. Maksimum görev: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Son 5 saniyede gözlemlenen en kötü giriş gecikmesi.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "Son 5 saniyedeki yaklaşık etkileşim süresi. Bu INP benzeridir, resmi Web Vitals INP değildir.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Mevcut uygulama ömrü için kümülatif düzen kayması.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Kullanılan JS yığınının yığın sınırına oranı. Yalnızca Chromium.", + "debugBar.mem.tip": "Kullanılan JS yığınının yığın sınırına oranı: {{used}} / {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Tüm etkileşimli öğelere odak stillerini zorla uygula", + "debugBar.focus.on": "AÇIK", + "debugBar.focus.off": "KAPALI", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Seçili dili değiştirmeden tüm uygulamanın düzen yönünü zorla ayarla", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Boşluk", + "common.key.backspace": "Geri Al", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "bilinmiyor", + "error.page.circular": "[Döngüsel]", + "error.serverSDK.noServerAvailable": "Sunucu yok", + "error.serverSDK.serverNotAvailable": "Sunucu mevcut değil", + "error.childStore.persistedCacheCreateFailed": "Kalıcı önbellek oluşturulamadı", + "error.childStore.persistedProjectMetadataCreateFailed": "Kalıcı proje meta verileri oluşturulamadı", + "error.childStore.persistedProjectIconCreateFailed": "Kalıcı proje simgesi oluşturulamadı", + "error.childStore.storeCreateFailed": "Depo oluşturulamadı", + "terminal.connectionLost.abnormalClose": "WebSocket anormal şekilde kapandı: {{code}}", +} satisfies Partial> diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts new file mode 100644 index 0000000000000000000000000000000000000000..54732a87b7ebdaa494cc33d4b8c682b7ae5fce07 --- /dev/null +++ b/packages/app/src/i18n/uk.ts @@ -0,0 +1,1267 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Файл", + "desktop.menu.edit": "Редагування", + "desktop.menu.view": "Вигляд", + "desktop.menu.go": "Перехід", + "desktop.menu.window": "Вікно", + "desktop.menu.help": "Довідка", + "desktop.menu.checkForUpdates": "Перевірити наявність оновлень...", + "desktop.menu.settings": "Налаштування", + "desktop.menu.reloadWebview": "Перезавантажити вебподання", + "desktop.menu.restart": "Перезапустити", + "desktop.menu.exportLogs": "Експортувати журнали...", + "desktop.menu.newSession": "Нова сесія", + "desktop.menu.openProject": "Відкрити проєкт...", + "desktop.menu.newWindow": "Нове вікно", + "desktop.menu.closeWindow": "Закрити вікно", + "desktop.menu.undo": "Скасувати", + "desktop.menu.redo": "Повторити", + "desktop.menu.cut": "Вирізати", + "desktop.menu.copy": "Копіювати", + "desktop.menu.paste": "Вставити", + "desktop.menu.delete": "Видалити", + "desktop.menu.selectAll": "Вибрати все", + "desktop.menu.toggleSidebar": "Перемкнути бічну панель", + "desktop.menu.toggleTerminal": "Перемкнути термінал", + "desktop.menu.toggleFileTree": "Перемкнути дерево файлів", + "desktop.menu.reload": "Перезавантажити", + "desktop.menu.toggleDeveloperTools": "Перемкнути інструменти розробника", + "desktop.menu.actualSize": "Фактичний розмір", + "desktop.menu.zoomIn": "Збільшити", + "desktop.menu.zoomOut": "Зменшити", + "desktop.menu.toggleFullScreen": "Перемкнути повноекранний режим", + "desktop.menu.back": "Назад", + "desktop.menu.forward": "Вперед", + "desktop.menu.previousSession": "Попередня сесія", + "desktop.menu.nextSession": "Наступна сесія", + "desktop.menu.previousProject": "Попередній проєкт", + "desktop.menu.nextProject": "Наступний проєкт", + "desktop.menu.minimize": "Згорнути", + "desktop.menu.maximize": "Розгорнути", + "desktop.menu.documentation": "Документація OpenCode", + "desktop.menu.supportForum": "Форум підтримки", + "desktop.menu.shareFeedback": "Надіслати відгук", + "desktop.menu.reportBug": "Повідомити про помилку", + "desktop.menu.ariaLabel": "Меню OpenCode", + + "desktop.updater.dialog.checkFailed.message": "Не вдалося перевірити наявність оновлень.", + "desktop.updater.dialog.checkFailed.title": "Помилка оновлення", + "desktop.updater.dialog.upToDate.message": "У вас найновіша версія.", + "desktop.updater.dialog.upToDate.title": "Оновлень немає", + "desktop.updater.dialog.ready.message": "Оновлення {{version}} завантажено. Перезапустити зараз?", + "desktop.updater.dialog.ready.title": "Оновлення готове", + "desktop.updater.dialog.restart": "Перезапустити", + "desktop.updater.dialog.later": "Пізніше", + + "desktop.recovery.action.relaunch": "Запустити повторно", + "desktop.recovery.action.exportLogs": "Експортувати журнали", + "desktop.recovery.action.keepWaiting": "Продовжити очікування", + "desktop.recovery.action.quit": "Завершити роботу", + "desktop.recovery.loadFailed": "Не вдалося завантажити OpenCode", + "desktop.recovery.terminated": "Роботу вікна OpenCode несподівано завершено", + "desktop.recovery.unresponsive": "OpenCode не відповідає", + "desktop.recovery.unresponsive.detail": + "Ви можете повторно запустити програму, відкрити журнали або продовжити очікування.", + "desktop.recovery.loadFailed.detail": "Вікно: {{window}}\nURL: {{url}}\nПомилка: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Вікно: {{window}}\nПричина: {{reason}}\nКод: {{code}}", + "desktop.recovery.unknown": "<невідомо>", + + "desktop.dialog.chooseFolder": "Виберіть папку", + "desktop.dialog.chooseFile": "Виберіть файл", + "desktop.dialog.saveFile": "Зберегти файл", + "desktop.dialog.files": "Файли", + "desktop.server.local": "Локальний сервер", + + "desktop.wsl.error.windowsOnly": "WSL доступна лише у Windows", + "desktop.wsl.error.unavailable": "WSL недоступна", + "desktop.wsl.error.listInstalled": "Не вдалося отримати список встановлених дистрибутивів WSL", + "desktop.wsl.error.listOnline": "Не вдалося отримати список доступних онлайн дистрибутивів WSL", + "desktop.wsl.error.executeDistro": "Не вдалося виконати команди в дистрибутиві", + "desktop.wsl.error.installWsl": "Не вдалося встановити WSL", + "desktop.wsl.error.installDistro": "Не вдалося встановити дистрибутив: {{distro}}", + "desktop.wsl.error.installOpencode": "Не вдалося встановити OpenCode", + "desktop.wsl.error.alreadyAdded": "{{distro}} уже додано", + "desktop.wsl.error.opencodeMissing": "opencode не встановлено в цьому дистрибутиві", + "desktop.wsl.error.opencodeCannotRun": "opencode встановлено, але його не вдалося запустити", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode не встановлено в {{distro}}", + "desktop.wsl.error.updateVersion": + "Оновлення OpenCode завершено, але {{distro}} усе ще повідомляє про версію {{installed}}; очікувалася {{expected}}", + "desktop.wsl.error.noVersion": "версію не вказано", + "desktop.wsl.error.serverExited": "Сервер WSL завершив роботу після запуску (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Сервер WSL завершив роботу, не досягнувши працездатного стану (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Час очікування перевірки працездатності sidecar для {{distro}} минув через {{timeout}}мс", + "desktop.wsl.error.commandTimeout": "Час очікування {{command}} {{args}} минув через {{timeout}}мс", + "desktop.wsl.error.failedPort": "Не вдалося отримати порт", + + "desktop.picker.error.notSelected": "Засіб вибору не повернув вибраний файл", + "desktop.picker.error.sizeLimit": "Вибрані вкладення перевищують обмеження в {{limit}} МБ", + + "command.category.suggested": "Рекомендовані", + "command.category.view": "Вигляд", + "command.category.project": "Проєкт", + "command.category.provider": "Провайдер", + "command.category.server": "Сервер", + "command.category.session": "Сесія", + "command.category.theme": "Тема", + "command.category.language": "Мова", + "command.category.file": "Файл", + "command.category.context": "Контекст", + "command.category.terminal": "Термінал", + "command.category.model": "Модель", + "command.category.mcp": "MCP", + "command.category.agent": "Агент", + "command.category.permissions": "Дозволи", + "command.category.workspace": "Робоча область", + "command.category.settings": "Налаштування", + "command.logs.export": "Експортувати журнали", + + "theme.scheme.system": "Системна", + "theme.scheme.light": "Світла", + "theme.scheme.dark": "Темна", + + "command.sidebar.toggle": "Перемкнути бічну панель", + "command.project.open": "Відкрити проєкт", + "command.project.previous": "Попередній проєкт", + "command.project.next": "Наступний проєкт", + "command.project.index": "Перемкнути на проєкт {{index}}", + "command.provider.connect": "Підключити провайдера", + "command.server.switch": "Перемкнути сервер", + "command.settings.open": "Відкрити налаштування", + "command.session.previous": "Попередня сесія", + "command.session.next": "Наступна сесія", + "command.session.previous.unseen": "Попередня непрочитана сесія", + "command.session.next.unseen": "Наступна непрочитана сесія", + "command.session.archive": "Архівувати сесію", + + "command.palette": "Палітра команд", + + "command.theme.cycle": "Перемкнути тему", + "command.theme.set": "Використати тему: {{theme}}", + "command.theme.scheme.cycle": "Перемкнути кольорову схему", + "command.theme.scheme.set": "Використати кольорову схему: {{scheme}}", + + "command.language.cycle": "Перемкнути мову", + "command.language.set": "Використати мову: {{language}}", + + "command.session.new": "Нова сесія", + "command.file.open": "Відкрити файл", + "command.tab.close": "Закрити вкладку", + "command.tab.reopenClosed": "Повторно відкрити закриту вкладку", + "command.context.addSelection": "Додати виділення до контексту", + "command.context.addSelection.description": "Додати вибрані рядки з поточного файлу", + "command.input.focus": "Фокус на полі введення", + "command.terminal.toggle": "Перемкнути термінал", + "command.fileTree.toggle": "Перемкнути дерево файлів", + "command.review.toggle": "Перемкнути огляд", + "command.terminal.new": "Новий термінал", + "command.terminal.new.description": "Створити нову вкладку термінала", + "command.steps.toggle": "Перемкнути кроки", + "command.steps.toggle.description": "Показати або приховати кроки для поточного повідомлення", + "command.message.previous": "Попереднє повідомлення", + "command.message.previous.description": "Перейти до попереднього повідомлення користувача", + "command.message.next": "Наступне повідомлення", + "command.message.next.description": "Перейти до наступного повідомлення користувача", + "command.model.choose": "Вибрати модель", + "command.model.choose.description": "Вибрати іншу модель", + "command.mcp.toggle": "Перемкнути MCP", + "command.mcp.toggle.description": "Перемкнути MCP", + "command.agent.cycle": "Перемкнути агента", + "command.agent.cycle.description": "Перемкнути на наступного агента", + "command.agent.cycle.reverse": "Перемкнути агента в зворотному напрямку", + "command.agent.cycle.reverse.description": "Перемкнути на попереднього агента", + "command.model.variant.cycle": "Перемкнути рівень мислення", + "command.model.variant.cycle.description": "Перемкнути на наступний рівень зусилля", + "command.prompt.mode.shell": "Команда", + "command.prompt.mode.normal": "Запит", + "command.permissions.autoaccept.enable": "Автоматично приймати дозволи", + "command.permissions.autoaccept.disable": "Зупинити автоматичне прийняття дозволів", + "command.workspace.toggle": "Перемкнути робочі області", + "command.workspace.toggle.description": "Увімкнути або вимкнути декілька робочих областей на бічній панелі", + "command.session.undo": "Скасувати", + "command.session.undo.description": "Скасувати останнє повідомлення", + "command.session.redo": "Повторити", + "command.session.redo.description": "Повторити останнє скасоване повідомлення", + "command.session.compact": "Стиснути сесію", + "command.session.compact.description": "Підсумувати сесію, щоб зменшити розмір контексту", + "command.session.fork": "Відгалузити від повідомлення", + "command.session.fork.description": "Створити нову сесію з попереднього повідомлення", + "command.session.share": "Поділитися сесією", + "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": "Результатів не знайдено", + "palette.group.commands": "Команди", + "palette.group.files": "Файли", + + "dialog.provider.search.placeholder": "Пошук провайдерів", + "dialog.provider.empty": "Провайдерів не знайдено", + "dialog.provider.group.popular": "Популярні", + "dialog.provider.group.other": "Інші", + "dialog.provider.custom.label": "Користувацький провайдер, сумісний з OpenAI", + "dialog.provider.tag.recommended": "Рекомендовані", + "dialog.provider.opencode.note": "Відібрані моделі, включаючи Claude, GPT, Gemini та інші", + "dialog.provider.opencode.tagline": "Надійні оптимізовані моделі", + "dialog.provider.opencodeGo.tagline": "Недорога підписка для всіх", + "dialog.provider.anthropic.note": "Прямий доступ до моделей Claude, включаючи Pro та Max", + "dialog.provider.copilot.note": "Моделі ШІ для допомоги в кодуванні через GitHub Copilot", + "dialog.provider.openai.note": "Моделі GPT для швидких і універсальних завдань ШІ", + "dialog.provider.google.note": "Моделі Gemini для швидких структурованих відповідей", + "dialog.provider.openrouter.note": "Доступ до всіх підтримуваних моделей від одного провайдера", + "dialog.provider.vercel.note": "Уніфікований доступ до моделей ШІ з інтелектуальною маршрутизацією", + + "dialog.model.select.title": "Вибрати модель", + "dialog.model.search.placeholder": "Пошук моделей", + "dialog.model.empty": "Моделей не знайдено", + "dialog.model.manage": "Керувати моделями", + "dialog.model.manage.description": "Налаштуйте, які моделі відображатимуться у виборі моделей.", + "dialog.model.manage.provider.toggle": "Перемкнути всі моделі {{provider}}", + + "dialog.model.unpaid.freeModels.title": "Безкоштовні моделі від OpenCode", + "dialog.model.unpaid.addMore.title": "Додати більше моделей від популярних провайдерів", + "dialog.model.unpaid.viewMoreProviders": "Переглянути ще понад 70 провайдерів", + + "dialog.provider.viewAll": "Показати більше провайдерів", + + "provider.connect.title": "Підключити {{provider}}", + "provider.connect.title.anthropicProMax": "Увійти з Claude Pro/Max", + "provider.connect.selectMethod": "Виберіть спосіб входу для {{provider}}.", + "provider.connect.method.apiKey": "Ключ API", + "provider.connect.method.browser": "Браузер", + "provider.connect.method.headless": "Без графічного інтерфейсу", + "provider.connect.status.inProgress": "Авторизація виконується...", + "provider.connect.status.waiting": "Очікування авторизації...", + "provider.connect.status.failed": "Авторизація не вдалася: {{error}}", + "provider.connect.apiKey.description": + "Введіть ключ API {{provider}}, щоб підключити обліковий запис і використовувати моделі {{provider}} у OpenCode.", + "provider.connect.apiKey.label": "Ключ API {{provider}}", + "provider.connect.apiKey.placeholder": "Ключ API", + "provider.connect.apiKey.required": "Ключ API обов'язковий", + "provider.connect.opencodeZen.line1": + "OpenCode Zen надає доступ до відібраного набору надійних оптимізованих моделей для агентів кодування.", + "provider.connect.opencodeZen.line2": + "З одним ключем API ви отримаєте доступ до таких моделей, як Claude, GPT, Gemini, GLM та інших.", + "provider.connect.opencodeZen.visit.prefix": "Відвідайте ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": ", щоб отримати ключ API.", + "provider.connect.oauth.code.visit.prefix": "Відвідайте ", + "provider.connect.oauth.code.visit.link": "це посилання", + "provider.connect.oauth.code.visit.suffix": + ", щоб отримати код авторизації, підключити обліковий запис і використовувати моделі {{provider}} у OpenCode.", + "provider.connect.oauth.code.label": "Код авторизації {{method}}", + "provider.connect.oauth.code.placeholder": "Код авторизації", + "provider.connect.oauth.code.required": "Код авторизації обов'язковий", + "provider.connect.oauth.code.invalid": "Недійсний код авторизації", + "provider.connect.oauth.auto.visit.prefix": "Відвідайте ", + "provider.connect.oauth.auto.visit.link": "це посилання", + "provider.connect.oauth.auto.visit.suffix": + " і введіть код нижче, щоб підключити обліковий запис і використовувати моделі {{provider}} у OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Код підтвердження", + "provider.connect.toast.connected.title": "{{provider}} підключено", + "provider.connect.toast.connected.description": "Моделі {{provider}} тепер доступні для використання.", + + "provider.custom.title": "Користувацький провайдер", + "provider.custom.unavailable": "Користувацькі провайдери недоступні на цьому сервері", + "provider.custom.description.prefix": "Налаштуйте провайдера, сумісного з OpenAI. Перегляньте ", + "provider.custom.description.link": "документацію з налаштування провайдера", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID провайдера", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Малі літери, цифри, дефіси або підкреслення", + "provider.custom.field.name.label": "Відображувана назва", + "provider.custom.field.name.placeholder": "Мій провайдер ШІ", + "provider.custom.field.baseURL.label": "Базовий URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "Ключ API", + "provider.custom.field.apiKey.placeholder": "Ключ API", + "provider.custom.field.apiKey.description": + "Необов'язково. Залиште порожнім, якщо ви керуєте авторизацією через заголовки.", + "provider.custom.models.label": "Моделі", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Назва", + "provider.custom.models.name.placeholder": "Відображувана назва", + "provider.custom.models.remove": "Видалити модель", + "provider.custom.models.add": "Додати модель", + "provider.custom.headers.label": "Заголовки (необов'язково)", + "provider.custom.headers.key.label": "Заголовок", + "provider.custom.headers.key.placeholder": "Назва-Заголовка", + "provider.custom.headers.value.label": "Значення", + "provider.custom.headers.value.placeholder": "значення", + "provider.custom.headers.remove": "Видалити заголовок", + "provider.custom.headers.add": "Додати заголовок", + "provider.custom.error.providerID.required": "ID провайдера обов'язковий", + "provider.custom.error.providerID.format": "Використовуйте малі літери, цифри, дефіси або підкреслення", + "provider.custom.error.providerID.exists": "ID провайдера вже існує", + "provider.custom.error.name.required": "Відображувана назва обов'язкова", + "provider.custom.error.baseURL.required": "Базовий URL обов'язковий", + "provider.custom.error.baseURL.format": "Має починатися з http:// або https://", + "provider.custom.error.required": "Обов'язково", + "provider.custom.error.duplicate": "Дублікат", + + "provider.disconnect.toast.disconnected.title": "{{provider}} відключено", + "provider.disconnect.toast.disconnected.description": "Моделі {{provider}} більше недоступні.", + + "model.tag.free": "Безкоштовно", + "model.tag.latest": "Остання", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "текст", + "model.input.image": "зображення", + "model.input.audio": "аудіо", + "model.input.video": "відео", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Дозволяє: {{inputs}}", + "model.tooltip.context.label": "Контекст", + "model.tooltip.inputs": "Вхідні дані", + "model.tooltip.model": "Модель", + "model.tooltip.provider": "Провайдер", + "model.tooltip.reasoning": "Мислення", + "model.tooltip.reasoning.allowed": "Підтримує мислення", + "model.tooltip.reasoning.none": "Без мислення", + "model.tooltip.context": "Ліміт контексту {{limit}}", + + "common.search.placeholder": "Пошук", + "common.goBack": "Назад", + "common.goForward": "Вперед", + "common.loading": "Завантаження", + "common.loading.ellipsis": "...", + "common.cancel": "Скасувати", + "common.clear": "Очистити", + "common.open": "Відкрити", + "common.connect": "Підключити", + "common.disconnect": "Відключити", + "common.continue": "Продовжити", + "common.submit": "Надіслати", + "common.save": "Зберегти", + "common.saving": "Збереження...", + "common.default": "За замовчуванням", + "common.attachment": "вкладення", + + "prompt.placeholder.shell": "Введіть команду термінала... {{example}}", + "prompt.placeholder.normal": 'Запитайте що завгодно... "{{example}}"', + "prompt.placeholder.simple": "Запитайте що завгодно...", + "prompt.placeholder.summarizeComments": "Підсумувати коментарі…", + "prompt.placeholder.summarizeComment": "Підсумувати коментар…", + "prompt.mode.shell": "Команда", + "prompt.mode.normal": "Запит", + "prompt.mode.shell.exit": "esc для виходу", + "session.child.promptDisabled": "Сесії підагентів не можна надсилати запити.", + "session.child.backToParent": "Назад до основної сесії.", + + "prompt.example.1": "Виправити TODO у коді", + "prompt.example.2": "Який технологічний стек цього проєкту?", + "prompt.example.3": "Виправити зламані тести", + "prompt.example.4": "Пояснити, як працює автентифікація", + "prompt.example.5": "Знайти та виправити вразливості безпеки", + "prompt.example.6": "Додати модульні тести для сервісу користувача", + "prompt.example.7": "Рефакторити цю функцію, щоб зробити її більш читабельною", + "prompt.example.8": "Що означає ця помилка?", + "prompt.example.9": "Допоможіть мені налагодити цю проблему", + "prompt.example.10": "Згенерувати документацію API", + "prompt.example.11": "Оптимізувати запити до бази даних", + "prompt.example.12": "Додати валідацію введення", + "prompt.example.13": "Створити новий компонент для...", + "prompt.example.14": "Як розгорнути цей проєкт?", + "prompt.example.15": "Перевірити мій код на відповідність найкращим практикам", + "prompt.example.16": "Додати обробку помилок до цієї функції", + "prompt.example.17": "Пояснити цей регулярний вираз", + "prompt.example.18": "Конвертувати це в TypeScript", + "prompt.example.19": "Додати логування по всьому коду", + "prompt.example.20": "Які залежності застарілі?", + "prompt.example.21": "Допоможіть написати скрипт міграції", + "prompt.example.22": "Реалізувати кешування для цього ендпоінта", + "prompt.example.23": "Додати посторінкову навігацію до цього списку", + "prompt.example.24": "Створити команду CLI для...", + "prompt.example.25": "Як тут працюють змінні середовища?", + + "prompt.popover.emptyResults": "Немає відповідних результатів", + "prompt.popover.emptyCommands": "Немає відповідних команд", + "prompt.dropzone.label": "Перетягніть сюди зображення, PDF або текстові файли", + "prompt.dropzone.file.label": "Перетягніть, щоб @згадати файл", + "prompt.slash.badge.custom": "користувацький", + "prompt.slash.badge.skill": "навичка", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "активний", + "prompt.context.includeActiveFile": "Включити активний файл", + "prompt.context.removeActiveFile": "Видалити активний файл з контексту", + "prompt.context.removeFile": "Видалити файл з контексту", + "prompt.action.attachFile": "Додати файли", + "prompt.menu.addImagesAndFiles": "Додати файли та інше", + "prompt.menu.imagesAndFiles": "Зображення та файли", + "prompt.menu.commands": "Команди", + "prompt.menu.context": "Контекст", + "prompt.menu.shellCommand": "Команда оболонки", + "prompt.attachment.remove": "Видалити вкладення", + "prompt.action.send": "Надіслати", + "prompt.action.stop": "Зупинити", + + "prompt.toast.pasteUnsupported.title": "Непідтримуване вкладення", + "prompt.toast.attachmentDuplicate.title": "Цей файл уже завантажено", + "prompt.toast.pasteUnsupported.description": "Сюди можна прикріплювати лише зображення, PDF або текстові файли.", + "prompt.toast.modelAgentRequired.title": "Виберіть агента та модель", + "prompt.toast.modelAgentRequired.description": "Виберіть агента та модель перед надсиланням запиту.", + "prompt.toast.worktreeCreateFailed.title": "Не вдалося створити робоче дерево", + "prompt.toast.sessionCreateFailed.title": "Не вдалося створити сесію", + "prompt.toast.shellSendFailed.title": "Не вдалося надіслати команду термінала", + "prompt.toast.commandSendFailed.title": "Не вдалося надіслати команду", + "prompt.toast.promptSendFailed.title": "Не вдалося надіслати запит", + "prompt.toast.promptSendFailed.description": "Не вдалося отримати сесію", + + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{enabled}} з {{total}} увімкнено", + "dialog.mcp.empty": "MCP не налаштовано", + + "dialog.lsp.empty": "Сервери LSP автоматично виявляються за типами файлів", + "dialog.plugins.empty": "Плагіни налаштовані в opencode.json", + + "mcp.status.connected": "підключено", + "mcp.status.failed": "помилка", + "mcp.status.needs_auth": "потрібна авторизація", + "mcp.status.disabled": "вимкнено", + "mcp.auth.clickToAuthenticate": "Натисніть для автентифікації", + + "dialog.fork.empty": "Немає повідомлень для відгалуження", + + "dialog.directory.search.placeholder": "Пошук папок", + "dialog.directory.empty": "Папок не знайдено", + "dialog.directory.action.selectFile": "Вибрати файл", + "dialog.directory.action.selectFolder": "Вибрати папку", + "dialog.directory.root": "Корінь", + "dialog.directory.parent": "Батьківська папка", + "dialog.directory.readError": "Не вдалося прочитати цю папку", + + "app.server.unreachable": "Не вдалося зв'язатися із {{server}}", + "app.server.retrying": "Автоматична повторна спроба...", + "app.server.otherServers": "Інші сервери", + + "dialog.server.title": "Сервери", + "dialog.server.description": "Перемкніть сервер OpenCode, до якого підключається ця програма.", + "dialog.server.search.placeholder": "Пошук серверів", + "dialog.server.empty": "Ще немає серверів", + "dialog.server.add.title": "Додати сервер", + "dialog.server.add.url": "Адреса сервера", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Не вдалося підключитися до сервера", + "dialog.server.add.checking": "Перевірка...", + "dialog.server.add.button": "Додати сервер", + "dialog.server.add.name": "Назва сервера (необов'язково)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Ім'я користувача (необов'язково)", + "dialog.server.add.usernamePlaceholder": "ім'я користувача", + "dialog.server.add.password": "Пароль (необов'язково)", + "dialog.server.add.passwordPlaceholder": "пароль", + "dialog.server.edit.title": "Редагувати сервер", + "dialog.server.default.title": "Сервер за замовчуванням", + "dialog.server.default.description": + "Підключатися до цього сервера під час запуску програми замість запуску локального сервера. Потребує перезапуску.", + "dialog.server.default.none": "Сервер не вибрано", + "dialog.server.default.set": "Встановити поточний сервер як сервер за замовчуванням", + "dialog.server.default.clear": "Очистити", + "dialog.server.action.remove": "Видалити сервер", + + "dialog.server.menu.edit": "Редагувати", + "dialog.server.menu.default": "Встановити за замовчуванням", + "dialog.server.menu.defaultRemove": "Скасувати використання за замовчуванням", + "dialog.server.menu.delete": "Видалити", + "dialog.server.current": "Поточний сервер", + "dialog.server.status.default": "За замовчуванням", + "wsl.server.add": "Додати сервер WSL", + "wsl.server.addShort": "Додати WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "Сервер WSL", + "wsl.server.retryStart": "Повторити запуск", + "wsl.server.updating": "Оновлення...", + "wsl.onboarding.step.distro": "Вибрати дистрибутив", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Перевірка WSL...", + "wsl.onboarding.restartRequired": "Щоб завершити встановлення WSL, потрібно перезапустити Windows.", + "wsl.onboarding.ready": "WSL готова до роботи.", + "wsl.onboarding.required": "Для продовження потрібна WSL.", + "wsl.onboarding.checkingDistros": "Перевірка дистрибутивів...", + "wsl.onboarding.installingDistro": "Встановлення {{distro}}...", + "wsl.onboarding.checkingDistro": "Перевірка {{distro}}...", + "wsl.onboarding.listingDistros": "Отримання списку дистрибутивів...", + "wsl.onboarding.distroReady": "{{distro}} готовий до роботи.", + "wsl.onboarding.distroNotInstalled": "{{distro}} ще не встановлено.", + "wsl.onboarding.openDistroOnce": "Відкрийте {{distro}} один раз, щоб завершити налаштування.", + "wsl.onboarding.finishingDistro": "Завершення налаштування {{distro}}.", + "wsl.onboarding.pickDistro": "Виберіть дистрибутив або встановіть один із наведених нижче.", + "wsl.onboarding.checkingOpencode": "Перевірка OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Перевірка OpenCode у {{distro}}...", + "wsl.onboarding.updatingOpencode": "Оновлення OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Оновлення OpenCode у {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Оновіть OpenCode у {{distro}}.", + "wsl.onboarding.updateOpencode": "Оновити OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode готовий до роботи у {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode готовий до роботи.", + "wsl.onboarding.installOpencodeIn": "Встановіть OpenCode у {{distro}}.", + "wsl.onboarding.installOpencode": "Встановити OpenCode", + "wsl.onboarding.chooseDistroFirst": "Спочатку виберіть дистрибутив.", + "wsl.onboarding.loadFailed": "Не вдалося завантажити стан WSL.", + "wsl.onboarding.loading": "Завантаження...", + "wsl.onboarding.installedDistros": "Встановлені дистрибутиви", + "wsl.onboarding.checkAgain": "Перевірити ще раз", + "wsl.onboarding.distroStatus.ready": "Готовий", + "wsl.onboarding.distroStatus.checking": "Перевірка...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode не встановлено", + "wsl.onboarding.distroStatus.missingTools": "Немає bash і curl", + "wsl.onboarding.distroStatus.unsupported": "Не підтримується · Використовуйте WSL 2", + "wsl.onboarding.needAnotherDistro": "Потрібен інший дистрибутив?", + "wsl.onboarding.needAnotherDistroHint": "Встановіть дистрибутив Linux із каталогу WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL не встановлено", + "wsl.onboarding.wslNotInstalled.description": + "Для додавання сервера WSL в OpenCode потрібна WSL (Підсистема Windows для Linux)", + "wsl.onboarding.wslUnavailable.title": "WSL недоступна", + "wsl.onboarding.wslUnavailable.description": "OpenCode не вдалося перевірити WSL на цьому комп'ютері.", + "wsl.onboarding.installWsl": "Встановити WSL", + "wsl.onboarding.windowsRestartRequired": + "Перезапустіть Windows, щоб завершити встановлення WSL, а потім знову відкрийте OpenCode.", + "wsl.onboarding.next": "Далі", + "wsl.onboarding.refresh": "Оновити", + "wsl.onboarding.allDistrosAdded": "Усі встановлені дистрибутиви вже додано.", + "wsl.onboarding.noDistros": "Дистрибутивів поки не виявлено.", + "wsl.onboarding.install": "Встановити", + "wsl.onboarding.installing": "Встановлення...", + "wsl.onboarding.installDistro": "Встановити дистрибутив", + "wsl.onboarding.searchDistros": "Пошук дистрибутивів", + "wsl.onboarding.wsl2Required": "Для роботи потрібна WSL 2.", + "wsl.onboarding.toolsRequired": "Для цього дистрибутива потрібні bash і curl.", + "wsl.onboarding.openTerminal": "Відкрити термінал", + "wsl.onboarding.path": "Шлях: {{path}}", + "wsl.onboarding.notFound": "не знайдено", + "wsl.onboarding.version": "Версія: {{version}}", + "wsl.onboarding.unknown": "невідомо", + "wsl.onboarding.desktopVersion": "десктопний застосунок {{version}}", + "wsl.onboarding.versionMismatch": "Встановлена версія не відповідає версії десктопного застосунку.", + "wsl.onboarding.adding": "Додавання...", + + "help.tabs.toast.ariaLabel": "Представляємо вкладки. Упорядковуйте роботу й активні сесії за допомогою вкладок", + "help.tabs.toast.dismiss": "Закрити інформацію про вкладки", + "help.tabs.title": "Представляємо вкладки", + "help.tabs.description": "Упорядковуйте роботу й активні сесії за допомогою вкладок", + "help.tabs.date": "14 липня", + "help.tabs.introduction": "OpenCode Desktop тепер побудовано навколо вкладок.", + "help.tabs.sessions": + "Почніть нову сесію у вкладці або відкрийте наявну сесію з будь-якого свого проєкту. Відкривайте нову вкладку, коли починаєте щось нове, і закривайте її після завершення.", + "help.tabs.organize": + "Кілька відкритих вкладок полегшують упорядкування активних сесій. Якщо плануєте залишити вкладки відкритими, перейменуйте їх на щось пам'ятне.", + "help.tabs.home": + "Усі свої сесії та проєкти ви знайдете на новому екрані «Головна». Вибрана сесія відкриється у вкладці.", + "help.tabs.persistence": "Після повторного відкриття програми ваші вкладки залишаться відкритими.", + "help.tabs.worktrees": + "Новий дизайн поки не підтримує робочі дерева Git, але ця можливість незабаром з'явиться. Тож якщо ви бажаєте й надалі користуватися попереднім макетом, перемикатися між макетами можна в налаштуваннях. Зауважте, що за кілька тижнів новий макет стане постійним.", + "server.row.noUsername": "без імені користувача", + + "dialog.project.edit.title": "Редагувати проєкт", + "dialog.project.edit.name": "Назва", + "dialog.project.edit.icon": "Іконка", + "dialog.project.edit.icon.alt": "Іконка проєкту", + "dialog.project.edit.icon.hint": "Натисніть або перетягніть зображення", + "dialog.project.edit.icon.recommended": "Рекомендовано: 128x128px", + "dialog.project.edit.color": "Колір", + "dialog.project.edit.color.select": "Вибрати колір {{color}}", + "dialog.project.edit.worktree.startup": "Скрипт запуску робочої області", + "dialog.project.edit.worktree.startup.description": "Виконується після створення нової робочої області (worktree).", + "dialog.project.edit.worktree.startup.placeholder": "напр. bun install", + + "dialog.releaseNotes.action.getStarted": "Розпочати", + "dialog.releaseNotes.action.next": "Далі", + "dialog.releaseNotes.action.hideFuture": "Не показувати це в майбутньому", + "dialog.releaseNotes.media.alt": "Попередній перегляд релізу", + + "dialog.usageExceeded.dontShowAgain": "Більше не показувати", + + "context.breakdown.title": "Розподіл контексту", + "context.breakdown.note": + 'Приблизний розподіл вхідних токенів. "Інше" включає визначення інструментів і накладні витрати.', + "context.breakdown.system": "Система", + "context.breakdown.user": "Користувач", + "context.breakdown.assistant": "Асистент", + "context.breakdown.tool": "Виклики інструментів", + "context.breakdown.other": "Інше", + + "context.systemPrompt.title": "Системний запит", + "context.rawMessages.title": "Необроблені повідомлення", + + "context.export.session": "Експортувати сесію", + + "context.stats.session": "Сесія", + "context.stats.messages": "Повідомлення", + "context.stats.provider": "Провайдер", + "context.stats.model": "Модель", + "context.stats.limit": "Ліміт контексту", + "context.stats.totalTokens": "Всього токенів", + "context.stats.usage": "Використання", + "context.stats.inputTokens": "Вхідні токени", + "context.stats.outputTokens": "Вихідні токени", + "context.stats.reasoningTokens": "Токени мислення", + "context.stats.cacheTokens": "Токени кешу (читання/запис)", + "context.stats.userMessages": "Повідомлення користувача", + "context.stats.assistantMessages": "Повідомлення асистента", + "context.stats.totalCost": "Загальна вартість", + "context.stats.sessionCreated": "Сесію створено", + "context.stats.lastActivity": "Остання активність", + + "context.usage.tokens": "Токени", + "context.usage.usage": "Використання", + "context.usage.cost": "Вартість", + "context.usage.clickToView": "Натисніть, щоб переглянути контекст", + "context.usage.view": "Переглянути використання контексту", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "Мова", + "toast.language.description": "Перемкнено на {{language}}", + + "toast.theme.title": "Тему змінено", + "toast.scheme.title": "Кольорова схема", + + "toast.workspace.enabled.title": "Робочі області увімкнено", + "toast.workspace.enabled.description": "Кілька робочих дерев тепер відображаються на бічній панелі", + "toast.workspace.disabled.title": "Робочі області вимкнено", + "toast.workspace.disabled.description": "Тільки головне робоче дерево відображається на бічній панелі", + + "toast.permissions.autoaccept.on.title": "Автоматичне прийняття дозволів", + "toast.permissions.autoaccept.on.description": "Запити дозволів будуть автоматично схвалюватися", + "toast.permissions.autoaccept.off.title": "Автоматичне прийняття дозволів зупинено", + "toast.permissions.autoaccept.off.description": "Запити дозволів вимагатимуть схвалення", + + "toast.model.none.title": "Модель не вибрано", + "toast.model.none.description": "Підключіть провайдера, щоб підсумувати цю сесію", + + "toast.file.loadFailed.title": "Не вдалося завантажити файл", + "toast.file.listFailed.title": "Не вдалося отримати список файлів", + + "toast.context.noLineSelection.title": "Не вибрано рядків", + "toast.context.noLineSelection.description": "Спочатку виберіть діапазон рядків у вкладці файлу.", + + "toast.session.share.copyFailed.title": "Не вдалося скопіювати URL у буфер обміну", + "toast.session.share.success.title": "Сесію опубліковано", + "toast.session.share.success.description": "Посилання скопійовано в буфер обміну!", + "toast.session.share.failed.title": "Не вдалося опублікувати сесію", + "toast.session.share.failed.description": "Під час публікації сесії сталася помилка", + + "toast.session.unshare.success.title": "Поширення сесії припинено", + "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": "Доступне оновлення", + "toast.update.description": "Нова версія OpenCode ({{version}}) тепер доступна для встановлення.", + "toast.update.action.installRestart": "Встановити та перезапустити", + "toast.update.action.notYet": "Не зараз", + + "error.page.title": "Щось пішло не так", + "error.page.description": "Під час завантаження програми сталася помилка.", + "error.page.description.localServerStartup": "Під час запуску локального сервера сталася помилка.", + "error.page.details.label": "Деталі помилки", + "error.page.action.restart": "Перезапустити", + "error.page.action.report": "Повідомити про помилку", + "error.page.action.reported": "Про помилку повідомлено", + "error.page.action.exportLogs": "Експортувати журнали", + "error.page.action.checking": "Перевірка...", + "error.page.action.checkUpdates": "Перевірити оновлення", + "error.page.action.updateTo": "Оновити до {{version}}", + "error.page.circular": "[Циклічне]", + "error.page.report.prefix": "Будь ласка, повідомте про цю помилку команді OpenCode", + "error.page.report.discord": "у Discord", + "error.page.version": "Версія: {{version}}", + + "error.dev.rootNotFound": + "Кореневий елемент не знайдено. Ви забули додати його до index.html? Або, можливо, атрибут id було написано з помилкою?", + + "error.serverSync.connectFailed": "Не вдалося підключитися до сервера. Чи працює сервер за адресою `{{url}}`?", + "error.serverSDK.noServerAvailable": "Сервер недоступний", + "error.serverSDK.serverNotAvailable": "Сервер недоступний", + "error.childStore.persistedCacheCreateFailed": "Не вдалося створити постійний кеш", + "error.childStore.persistedProjectMetadataCreateFailed": "Не вдалося створити постійні метадані проєкту", + "error.childStore.persistedProjectIconCreateFailed": "Не вдалося створити постійну іконку проєкту", + "error.childStore.storeCreateFailed": "Не вдалося створити сховище", + "directory.error.invalidUrl": "Недійсний каталог у URL.", + + "error.chain.unknown": "Невідома помилка", + "error.server.invalidConfiguration": "Недійсна конфігурація", + "error.chain.causedBy": "Причина:", + "error.chain.apiError": "Помилка API", + "error.chain.status": "Статус: {{status}}", + "error.chain.retryable": "Можна повторити: {{retryable}}", + "error.chain.responseBody": "Тіло відповіді:\n{{body}}", + "error.chain.didYouMean": "Можливо, ви мали на увазі: {{suggestions}}", + "error.chain.modelNotFound": "Модель не знайдено: {{provider}}/{{model}}", + "error.chain.checkConfig": "Перевірте назви провайдерів/моделей у конфігурації (opencode.json)", + "error.chain.mcpFailed": + 'Сервер MCP "{{name}}" не працює. Зверніть увагу, OpenCode ще не підтримує автентифікацію MCP.', + "error.chain.providerAuthFailed": "Автентифікація провайдера не вдалася ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Не вдалося ініціалізувати провайдера "{{provider}}". Перевірте облікові дані та конфігурацію.', + "error.chain.configJsonInvalid": "Файл конфігурації {{path}} не є дійсним JSON(C)", + "error.chain.configJsonInvalidWithMessage": "Файл конфігурації {{path}} не є дійсним JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + 'Каталог "{{dir}}" у {{path}} недійсний. Перейменуйте каталог на "{{suggestion}}" або видаліть його. Це поширена помилка.', + "error.chain.configFrontmatterError": "Не вдалося розібрати frontmatter у {{path}}:\n{{message}}", + "error.chain.configInvalid": "Файл конфігурації {{path}} недійсний", + "error.chain.configInvalidWithMessage": "Файл конфігурації {{path}} недійсний: {{message}}", + + "notification.permission.title": "Потрібен дозвіл", + "notification.permission.description": "{{sessionTitle}} у {{projectName}} потребує дозволу", + "notification.question.title": "Запитання", + "notification.question.description": "{{sessionTitle}} у {{projectName}} має запитання", + "notification.action.goToSession": "Перейти до сесії", + + "notification.session.responseReady.title": "Відповідь готова", + "notification.session.error.title": "Помилка сесії", + "notification.session.error.fallbackDescription": "Сталася помилка", + + "home.recentProjects": "Нещодавні проєкти", + "home.empty.title": "Немає нещодавніх проєктів", + "home.empty.description": "Почніть, відкривши локальний проєкт", + "home.title": "Головна", + "home.projects": "Проєкти", + "home.project.add": "Додати проєкт", + "home.recentlyClosed": "Нещодавно закриті", + "home.server.collapse": "Згорнути проєкти сервера", + "home.server.expand": "Розгорнути проєкти сервера", + "home.sessions.search.placeholder": "Пошук сесій", + "home.sessions.search.placeholder.scoped": "Пошук сесій у {{scope}}", + "home.sessions.search.sessions": "Сесії", + "home.sessions.search.noResults": "Сесій за запитом {{query}} не знайдено", + "home.sessions.empty": "Тут поки нічого немає", + "home.sessions.empty.description": "Створіть сесію, щоб почати", + "home.sessions.group.today": "Сьогодні", + "home.sessions.group.yesterday": "Учора", + "home.sessions.group.older": "Раніше", + "home.providerTip": + "Підключіться до понад 75 провайдерів, щоб використовувати інші моделі, зокрема Claude, GPT, Gemini та інші", + + "session.tab.session": "Сесія", + "session.tab.review": "Огляд", + "session.tab.context": "Контекст", + "session.tab.unknown": "Невідома сесія", + "session.panel.reviewAndFiles": "Огляд і файли", + "session.error.notFound": "Цю сесію не знайдено", + "session.error.notFound.description": "Ця вкладка посилається на сесію, якої більше немає на цьому сервері.", + "session.error.notFound.closeTab": "Закрити вкладку", + "session.error.serverConnection": "Не вдається підключитися до цього сервера", + "session.review.filesChanged": "Змінено файлів: {{count}}", + "session.review.change.one": "Зміна", + "session.review.change.other": "Зміни", + "session.review.loadingChanges": "Завантаження змін...", + "session.review.empty": "У цій сесії ще немає змін", + "session.review.noVcs": "Систему контролю версій Git не виявлено, зміни не відображаються", + "session.review.noVcs.createGit.title": "Створити Git-репозиторій", + "session.review.noVcs.createGit.description": "Відстежуйте, переглядайте та скасовуйте зміни в цьому проєкті", + "session.review.noVcs.createGit.actionLoading": "Створення Git-репозиторію...", + "session.review.noVcs.createGit.action": "Створити Git-репозиторій", + "session.review.noSnapshot": "Відстеження знімків вимкнено в конфігурації, тому зміни сесії недоступні", + "session.review.noChanges": "Немає змін", + "session.review.noUncommittedChanges": "Ще немає незафіксованих змін", + "session.review.noBranchChanges": "Ще немає змін у гілці", + + "session.files.selectToOpen": "Виберіть файл для відкриття", + "session.files.all": "Усі файли", + "session.files.empty": "Немає файлів", + "session.files.binaryContent": "Двійковий файл (вміст неможливо відобразити)", + + "session.messages.renderEarlier": "Відобразити раніші повідомлення", + "session.messages.loadingEarlier": "Завантаження раніших повідомлень...", + "session.messages.loadEarlier": "Завантажити раніші повідомлення", + "session.messages.loading": "Завантаження повідомлень...", + "session.messages.jumpToLatest": "Перейти до останніх", + + "session.context.addToContext": "Додати {{selection}} до контексту", + "session.todo.title": "Завдання", + "session.todo.collapse": "Згорнути", + "session.todo.expand": "Розгорнути", + "session.todo.progress": "Виконано {{done}} з {{total}} завдань", + "session.question.progress": "{{current}} з {{total}} запитань", + "session.question.minimize": "Згорнути запитання", + "session.question.restore": "Відновити запитання", + "session.question.pending.one": "{{count}} запитання очікує відповіді", + "session.question.pending.few": "{{count}} запитання очікують відповіді", + "session.question.pending.many": "{{count}} запитань очікують відповіді", + "session.question.pending.other": "Запитання, що очікують відповіді: {{count}}", + "session.followupDock.summary.one": "{{count}} повідомлення в черзі", + "session.followupDock.summary.few": "{{count}} повідомлення в черзі", + "session.followupDock.summary.many": "{{count}} повідомлень у черзі", + "session.followupDock.summary.other": "{{count}} повідомлень у черзі", + "session.followupDock.sendNow": "Надіслати зараз", + "session.followupDock.edit": "Редагувати", + "session.followupDock.collapse": "Згорнути повідомлення в черзі", + "session.followupDock.expand": "Розгорнути повідомлення в черзі", + "session.revertDock.summary.one": "{{count}} скасоване повідомлення", + "session.revertDock.summary.few": "{{count}} скасовані повідомлення", + "session.revertDock.summary.many": "{{count}} скасованих повідомлень", + "session.revertDock.summary.other": "{{count}} скасованих повідомлень", + "session.revertDock.collapse": "Згорнути скасовані повідомлення", + "session.revertDock.expand": "Розгорнути скасовані повідомлення", + "session.revertDock.restore": "Відновити повідомлення", + + "session.new.title": "Створити що завгодно", + "session.new.project.new": "Новий проєкт", + "session.new.project.search": "Пошук проєктів", + "session.new.project.add": "Додати проєкт", + "session.new.worktree.main": "Основна гілка", + "session.new.worktree.mainWithBranch": "Основна гілка ({{branch}})", + "session.new.worktree.create": "Створити нове робоче дерево", + "session.new.workspace.runIn": "Запустити сесію в", + "session.new.workspace.triggerLocal": "Локально", + "session.new.workspace.local": "Локальний репозиторій", + "session.new.workspace.existing": "Робоча область…", + "session.new.git.none": "Немає Git", + "session.new.lastModified": "Востаннє змінено", + + "session.header.search.placeholder": "Пошук {{project}}", + "session.header.searchFiles": "Пошук файлів", + "session.header.openIn": "Відкрити в", + "session.header.open.action": "Відкрити {{app}}", + "session.header.open.ariaLabel": "Відкрити в {{app}}", + "session.header.open.menu": "Параметри відкриття", + "session.header.open.copyPath": "Копіювати шлях", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Файловий провідник", + "session.header.reveal.finder": "Показати у Finder", + "session.header.reveal.fileExplorer": "Показати у Файловому провіднику", + "session.header.reveal.containingFolder": "Відкрити папку розташування", + "session.header.open.fileManager": "Файловий менеджер", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Термінал", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + + "status.popover.trigger": "Статус", + "status.popover.ariaLabel": "Конфігурації серверів", + "status.popover.tab.servers": "Сервери", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Плагіни", + "status.popover.action.manageServers": "Керувати серверами", + + "session.share.popover.title": "Опублікувати в інтернеті", + "session.share.popover.description.shared": + "Ця сесія є публічною в інтернеті. Вона доступна будь-кому за посиланням.", + "session.share.popover.description.unshared": + "Опублікуйте сесію публічно в інтернеті. Вона буде доступна будь-кому за посиланням.", + "session.share.action.share": "Поділитися", + "session.share.action.publish": "Опублікувати", + "session.share.action.publishing": "Публікація...", + "session.share.action.unpublish": "Скасувати публікацію", + "session.share.action.unpublishing": "Скасування публікації...", + "session.share.action.view": "Переглянути", + "session.share.copy.copied": "Скопійовано", + "session.share.copy.copyLink": "Копіювати посилання", + + "lsp.tooltip.none": "Немає серверів LSP", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "Завантаження запиту...", + "terminal.loading": "Завантаження термінала...", + "terminal.title": "Термінал", + "terminal.title.numbered": "Термінал {{number}}", + "terminal.close": "Закрити термінал", + "terminal.connectionLost.title": "З'єднання втрачено", + "terminal.connectionLost.abnormalClose": "WebSocket закрито аномально: {{code}}", + "terminal.connectionLost.description": + "З'єднання з терміналом було перервано. Це може статися під час перезапуску сервера.", + "terminal.connectTicket.csrfError": + "Квиток підключення PTY відхилено через перевірку джерела або CSRF. Перевірте конфігурацію CORS сервера.", + "terminal.connectTicket.statusError": "Помилка квитка підключення PTY зі статусом {{status}}", + + "titlebar.update": "Оновити", + "titlebar.updateVersion": "Оновити {{version}}", + + "common.closeTab": "Закрити вкладку", + "common.dismiss": "Відхилити", + "common.moreCountSuffix": " (ще {{count}})", + "common.requestFailed": "Запит не виконано", + "common.moreOptions": "Більше параметрів", + "common.learnMore": "Дізнатися більше", + "common.rename": "Перейменувати", + "common.export": "Експортувати", + "common.reset": "Скинути", + "common.archive": "Архівувати", + "common.delete": "Видалити", + "common.close": "Закрити", + "common.edit": "Редагувати", + "common.loadMore": "Завантажити більше", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Пробіл", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "невідомо", + + "common.time.justNow": "Щойно", + "common.time.minutesAgo.short": "{{count}} хв тому", + "common.time.hoursAgo.short": "{{count}} год тому", + "common.time.daysAgo.short": "{{count}} дн тому", + + "sidebar.menu.toggle": "Перемкнути меню", + "sidebar.nav.projectsAndSessions": "Проєкти та сесії", + "sidebar.settings": "Налаштування", + "sidebar.help": "Довідка", + "sidebar.workspaces.enable": "Увімкнути робочі області", + "sidebar.workspaces.disable": "Вимкнути робочі області", + "sidebar.gettingStarted.title": "Початок роботи", + "sidebar.gettingStarted.line1": "OpenCode містить безкоштовні моделі, тому ви можете почати негайно.", + "sidebar.gettingStarted.line2": + "Підключіть будь-якого провайдера, щоб використовувати моделі, включаючи Claude, GPT, Gemini тощо.", + "sidebar.project.recentSessions": "Нещодавні сесії", + "sidebar.project.viewAllSessions": "Переглянути всі сесії", + "sidebar.project.clearNotifications": "Очистити сповіщення", + "sidebar.empty.title": "Немає відкритих проєктів", + "sidebar.empty.description": "Відкрийте проєкт, щоб почати", + + "debugBar.ariaLabel": "Діагностика продуктивності розробки", + "debugBar.na": "н/д", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Останній завершений перехід маршруту, що торкається сторінки сесії, виміряний від запуску маршрутизатора до першого відображення після стабілізації.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Поточна кількість кадрів за секунду за останні 5 секунд.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Найгірший час кадру за останні 5 секунд.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Кадри понад 32 мс за останні 5 секунд.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Заблокований час і кількість довгих завдань за останні 5 секунд. Макс. завдання: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Найгірша спостережувана затримка введення за останні 5 секунд.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Приблизна тривалість взаємодії за останні 5 секунд. Це схоже на INP, а не на офіційний Web Vitals INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Сукупний зсув макета за весь час роботи програми.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Використана купа JS проти ліміту купи. Тільки Chromium.", + "debugBar.mem.tip": "Використана купа JS проти ліміту купи. {{used}} з {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Примусово застосувати стилі фокуса до всіх інтерактивних елементів", + "debugBar.focus.on": "УВІМК.", + "debugBar.focus.off": "ВИМК.", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Примусово задати напрямок макета всієї програми, не змінюючи вибрану мову", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + + "app.name.desktop": "OpenCode Desktop", + + "settings.section.desktop": "Десктопний застосунок", + "settings.section.server": "Сервер", + "settings.tab.general": "Загальні", + "settings.tab.shortcuts": "Клавіатурні скорочення", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "Інтеграція WSL", + "settings.desktop.wsl.description": "Запускати сервер OpenCode всередині WSL на Windows.", + + "settings.general.section.appearance": "Зовнішній вигляд", + "settings.general.section.advanced": "Додатково", + "settings.general.section.notifications": "Системні сповіщення", + "settings.general.section.updates": "Оновлення", + "settings.general.section.sounds": "Звукові ефекти", + "settings.general.section.feed": "Стрічка", + "settings.general.section.display": "Дисплей", + + "settings.general.row.language.title": "Мова", + "settings.general.row.language.description": "Змінити мову інтерфейсу OpenCode", + "settings.general.row.shell.title": "Командна оболонка термінала", + "settings.general.row.shell.description": + "Виберіть оболонку для термінала. Сумісні оболонки також використовуються для викликів інструментів агента.", + "settings.general.row.shell.autoDefault": "Автоматично (за замовчуванням)", + "settings.general.row.shell.terminalOnly": "тільки термінал", + "settings.general.row.appearance.title": "Зовнішній вигляд", + "settings.general.row.appearance.description": "Налаштуйте вигляд OpenCode на вашому пристрої", + "settings.general.row.colorScheme.title": "Кольорова схема", + "settings.general.row.colorScheme.description": "Виберіть, чи OpenCode використовує системну, світлу або темну тему", + "settings.general.row.theme.title": "Тема", + "settings.general.row.theme.description": "Налаштуйте тему OpenCode.", + "settings.general.row.font.title": "Шрифт коду", + "settings.general.row.font.description": "Налаштуйте шрифт, який використовується в блоках коду", + "settings.general.row.terminalFont.title": "Шрифт термінала", + "settings.general.row.terminalFont.description": "Налаштуйте шрифт, який використовується в терміналі", + "settings.general.row.uiFont.title": "Шрифт інтерфейсу", + "settings.general.row.uiFont.description": "Налаштуйте шрифт, який використовується в інтерфейсі", + "settings.general.row.followup.title": "Поведінка продовження", + "settings.general.row.followup.description": "Виберіть, чи продовження виконується негайно, чи чекає в черзі", + "settings.general.row.followup.option.queue": "Черга", + "settings.general.row.followup.option.steer": "Керування", + "settings.general.row.showFileTree.title": "Дерево файлів", + "settings.general.row.showFileTree.description": "Показувати панель дерева файлів у сесіях", + "settings.general.row.showNavigation.title": "Елементи навігації", + "settings.general.row.showNavigation.description": + "Показувати кнопки назад і вперед на панелі заголовка десктопного застосунку", + "settings.general.row.showSearch.title": "Палітра команд", + "settings.general.row.showSearch.description": "Показувати кнопку пошуку та палітри команд на панелі заголовка", + "settings.general.row.showTerminal.title": "Термінал", + "settings.general.row.showTerminal.description": + "Показувати кнопку термінала на панелі заголовка десктопного застосунку", + "settings.general.row.showStatus.title": "Статус сервера", + "settings.general.row.showStatus.description": "Показувати кнопку стану сервера на панелі заголовка", + "settings.general.row.mobileTitlebarBottom.title": "Нижня навігація", + "settings.general.row.mobileTitlebarBottom.description": + "Розмістити панель заголовка та вкладки сесії внизу екрана на мобільних пристроях", + "settings.general.row.showCustomAgents.title": "Показувати агента", + "settings.general.row.showCustomAgents.description": + "Перемикатися між агентами в редакторі запиту. Якщо приховано, типовим є агент Build.", + "settings.general.row.reasoningSummaries.title": "Показувати підсумки мислення", + "settings.general.row.reasoningSummaries.description": "Відображати підсумки мислення моделі на часовій шкалі", + "settings.general.row.shellToolPartsExpanded.title": "Розгортати частини інструменту оболонки", + "settings.general.row.shellToolPartsExpanded.description": + "Показувати частини інструменту оболонки розгорнутими за замовчуванням на часовій шкалі", + "settings.general.row.editToolPartsExpanded.title": "Розгортати частини інструменту редагування", + "settings.general.row.editToolPartsExpanded.description": + "Показувати частини інструментів редагування, запису та патчів розгорнутими за замовчуванням на часовій шкалі", + "settings.general.row.newInterface.title": "Новий макет", + "settings.general.row.newInterface.badge": "Нове", + "settings.general.row.newInterface.description": + "Використовуйте нові вкладки та макет головної сторінки. Протягом обмеженого часу можна перемикатися між макетами.", + "settings.general.row.newInterfaceNotice.title": "Тепер ви використовуєте новий макет", + "settings.general.row.newInterfaceNotice.description": "Попередній макет більше недоступний", + "settings.general.row.newInterfaceNotice.dismiss": "Відхилити", + "settings.general.row.pinchZoom.title": "Масштабування щипком", + "settings.general.row.pinchZoom.description": + "Дозволити масштабування жестом щипка на трекпаді та прокручуванням із Ctrl", + + "settings.general.row.wayland.title": "Використовувати нативний Wayland", + "settings.general.row.wayland.description": "Вимкнути резервний X11 на Wayland. Потребує перезапуску.", + "settings.general.row.wayland.tooltip": + "На Linux з моніторами з різною частотою оновлення нативний Wayland може бути більш стабільним.", + + "settings.general.row.releaseNotes.title": "Нотатки до релізу", + "settings.general.row.releaseNotes.description": 'Показувати спливаючі вікна "Що нового" після оновлень', + + "settings.updates.row.startup.title": "Перевіряти оновлення під час запуску", + "settings.updates.row.startup.description": "Автоматично перевіряти наявність оновлень під час запуску OpenCode", + "settings.updates.row.check.title": "Перевірити оновлення", + "settings.updates.row.check.description": "Вручну перевірити наявність оновлень і встановити, якщо доступні", + "settings.updates.action.checkNow": "Перевірити зараз", + "settings.updates.action.checking": "Перевірка...", + "settings.updates.action.downloading": "Завантаження...", + "settings.updates.action.installing": "Встановлення...", + "settings.updates.toast.latest.title": "У вас актуальна версія", + "settings.updates.toast.latest.description": "Ви використовуєте останню версію OpenCode.", + "sound.option.none": "Немає", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + + "settings.general.notifications.agent.title": "Агент", + "settings.general.notifications.agent.description": + "Показувати системне сповіщення, коли агент завершує роботу або потребує уваги", + "settings.general.notifications.permissions.title": "Дозволи", + "settings.general.notifications.permissions.description": "Показувати системне сповіщення, коли потрібен дозвіл", + "settings.general.notifications.errors.title": "Помилки", + "settings.general.notifications.errors.description": "Показувати системне сповіщення, коли виникає помилка", + + "settings.general.sounds.agent.title": "Агент", + "settings.general.sounds.agent.description": "Відтворювати звук, коли агент завершує роботу або потребує уваги", + "settings.general.sounds.permissions.title": "Дозволи", + "settings.general.sounds.permissions.description": "Відтворювати звук, коли потрібен дозвіл", + "settings.general.sounds.errors.title": "Помилки", + "settings.general.sounds.errors.description": "Відтворювати звук, коли виникає помилка", + + "settings.shortcuts.title": "Клавіатурні скорочення", + "settings.shortcuts.reset.button": "Відновити стандартні", + "settings.shortcuts.reset.toast.title": "Клавіатурні скорочення відновлено", + "settings.shortcuts.reset.toast.description": "Стандартні клавіатурні скорочення відновлено.", + "settings.shortcuts.conflict.title": "Клавіатурне скорочення вже використовується", + "settings.shortcuts.conflict.description": "{{keybind}} вже призначено для {{titles}}.", + "settings.shortcuts.unassigned": "Не призначено", + "settings.shortcuts.pressKeys": "Натисніть клавіші", + "settings.shortcuts.search.placeholder": "Пошук клавіатурних скорочень", + "settings.shortcuts.search.empty": "Клавіатурних скорочень не знайдено", + + "settings.shortcuts.group.general": "Загальні", + "settings.shortcuts.group.session": "Сесія", + "settings.shortcuts.group.navigation": "Навігація", + "settings.shortcuts.group.modelAndAgent": "Модель та агент", + "settings.shortcuts.group.terminal": "Термінал", + "settings.shortcuts.group.prompt": "Запит", + + "settings.providers.title": "Провайдери", + "settings.providers.description": "Налаштування провайдерів будуть доступні тут.", + "settings.providers.section.connected": "Підключені провайдери", + "settings.providers.connected.empty": "Немає підключених провайдерів", + "settings.providers.connected.environmentDescription": "Підключено зі змінних середовища", + "settings.providers.section.popular": "Популярні провайдери", + "settings.providers.custom.description": "Додайте провайдера, сумісного з OpenAI, за базовим URL.", + "settings.providers.tag.environment": "Середовище", + "settings.providers.tag.config": "Конфігурація", + "settings.providers.tag.custom": "Користувацький", + "settings.providers.tag.other": "Інше", + "settings.models.title": "Моделі", + "settings.models.description": "Налаштування моделей будуть доступні тут.", + "settings.agents.title": "Агенти", + "settings.agents.description": "Налаштування агентів будуть доступні тут.", + "settings.commands.title": "Команди", + "settings.commands.description": "Налаштування команд будуть доступні тут.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Налаштування MCP будуть доступні тут.", + + "settings.permissions.title": "Дозволи", + "settings.permissions.description": "Керуйте тим, які інструменти сервер може використовувати за замовчуванням.", + "settings.permissions.section.tools": "Інструменти", + "settings.permissions.toast.updateFailed.title": "Не вдалося оновити дозволи", + + "settings.permissions.action.allow": "Дозволити", + "settings.permissions.action.ask": "Запитувати", + "settings.permissions.action.deny": "Заборонити", + + "settings.permissions.tool.read.title": "Читання", + "settings.permissions.tool.read.description": "Читання файлу (відповідає шляху файлу)", + "settings.permissions.tool.edit.title": "Редагування", + "settings.permissions.tool.edit.description": "Зміна файлів, включаючи редагування, запис і патчі", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Зіставлення файлів за допомогою glob-шаблонів", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Пошук вмісту файлів за допомогою регулярних виразів", + "settings.permissions.tool.list.title": "Список", + "settings.permissions.tool.list.description": "Список файлів у каталозі", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Запуск команд оболонки", + "settings.permissions.tool.task.title": "Завдання", + "settings.permissions.tool.task.description": "Запуск підагентів", + "settings.permissions.tool.skill.title": "Навичка", + "settings.permissions.tool.skill.description": "Завантаження навички за назвою", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Виконання запитів мовного сервера", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Оновлення списку завдань", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "Отримання вмісту з URL", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "Пошук в інтернеті", + "settings.permissions.tool.external_directory.title": "Зовнішній каталог", + "settings.permissions.tool.external_directory.description": "Доступ до файлів за межами каталогу проєкту", + "settings.permissions.tool.doom_loop.title": "Цикл приреченості", + "settings.permissions.tool.doom_loop.description": + "Виявлення повторюваних викликів інструментів з однаковими вхідними даними", + + "session.delete.failed.title": "Не вдалося видалити сесію", + "session.delete.title": "Видалити сесію", + "session.delete.confirm": 'Видалити сесію "{{name}}"?', + "session.delete.button": "Видалити сесію", + + "workspace.new": "Нова робоча область", + "workspace.type.local": "локальна", + "workspace.type.sandbox": "пісочниця", + "workspace.create.failed.title": "Не вдалося створити робочу область", + "workspace.delete.failed.title": "Не вдалося видалити робочу область", + "workspace.resetting.title": "Скидання робочої області", + "workspace.resetting.description": "Це може зайняти хвилину.", + "workspace.reset.failed.title": "Не вдалося скинути робочу область", + "workspace.reset.success.title": "Робочу область скинуто", + "workspace.reset.success.description": "Робоча область тепер відповідає гілці за замовчуванням.", + "workspace.error.stillPreparing": "Робоча область все ще готується", + "workspace.status.checking": "Перевірка незлитих змін...", + "workspace.status.error": "Не вдалося перевірити статус git.", + "workspace.status.clean": "Незлитих змін не виявлено.", + "workspace.status.dirty": "Виявлено незлиті зміни в цій робочій області.", + "workspace.delete.title": "Видалити робочу область", + "workspace.delete.confirm": 'Видалити робочу область "{{name}}"?', + "workspace.delete.button": "Видалити робочу область", + "workspace.reset.title": "Скинути робочу область", + "workspace.reset.confirm": 'Скинути робочу область "{{name}}"?', + "workspace.reset.button": "Скинути робочу область", + "workspace.reset.archived.none": "Жодна активна сесія не буде заархівована.", + "workspace.reset.archived.one": "1 сесію буде заархівовано.", + "workspace.reset.archived.many": "{{count}} сесій буде заархівовано.", + "workspace.reset.note": "Це скине робочу область, щоб вона відповідала гілці за замовчуванням.", +} diff --git a/packages/app/src/i18n/uz.ts b/packages/app/src/i18n/uz.ts new file mode 100644 index 0000000000000000000000000000000000000000..8cb7097fc07aea8b534254951d6a2d3098b49927 --- /dev/null +++ b/packages/app/src/i18n/uz.ts @@ -0,0 +1,1171 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Fayl", + "desktop.menu.edit": "Tahrirlash", + "desktop.menu.view": "Ko'rish", + "desktop.menu.go": "Bor", + "desktop.menu.window": "Oyna", + "desktop.menu.help": "Yordam", + "desktop.menu.checkForUpdates": "Yangilanishlarni tekshiring...", + "desktop.menu.settings": "Sozlamalar", + "desktop.menu.reloadWebview": "Veb koʻrinishini qayta yuklash", + "desktop.menu.restart": "Qayta ishga tushirish", + "desktop.menu.exportLogs": "Jurnallarni eksport qilish...", + "desktop.menu.newSession": "Yangi sessiya", + "desktop.menu.openProject": "Loyihani ochish...", + "desktop.menu.newWindow": "Yangi oyna", + "desktop.menu.closeWindow": "Oynani yopish", + "desktop.menu.undo": "Bekor qilish", + "desktop.menu.redo": "Qayta qiling", + "desktop.menu.cut": "Kesish", + "desktop.menu.copy": "Nusxalash", + "desktop.menu.paste": "joylashtirish", + "desktop.menu.delete": "Oʻchirish", + "desktop.menu.selectAll": "Hammasini tanlang", + "desktop.menu.toggleSidebar": "Yon panelni almashtirish", + "desktop.menu.toggleTerminal": "Terminalni almashtirish", + "desktop.menu.toggleFileTree": "Fayl daraxtini almashtirish", + "desktop.menu.reload": "Qayta yuklash", + "desktop.menu.toggleDeveloperTools": "Dasturchi vositalarini almashtirish", + "desktop.menu.actualSize": "Haqiqiy o'lcham", + "desktop.menu.zoomIn": "Kattalashtirish", + "desktop.menu.zoomOut": "Kichraytirish", + "desktop.menu.toggleFullScreen": "Toʻliq ekran rejimiga oʻtish", + "desktop.menu.back": "Orqaga", + "desktop.menu.forward": "Oldinga", + "desktop.menu.previousSession": "Oldingi sessiya", + "desktop.menu.nextSession": "Keyingi sessiya", + "desktop.menu.previousProject": "Oldingi loyiha", + "desktop.menu.nextProject": "Keyingi loyiha", + "desktop.menu.minimize": "Minimallashtirish", + "desktop.menu.maximize": "Maksimallashtirish", + "desktop.menu.documentation": "OpenCode hujjatlari", + "desktop.menu.supportForum": "Qo'llab-quvvatlash forumi", + "desktop.menu.shareFeedback": "Fikr-mulohaza ulashing", + "desktop.menu.reportBug": "Xato haqida xabar berish", + "desktop.menu.ariaLabel": "OpenCode menyusi", + "desktop.updater.dialog.checkFailed.message": "Yangilanish tekshiruvi amalga oshmadi.", + "desktop.updater.dialog.checkFailed.title": "Yangilash xatosi", + "desktop.updater.dialog.upToDate.message": "Siz yangilangansiz.", + "desktop.updater.dialog.upToDate.title": "Yangilanishlar yoʻq", + "desktop.updater.dialog.ready.message": "{{version}} yangilanishi yuklab olindi. Hozir qayta ishga tushirilsinmi?", + "desktop.updater.dialog.ready.title": "Yangilanish tayyor", + "desktop.updater.dialog.restart": "Qayta ishga tushirish", + "desktop.updater.dialog.later": "Keyinchalik", + "desktop.recovery.action.relaunch": "Qayta ishga tushirish", + "desktop.recovery.action.exportLogs": "Jurnallarni eksport qilish", + "desktop.recovery.action.keepWaiting": "Kutishda davom eting", + "desktop.recovery.action.quit": "Chiqish", + "desktop.recovery.loadFailed": "OpenCode yuklanmadi", + "desktop.recovery.terminated": "OpenCode oynasi kutilmaganda tugatildi", + "desktop.recovery.unresponsive": "OpenCode javob bermayapti", + "desktop.recovery.unresponsive.detail": + "Siz ilovani qayta ishga tushirishingiz, jurnallarni ochishingiz yoki kutishda davom etishingiz mumkin.", + "desktop.recovery.loadFailed.detail": "Oyna: {{window}}\nURL: {{url}}\nXato: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Oyna: {{window}}\nSababi: {{reason}}\nKod: {{code}}", + "desktop.recovery.unknown": "", + "desktop.dialog.chooseFolder": "Jildni tanlang", + "desktop.dialog.chooseFile": "Faylni tanlang", + "desktop.dialog.saveFile": "Faylni saqlash", + "desktop.dialog.files": "Fayllar", + "desktop.server.local": "Mahalliy server", + "desktop.wsl.error.windowsOnly": "WSL faqat Windows da mavjud", + "desktop.wsl.error.unavailable": "WSL mavjud emas", + "desktop.wsl.error.listInstalled": "Oʻrnatilgan WSL distrolarini roʻyxatga kiritib boʻlmadi", + "desktop.wsl.error.listOnline": "Onlayn WSL distrolarini roʻyxatga kiritib boʻlmadi", + "desktop.wsl.error.executeDistro": "Distroda buyruqlarni bajarib bo'lmaydi", + "desktop.wsl.error.installWsl": "WSL oʻrnatilmadi", + "desktop.wsl.error.installDistro": "Distroni oʻrnatib boʻlmadi: {{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode oʻrnatilmadi", + "desktop.wsl.error.alreadyAdded": "{{distro}} allaqachon qo'shilgan", + "desktop.wsl.error.opencodeMissing": "Opencode ushbu tarqatishda o'rnatilmagan", + "desktop.wsl.error.opencodeCannotRun": "opencode o'rnatilgan, lekin ishga tushirilmadi", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode {{distro}} da o'rnatilmagan", + "desktop.wsl.error.updateVersion": + "OpenCode yangilanishi tugallandi, lekin {{distro}} hali ham {{installed}} haqida xabar beradi; kutilgan {{expected}}", + "desktop.wsl.error.noVersion": "versiya yo'q", + "desktop.wsl.error.serverExited": "WSL server ishga tushirilgandan so'ng chiqdi (kod = {{code}} signal = {{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL serveri sog'lom bo'lgunga qadar chiqdi (kod={{code}} signali={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "{{distro}} sog'lig'ini tekshirish uchun yon mashina {{timeout}}ms dan keyin tugadi", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} {{timeout}}ms dan keyin vaqt tugadi", + "desktop.wsl.error.failedPort": "Port olinmadi", + "desktop.picker.error.notSelected": "Fayl tanlovchi tomonidan tanlanmagan", + "desktop.picker.error.sizeLimit": "Tanlangan biriktirmalar {{limit}} MB chegarasidan oshib ketadi", + "command.category.suggested": "Tavsiya etilgan", + "command.category.view": "Ko'rish", + "command.category.project": "Loyiha", + "command.category.provider": "Provayder", + "command.category.server": "Server", + "command.category.session": "Sessiya", + "command.category.theme": "Mavzu", + "command.category.language": "Til", + "command.category.file": "Fayl", + "command.category.context": "Kontekst", + "command.category.terminal": "Terminal", + "command.category.model": "Model", + "command.category.mcp": "MCP", + "command.category.agent": "Agent", + "command.category.permissions": "Ruxsatlar", + "command.category.workspace": "Ish maydoni", + "command.category.settings": "Sozlamalar", + "command.logs.export": "Jurnallarni eksport qilish", + "theme.scheme.system": "Tizim", + "theme.scheme.light": "Nur", + "theme.scheme.dark": "Qorong'i", + "command.sidebar.toggle": "Yon panelni almashtirish", + "command.project.open": "Ochiq loyiha", + "command.project.previous": "Oldingi loyiha", + "command.project.next": "Keyingi loyiha", + "command.project.index": "{{index}} loyihasiga o'tish", + "command.provider.connect": "Provayderga ulanish", + "command.server.switch": "Serverni almashtirish", + "command.settings.open": "Sozlamalarni oching", + "command.session.previous": "Oldingi sessiya", + "command.session.next": "Keyingi sessiya", + "command.session.previous.unseen": "Oldingi oʻqilmagan seans", + "command.session.next.unseen": "Keyingi oʻqilmagan seans", + "command.session.archive": "Arxiv seansi", + "command.palette": "Buyruqlar palitrasi", + "command.theme.cycle": "Keyingi mavzu", + "command.theme.set": "Mavzudan foydalaning: {{theme}}", + "command.theme.scheme.cycle": "Tsiklning rang sxemasi", + "command.theme.scheme.set": "Rang sxemasidan foydalaning: {{scheme}}", + "command.language.cycle": "Keyingi til", + "command.language.set": "Tildan foydalaning: {{language}}", + "command.session.new": "Yangi sessiya", + "command.file.open": "Faylni ochish", + "command.tab.close": "Tabni yoping", + "command.tab.reopenClosed": "Yopiq tabni qayta oching", + "command.context.addSelection": "Kontekstga tanlov qo'shing", + "command.context.addSelection.description": "Joriy fayldan tanlangan qatorlarni qo'shing", + "command.input.focus": "Fokus kiritish", + "command.terminal.toggle": "Terminalni almashtirish", + "command.fileTree.toggle": "Fayl daraxtini almashtirish", + "command.review.toggle": "Koʻrib chiqish", + "command.terminal.new": "Yangi terminal", + "command.terminal.new.description": "Yangi terminal yorlig'ini yarating", + "command.steps.toggle": "Qadamlarni almashtirish", + "command.steps.toggle.description": "Joriy xabar uchun qadamlarni ko'rsatish yoki yashirish", + "command.message.previous": "Oldingi xabar", + "command.message.previous.description": "Oldingi foydalanuvchi xabariga o'ting", + "command.message.next": "Keyingi xabar", + "command.message.next.description": "Keyingi foydalanuvchi xabariga o'ting", + "command.model.choose": "Modelni tanlang", + "command.model.choose.description": "Boshqa modelni tanlang", + "command.mcp.toggle": "MCPlarni almashtirish", + "command.mcp.toggle.description": "MCPlarni almashtirish", + "command.agent.cycle": "Keyingi agent", + "command.agent.cycle.description": "Keyingi agentga o'ting", + "command.agent.cycle.reverse": "Oldingi agent", + "command.agent.cycle.reverse.description": "Oldingi agentga o'tish", + "command.model.variant.cycle": "Keyingi fikrlash darajasi", + "command.model.variant.cycle.description": "Keyingi harakat darajasiga o'ting", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Tezkor", + "command.permissions.autoaccept.enable": "Ruxsatlarni avtomatik qabul qilish", + "command.permissions.autoaccept.disable": "Ruxsatlarni avtomatik qabul qilishni to'xtating", + "command.workspace.toggle": "Ish joylarini almashtirish", + "command.workspace.toggle.description": "Yon panelda bir nechta ish joylarini yoqing yoki o'chiring", + "command.session.undo": "Bekor qilish", + "command.session.undo.description": "Oxirgi xabarni bekor qiling", + "command.session.redo": "Qayta qiling", + "command.session.redo.description": "Oxirgi bekor qilingan xabarni takrorlang", + "command.session.compact": "Yilni seans", + "command.session.compact.description": "Kontekst hajmini kamaytirish uchun sessiyani umumlashtiring", + "command.session.fork": "Xabardan vilka", + "command.session.fork.description": "Oldingi xabardan yangi seans yarating", + "command.session.share": "Seansni ulashish", + "command.session.share.description": "Ushbu seansni baham ko'ring va URL-ni vaqtinchalik xotiraga nusxalash", + "command.session.unshare": "Seansni ulashishni bekor qilish", + "command.session.unshare.description": "Ushbu seansni baham ko'rishni to'xtating", + "command.session.export": "Eksport seansi", + "command.session.export.description": "Toʻliq seans transkriptini JSON sifatida eksport qiling", + "palette.search.placeholder": "Fayllar, buyruqlar va seanslarni qidiring", + "palette.search.placeholder.home": "Qidiruv buyruqlar va seanslar", + "palette.empty": "Hech qanday natija topilmadi", + "palette.group.commands": "Buyruqlar", + "palette.group.files": "Fayllar", + "dialog.provider.search.placeholder": "Provayderlarni qidirish", + "dialog.provider.empty": "Hech qanday provayder topilmadi", + "dialog.provider.group.popular": "Mashhur", + "dialog.provider.group.other": "Boshqa", + "dialog.provider.custom.label": "Maxsus OpenAI-mos keladigan provayder", + "dialog.provider.tag.recommended": "Tavsiya etilgan", + "dialog.provider.opencode.note": "Tanlangan modellar, jumladan Claude, GPT, Gemini va boshqalar", + "dialog.provider.opencode.tagline": "Ishonchli optimallashtirilgan modellar", + "dialog.provider.opencodeGo.tagline": "Hamma uchun arzon obuna", + "dialog.provider.anthropic.note": "Claude modellariga, jumladan Pro va Maksga to'g'ridan-to'g'ri kirish", + "dialog.provider.copilot.note": "GitHub Copilot orqali kodlash yordami uchun AI modellari", + "dialog.provider.openai.note": "Tez, qobiliyatli umumiy AI vazifalari uchun GPT modellari", + "dialog.provider.google.note": "Tez, tuzilgan javoblar uchun Gemini modellari", + "dialog.provider.openrouter.note": "Barcha qo'llab-quvvatlanadigan modellarga bitta provayderdan kiring", + "dialog.provider.vercel.note": "Aqlli marshrutlash bilan AI modellariga yagona kirish", + "dialog.model.select.title": "Modelni tanlang", + "dialog.model.search.placeholder": "Modellarni qidirish", + "dialog.model.empty": "Model natijalari yo'q", + "dialog.model.manage": "Modellarni boshqarish", + "dialog.model.manage.description": "Model selektorida qaysi modellar ko'rinishini sozlang.", + "dialog.model.manage.provider.toggle": "Barcha {{provider}} modellarini almashtiring", + "dialog.model.unpaid.freeModels.title": "OpenCode tomonidan taqdim etilgan bepul modellar", + "dialog.model.unpaid.addMore.title": "Mashhur provayderlarning ko'proq modellarini qo'shing", + "dialog.model.unpaid.viewMoreProviders": "Yana 70 dan ortiq provayderlarni koʻring", + "dialog.provider.viewAll": "Ko'proq provayderlarni ko'rsatish", + "provider.connect.title": "{{provider}} ni ulang", + "provider.connect.title.anthropicProMax": "Claude Pro/Maks bilan tizimga kiring", + "provider.connect.selectMethod": "{{provider}} uchun kirish usulini tanlang.", + "provider.connect.method.apiKey": "API kaliti", + "provider.connect.method.browser": "Brauzer", + "provider.connect.method.headless": "Boshsiz", + "provider.connect.status.inProgress": "Avtorizatsiya davom etmoqda...", + "provider.connect.status.waiting": "Avtorizatsiya kutilmoqda...", + "provider.connect.status.failed": "Avtorizatsiya amalga oshmadi: {{error}}", + "provider.connect.apiKey.description": + "Hisobingizni ulash va OpenCode’da {{provider}} modellaridan foydalanish uchun {{provider}} API kalitini kiriting.", + "provider.connect.apiKey.label": "{{provider}} API kaliti", + "provider.connect.apiKey.placeholder": "API kaliti", + "provider.connect.apiKey.required": "API kaliti talab qilinadi", + "provider.connect.opencodeZen.line1": + "OpenCode Zen sizga kodlash agentlari uchun ishonchli optimallashtirilgan modellar to'plamiga kirish imkonini beradi.", + "provider.connect.opencodeZen.line2": + "Bitta API kaliti bilan siz Claude, GPT, Gemini, GLM va boshqalar kabi modellarga kirishingiz mumkin.", + "provider.connect.opencodeZen.visit.prefix": "Tashrif buyuring ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " API kalitingizni olish uchun.", + "provider.connect.oauth.code.visit.prefix": "Tashrif buyuring ", + "provider.connect.oauth.code.visit.link": "bu havola", + "provider.connect.oauth.code.visit.suffix": + " hisobingizni ulash va OpenCode da {{provider}} modellaridan foydalanish uchun avtorizatsiya kodingizni yig'ish.", + "provider.connect.oauth.code.label": "{{method}} avtorizatsiya kodi", + "provider.connect.oauth.code.placeholder": "Avtorizatsiya kodi", + "provider.connect.oauth.code.required": "Avtorizatsiya kodi talab qilinadi", + "provider.connect.oauth.code.invalid": "Avtorizatsiya kodi yaroqsiz", + "provider.connect.oauth.auto.visit.prefix": "Tashrif buyuring ", + "provider.connect.oauth.auto.visit.link": "bu havola", + "provider.connect.oauth.auto.visit.suffix": + " va hisobingizni ulash uchun quyidagi kodni kiriting va OpenCode da {{provider}} modellaridan foydalaning.", + "provider.connect.oauth.auto.confirmationCode": "Tasdiqlash kodi", + "provider.connect.toast.connected.title": "{{provider}} ulandi", + "provider.connect.toast.connected.description": "{{provider}} modellaridan endi foydalanish mumkin.", + "provider.custom.title": "Maxsus provayder", + "provider.custom.unavailable": "Ushbu serverda maxsus provayderlar mavjud emas", + "provider.custom.description.prefix": "OpenAI-mos keladigan provayderni sozlang. ga qarang ", + "provider.custom.description.link": "provayder konfiguratsiyasi hujjatlari", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "Provayder identifikatori", + "provider.custom.field.providerID.placeholder": "provayderim", + "provider.custom.field.providerID.description": "Kichik harflar, raqamlar, tire yoki pastki chiziq", + "provider.custom.field.name.label": "Ko'rsatilgan nom", + "provider.custom.field.name.placeholder": "Mening AI provayderim", + "provider.custom.field.baseURL.label": "Baza URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API kaliti", + "provider.custom.field.apiKey.placeholder": "API kaliti", + "provider.custom.field.apiKey.description": + "Ixtiyoriy. Agar autentifikatsiyani sarlavhalar orqali boshqarsangiz, boʻsh qoldiring.", + "provider.custom.models.label": "Modellar", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model identifikatori", + "provider.custom.models.name.label": "Ism", + "provider.custom.models.name.placeholder": "Ko'rsatilgan ism", + "provider.custom.models.remove": "Modelni olib tashlang", + "provider.custom.models.add": "Model qo'shing", + "provider.custom.headers.label": "Sarlavhalar (ixtiyoriy)", + "provider.custom.headers.key.label": "Sarlavha", + "provider.custom.headers.key.placeholder": "Sarlavha - nomi", + "provider.custom.headers.value.label": "Qiymat", + "provider.custom.headers.value.placeholder": "qiymat", + "provider.custom.headers.remove": "Sarlavhani olib tashlang", + "provider.custom.headers.add": "Sarlavha qo'shing", + "provider.custom.error.providerID.required": "Provayder identifikatori talab qilinadi", + "provider.custom.error.providerID.format": "Kichik harflar, raqamlar, tire yoki pastki chiziqdan foydalaning", + "provider.custom.error.providerID.exists": "Bu provayder identifikatori allaqachon mavjud", + "provider.custom.error.name.required": "Ko'rsatilgan nom talab qilinadi", + "provider.custom.error.baseURL.required": "URL bazasi talab qilinadi", + "provider.custom.error.baseURL.format": "http:// yoki https:// bilan boshlanishi kerak", + "provider.custom.error.required": "Majburiy", + "provider.custom.error.duplicate": "Dublikat", + "provider.disconnect.toast.disconnected.title": "{{provider}} uzildi", + "provider.disconnect.toast.disconnected.description": "{{provider}} modellari endi mavjud emas.", + "model.tag.free": "Bepul", + "model.tag.latest": "Oxirgi", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "matn", + "model.input.image": "tasvir", + "model.input.audio": "audio", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Ruxsat beradi: {{inputs}}", + "model.tooltip.context.label": "Kontekst", + "model.tooltip.inputs": "Kirishlar", + "model.tooltip.model": "Model", + "model.tooltip.provider": "Provayder", + "model.tooltip.reasoning": "Mulohaza yuritish", + "model.tooltip.reasoning.allowed": "Fikrlash imkonini beradi", + "model.tooltip.reasoning.none": "Mantiq yo'q", + "model.tooltip.context": "Kontekst chegarasi {{limit}}", + "common.search.placeholder": "Qidiruv", + "common.goBack": "Orqaga o'tish", + "common.goForward": "Oldinga harakatlanish", + "common.loading": "Yuklanmoqda", + "common.loading.ellipsis": "...", + "common.cancel": "Bekor qilish", + "common.clear": "Toza", + "common.open": "Ochiq", + "common.connect": "Ulanish", + "common.disconnect": "Ulanishni uzing", + "common.continue": "Davom eting", + "common.submit": "Yuborish", + "common.save": "Saqlash", + "common.saving": "Saqlanmoqda...", + "common.default": "Standart", + "common.attachment": "biriktirma", + "prompt.placeholder.shell": "Shell buyrug'ini kiriting... {{example}}", + "prompt.placeholder.normal": 'Har qanday narsani so\'rang... "{{example}}"', + "prompt.placeholder.simple": "Har qanday narsani so'rang ...", + "prompt.placeholder.summarizeComments": "Fikrlarni umumlashtiring…", + "prompt.placeholder.summarizeComment": "Fikrni umumlashtiring…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Tezkor", + "prompt.mode.shell.exit": "chiqish uchun esc", + "session.child.promptDisabled": "Subagent seanslarini so'rash mumkin emas.", + "session.child.backToParent": "Asosiy sessiyaga qaytish.", + "prompt.example.1": "Kod bazasida TODO ni tuzating", + "prompt.example.2": "Ushbu loyihaning texnologik to'plami nima?", + "prompt.example.3": "Buzilgan testlarni tuzatish", + "prompt.example.4": "Autentifikatsiya qanday ishlashini tushuntiring", + "prompt.example.5": "Xavfsizlik zaifliklarini toping va tuzating", + "prompt.example.6": "Foydalanuvchi xizmati uchun birlik testlarini qo'shing", + "prompt.example.7": "Ko'proq o'qilishi uchun ushbu funktsiyani qayta tahrirlang", + "prompt.example.8": "Bu xato nimani anglatadi?", + "prompt.example.9": "Bu muammoni tuzatishga yordam bering", + "prompt.example.10": "API hujjatlarini yarating", + "prompt.example.11": "Ma'lumotlar bazasi so'rovlarini optimallashtirish", + "prompt.example.12": "Kirish tekshiruvini qo'shing", + "prompt.example.13": "Yangi komponent yarating...", + "prompt.example.14": "Ushbu loyihani qanday tarqataman?", + "prompt.example.15": "Eng yaxshi amaliyotlar uchun mening kodimni ko'rib chiqing", + "prompt.example.16": "Ushbu funktsiyaga xatolarni qayta ishlashni qo'shing", + "prompt.example.17": "Ushbu regex naqshini tushuntiring", + "prompt.example.18": "Buni TypeScript-ga aylantiring", + "prompt.example.19": "Butun kod bazasida jurnalni qo'shing", + "prompt.example.20": "Qanday bog'liqliklar eskirgan?", + "prompt.example.21": "Migratsiya skriptini yozishga yordam bering", + "prompt.example.22": "Ushbu oxirgi nuqta uchun keshlashni amalga oshiring", + "prompt.example.23": "Ushbu ro'yxatga sahifalash qo'shing", + "prompt.example.24": "Buning uchun CLI buyrug'ini yarating...", + "prompt.example.25": "Atrof-muhit o'zgaruvchilari bu erda qanday ishlaydi?", + "prompt.popover.emptyResults": "Mos natijalar topilmadi", + "prompt.popover.emptyCommands": "Mos keladigan buyruqlar yo'q", + "prompt.dropzone.label": "Tasvirlar, PDF yoki matnli fayllarni bu yerga tashlang", + "prompt.dropzone.file.label": "@mention fayliga o'tkazing", + "prompt.slash.badge.custom": "odatiy", + "prompt.slash.badge.skill": "mahorat", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "faol", + "prompt.context.includeActiveFile": "Faol faylni qo'shing", + "prompt.context.removeActiveFile": "Kontekstdan faol faylni olib tashlang", + "prompt.context.removeFile": "Faylni kontekstdan olib tashlang", + "prompt.action.attachFile": "Fayllarni qo'shish", + "prompt.menu.addImagesAndFiles": "Fayllar va boshqalarni qo'shing", + "prompt.menu.imagesAndFiles": "Rasmlar va fayllar", + "prompt.menu.commands": "Buyruqlar", + "prompt.menu.context": "Kontekst", + "prompt.menu.shellCommand": "Shell buyrug'i", + "prompt.attachment.remove": "Qo'shimchani olib tashlang", + "prompt.action.send": "Yuborish", + "prompt.action.stop": "To'xtang", + "prompt.toast.pasteUnsupported.title": "Qoʻllab-quvvatlanmaydigan biriktirma", + "prompt.toast.pasteUnsupported.description": "Bu yerda faqat rasmlar, PDF yoki matnli fayllar biriktirilishi mumkin.", + "prompt.toast.attachmentDuplicate.title": "Bu fayl allaqachon yuklangan", + "prompt.toast.modelAgentRequired.title": "Agent va modelni tanlang", + "prompt.toast.modelAgentRequired.description": "So'rov yuborishdan oldin agent va modelni tanlang.", + "prompt.toast.worktreeCreateFailed.title": "Ish daraxtini yaratib boʻlmadi", + "prompt.toast.sessionCreateFailed.title": "Seans yaratib boʻlmadi", + "prompt.toast.shellSendFailed.title": "Shell buyrug'ini yuborib bo'lmadi", + "prompt.toast.commandSendFailed.title": "Buyruq yuborilmadi", + "prompt.toast.promptSendFailed.title": "Soʻrov yuborilmadi", + "prompt.toast.promptSendFailed.description": "Seansni olib boʻlmadi", + "dialog.mcp.title": "MCPlar", + "dialog.mcp.description": "{{enabled}}, {{total}} yoqilgan", + "dialog.mcp.empty": "Hech qanday MCP sozlanmagan", + "dialog.lsp.empty": "LSP'lar fayl turlaridan avtomatik ravishda aniqlanadi", + "dialog.plugins.empty": "opencode.json da sozlangan plaginlar", + "mcp.status.connected": "ulangan", + "mcp.status.failed": "muvaffaqiyatsiz", + "mcp.status.needs_auth": "avtorizatsiya kerak", + "mcp.status.disabled": "nogiron", + "mcp.auth.clickToAuthenticate": "Haqiqiylikni tekshirish uchun bosing", + "dialog.fork.empty": "Hech qanday xabar yo'q", + "dialog.directory.search.placeholder": "Jildlarni qidirish", + "dialog.directory.empty": "Hech qanday jild topilmadi", + "dialog.directory.action.selectFile": "Faylni tanlang", + "dialog.directory.action.selectFolder": "Jildni tanlang", + "dialog.directory.root": "Ildiz", + "dialog.directory.parent": "Ota-ona", + "dialog.directory.readError": "Bu jildni oʻqib boʻlmadi", + "app.server.unreachable": "{{server}} ga etib boʻlmadi", + "app.server.retrying": "Avtomatik qayta urinish...", + "app.server.otherServers": "Boshqa serverlar", + "dialog.server.title": "Serverlar", + "dialog.server.description": "Ushbu ilova qaysi OpenCode serveriga ulanishini almashtiring.", + "dialog.server.search.placeholder": "Serverlarni qidirish", + "dialog.server.empty": "Hozircha serverlar yoʻq", + "dialog.server.add.title": "Server qo'shish", + "dialog.server.add.url": "Server manzili", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Serverga ulanib boʻlmadi", + "dialog.server.add.checking": "Tekshirilmoqda...", + "dialog.server.add.button": "Server qo'shish", + "dialog.server.add.name": "Server nomi (ixtiyoriy)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "Foydalanuvchi nomi (ixtiyoriy)", + "dialog.server.add.usernamePlaceholder": "foydalanuvchi nomi", + "dialog.server.add.password": "Parol (ixtiyoriy)", + "dialog.server.add.passwordPlaceholder": "parol", + "dialog.server.edit.title": "Serverni tahrirlash", + "dialog.server.default.title": "Standart server", + "dialog.server.default.description": + "Mahalliy serverni ishga tushirish oʻrniga ilova ishga tushganda ushbu serverga ulaning. Qayta ishga tushirishni talab qiladi.", + "dialog.server.default.none": "Hech qanday server tanlanmagan", + "dialog.server.default.set": "Joriy serverni standart sifatida o'rnating", + "dialog.server.default.clear": "Tozalash", + "dialog.server.action.remove": "Serverni olib tashlang", + "dialog.server.menu.edit": "Tahrirlash", + "dialog.server.menu.default": "Standart sifatida o'rnating", + "dialog.server.menu.defaultRemove": "Standartni olib tashlang", + "dialog.server.menu.delete": "Oʻchirish", + "dialog.server.current": "Joriy server", + "dialog.server.status.default": "Standart", + "wsl.server.add": "WSL serverini qo'shing", + "wsl.server.addShort": "WSL qo'shing", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL serveri", + "wsl.server.retryStart": "Qaytadan boshlash", + "wsl.server.updating": "Yangilanmoqda...", + "wsl.onboarding.step.distro": "Distroni tanlang", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "WSL tekshirilmoqda...", + "wsl.onboarding.restartRequired": "WSL oʻrnatilishini tugatish uchun Windows qayta ishga tushirilishi kerak.", + "wsl.onboarding.ready": "WSL tayyor.", + "wsl.onboarding.required": "Davom etish uchun WSL talab qilinadi.", + "wsl.onboarding.checkingDistros": "Tarqatishlar tekshirilmoqda...", + "wsl.onboarding.installingDistro": "{{distro}} o'rnatilmoqda...", + "wsl.onboarding.checkingDistro": "{{distro}} tekshirilmoqda...", + "wsl.onboarding.listingDistros": "Tarqatishlar roʻyxati...", + "wsl.onboarding.distroReady": "{{distro}} tayyor.", + "wsl.onboarding.distroNotInstalled": "{{distro}} hali o'rnatilmagan.", + "wsl.onboarding.openDistroOnce": "Oʻrnatishni tugatish uchun {{distro}} ni bir marta oching.", + "wsl.onboarding.finishingDistro": "{{distro}} uchun sozlash tugallanmoqda.", + "wsl.onboarding.pickDistro": "Distroni tanlang yoki quyida o'rnating.", + "wsl.onboarding.checkingOpencode": "OpenCode tekshirilmoqda...", + "wsl.onboarding.checkingOpencodeIn": "OpenCode {{distro}} da tekshirilmoqda...", + "wsl.onboarding.updatingOpencode": "OpenCode yangilanmoqda...", + "wsl.onboarding.updatingOpencodeIn": "OpenCode {{distro}} da yangilanmoqda...", + "wsl.onboarding.updateOpencodeIn": "OpenCode-ni {{distro}}-da yangilang.", + "wsl.onboarding.updateOpencode": "OpenCode-ni yangilang", + "wsl.onboarding.opencodeReadyIn": "OpenCode {{distro}} da tayyor.", + "wsl.onboarding.opencodeReady": "OpenCode tayyor.", + "wsl.onboarding.installOpencodeIn": "OpenCode-ni {{distro}}-ga o'rnating.", + "wsl.onboarding.installOpencode": "OpenCode-ni o'rnating", + "wsl.onboarding.chooseDistroFirst": "Avval distroni tanlang.", + "wsl.onboarding.loadFailed": "WSL holatini yuklab boʻlmadi.", + "wsl.onboarding.loading": "Yuklanmoqda...", + "wsl.onboarding.installedDistros": "O'rnatilgan distroslar", + "wsl.onboarding.checkAgain": "Yana tekshiring", + "wsl.onboarding.distroStatus.ready": "Tayyor", + "wsl.onboarding.distroStatus.checking": "Tekshirilmoqda...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode o'rnatilmagan", + "wsl.onboarding.distroStatus.missingTools": "Bash, curl etishmayapti", + "wsl.onboarding.distroStatus.unsupported": "Qo'llab-quvvatlanmaydi · WSL 2 dan foydalaning", + "wsl.onboarding.needAnotherDistro": "Boshqa tarqatish kerakmi?", + "wsl.onboarding.needAnotherDistroHint": "WSL katalogidan Linux distributivini o'rnating", + "wsl.onboarding.wslNotInstalled.title": "WSL o'rnatilmagan", + "wsl.onboarding.wslNotInstalled.description": + "OpenCode WSL serverini qoʻshishdan oldin WSL (Linux uchun Windows quyi tizimi) talab qilinadi.", + "wsl.onboarding.wslUnavailable.title": "WSL mavjud emas", + "wsl.onboarding.wslUnavailable.description": "OpenCode WSL ni ushbu mashinada tasdiqlay olmadi.", + "wsl.onboarding.installWsl": "WSL-ni o'rnating", + "wsl.onboarding.windowsRestartRequired": + "WSL-ni o'rnatishni tugatish uchun Windows-ni qayta ishga tushiring, so'ngra OpenCode-ni qayta oching.", + "wsl.onboarding.next": "Keyingi", + "wsl.onboarding.refresh": "Yangilash", + "wsl.onboarding.allDistrosAdded": "Barcha o'rnatilgan tarqatishlar allaqachon qo'shilgan.", + "wsl.onboarding.noDistros": "Hali hech qanday tarqatish aniqlanmadi.", + "wsl.onboarding.install": "O'rnatish", + "wsl.onboarding.installing": "Oʻrnatilmoqda...", + "wsl.onboarding.installDistro": "Distroni o'rnating", + "wsl.onboarding.searchDistros": "Distroslarni qidirish", + "wsl.onboarding.wsl2Required": "WSL 2 talab qilinadi.", + "wsl.onboarding.toolsRequired": "Bu distroga bash va curl kerak.", + "wsl.onboarding.openTerminal": "Ochiq terminal", + "wsl.onboarding.path": "Yo'l: {{path}}", + "wsl.onboarding.notFound": "topilmadi", + "wsl.onboarding.version": "Versiya: {{version}}", + "wsl.onboarding.unknown": "noma'lum", + "wsl.onboarding.desktopVersion": "ish stoli {{version}}", + "wsl.onboarding.versionMismatch": "Oʻrnatilgan versiya ish stoli ilovasi versiyasiga mos kelmaydi.", + "wsl.onboarding.adding": "Qo'shilmoqda...", + "help.tabs.toast.ariaLabel": "Tablar bilan tanishtirish. Yorliqlar bilan ish va faol seanslaringizni tashkil qiling", + "help.tabs.toast.dismiss": "Yorliqlar ma'lumotlarini o'chirish", + "help.tabs.title": "Tablar bilan tanishtirish", + "help.tabs.description": "Yorliqlar bilan ish va faol seanslaringizni tashkil qiling", + "help.tabs.date": "14 iyul", + "help.tabs.introduction": "OpenCode Desktop endi yorliqlar atrofida qurilgan.", + "help.tabs.sessions": + "Yorliqda yangi seansni boshlang yoki loyihalaringizdan mavjud sessiyani oching. Yangi ishni boshlaganingizda yangi varaqni oching va ish tugagach, uni yoping.", + "help.tabs.organize": + "Bir nechta yorliqlarni ochiq saqlash sizning faol seanslaringizni tashkil qilishni osonlashtiradi. Yorliqlar nomlarini esda qolarli qilib o'zgartiring, agar siz ularni atrofingizda saqlamoqchi bo'lsangiz.", + "help.tabs.home": + "Siz barcha seanslaringiz va loyihalaringizni yangi Home ekranida topasiz. Seansni tanlash uni yorliqda ochadi.", + "help.tabs.persistence": "Ilovani qayta ochganingizda, yorliqlaringiz hali ham ochiq.", + "help.tabs.worktrees": + "Yangi dizayn hali Git Worktrees-ni qo'llab-quvvatlamaydi, u tez orada chiqadi. Shunday qilib, agar siz avvalgi tartibdan foydalanishni davom ettirmoqchi boʻlsangiz, Sozlamalar orqali tartiblar oʻrtasida almashishingiz mumkin. Shuni yodda tutingki, yangi tartib bir necha hafta ichida doimiy bo'ladi.", + "server.row.noUsername": "foydalanuvchi nomi yo'q", + "dialog.project.edit.title": "Loyihani tahrirlash", + "dialog.project.edit.name": "Ism", + "dialog.project.edit.icon": "Belgi", + "dialog.project.edit.icon.alt": "Loyiha belgisi", + "dialog.project.edit.icon.hint": "Rasmni bosing yoki torting", + "dialog.project.edit.icon.recommended": "Tavsiya etilgan: 128x128px", + "dialog.project.edit.color": "Rang", + "dialog.project.edit.color.select": "{{color}} rangini tanlang", + "dialog.project.edit.worktree.startup": "Ish maydonini ishga tushirish skripti", + "dialog.project.edit.worktree.startup.description": "Yangi ish maydoni (ishchi daraxt) yaratilgandan keyin ishlaydi.", + "dialog.project.edit.worktree.startup.placeholder": "masalan bun install", + "dialog.releaseNotes.action.getStarted": "Boshlash", + "dialog.releaseNotes.action.next": "Keyingi", + "dialog.releaseNotes.action.hideFuture": "Kelajakda bularni ko'rsatmang", + "dialog.releaseNotes.media.alt": "Chiqarishni oldindan ko'rish", + "context.breakdown.title": "Kontekstni taqsimlash", + "context.breakdown.note": + "Kirish belgilarining taxminiy taqsimoti. \"Boshqa\" asboblar ta'riflari va qo'shimcha xarajatlarni o'z ichiga oladi.", + "context.breakdown.system": "Tizim", + "context.breakdown.user": "Foydalanuvchi", + "context.breakdown.assistant": "Yordamchi", + "context.breakdown.tool": "Asbob qo'ng'iroqlari", + "context.breakdown.other": "Boshqa", + "context.systemPrompt.title": "Tizim so'rovi", + "context.rawMessages.title": "Xom xabarlar", + "context.export.session": "Eksport seansi", + "context.stats.session": "Sessiya", + "context.stats.messages": "Xabarlar", + "context.stats.provider": "Provayder", + "context.stats.model": "Model", + "context.stats.limit": "Kontekst chegarasi", + "context.stats.totalTokens": "Jami tokenlar", + "context.stats.usage": "Foydalanish", + "context.stats.inputTokens": "Kirish tokenlari", + "context.stats.outputTokens": "Chiqish tokenlari", + "context.stats.reasoningTokens": "Fikrlash belgilari", + "context.stats.cacheTokens": "Kesh tokenlari (o'qish/yozish)", + "context.stats.userMessages": "Foydalanuvchi xabarlari", + "context.stats.assistantMessages": "Yordamchi xabarlari", + "context.stats.totalCost": "Umumiy xarajat", + "context.stats.sessionCreated": "Sessiya yaratildi", + "context.stats.lastActivity": "Oxirgi faoliyat", + "context.usage.tokens": "Tokenlar", + "context.usage.usage": "Foydalanish", + "context.usage.cost": "Narxi", + "context.usage.clickToView": "Kontekstni ko'rish uchun bosing", + "context.usage.view": "Kontekstdan foydalanishni ko'rish", + "language.en": "Ingliz", + "language.zh": "shīngīshīn", + "language.zht": "línìnìnì", + "language.ko": "|", + "language.de": "nemis", + "language.es": "ispan", + "language.fr": "Fransiya", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Russkiy", + "language.ar": "ạlʿrbyẗ", + "language.no": "Norsk", + "language.br": "Português (Braziliya)", + "language.bs": "Bosanski", + "language.uk": "Ukraínska", + "language.th": "ไทย", + "language.tr": "o'zbekcha", + "toast.language.title": "Til", + "toast.language.description": "{{language}} ga almashtirildi", + "toast.theme.title": "Mavzu almashtirildi", + "toast.scheme.title": "Rang sxemasi", + "toast.workspace.enabled.title": "Ish joylari yoqilgan", + "toast.workspace.enabled.description": "Endi yon panelda bir nechta ishchi daraxtlar ko'rsatilgan", + "toast.workspace.disabled.title": "Ish joylari oʻchirilgan", + "toast.workspace.disabled.description": "Yon panelda faqat asosiy ishchi daraxt ko'rsatilgan", + "toast.permissions.autoaccept.on.title": "Ruxsatlarni avtomatik qabul qilish", + "toast.permissions.autoaccept.on.description": "Ruxsat so'rovlari avtomatik ravishda tasdiqlanadi", + "toast.permissions.autoaccept.off.title": "Ruxsatlarni avtomatik qabul qilish toʻxtatildi", + "toast.permissions.autoaccept.off.description": "Ruxsat so'rovlari tasdiqlashni talab qiladi", + "toast.model.none.title": "Hech qanday model tanlanmagan", + "toast.model.none.description": "Ushbu seansni yakunlash uchun provayderni ulang", + "toast.file.loadFailed.title": "Faylni yuklab boʻlmadi", + "toast.file.listFailed.title": "Fayllarni roʻyxatga kiritib boʻlmadi", + "toast.context.noLineSelection.title": "Qator tanlovi yo'q", + "toast.context.noLineSelection.description": "Avval fayl yorlig'ida qator oralig'ini tanlang.", + "toast.session.share.copyFailed.title": "URLni vaqtinchalik xotiraga nusxalab boʻlmadi", + "toast.session.share.success.title": "Seans ulashildi", + "toast.session.share.success.description": "Buferga nusxalangan URL bilan baham ko'ring!", + "toast.session.share.failed.title": "Seansni ulashib boʻlmadi", + "toast.session.share.failed.description": "Seansni ulashishda xatolik yuz berdi", + "toast.session.unshare.success.title": "Seans ulashilmadi", + "toast.session.unshare.success.description": "Seans muvaffaqiyatli bekor qilindi!", + "toast.session.unshare.failed.title": "Seansni taqsimlab boʻlmadi", + "toast.session.unshare.failed.description": "Seansni taqsimlashda xatolik yuz berdi", + "toast.session.export.success.title": "Seans eksport qilindi", + "toast.session.export.success.description": "Seans {{filename}}ga saqlandi", + "toast.session.export.failed.title": "Seansni eksport qilib boʻlmadi", + "toast.session.export.failed.description": "Seansni eksport qilishda xatolik yuz berdi", + "toast.session.listFailed.title": "{{project}} uchun seanslar yuklanmadi", + "toast.project.reloadFailed.title": "{{project}} qayta yuklanmadi", + "toast.update.title": "Yangilanish mavjud", + "toast.update.description": "OpenCode ({{version}}) yangi versiyasi endi oʻrnatish mumkin.", + "toast.update.action.installRestart": "O'rnating va qayta ishga tushiring", + "toast.update.action.notYet": "Hali emas", + "error.page.title": "Nimadir xato ketdi", + "error.page.description": "Ilovani yuklashda xatolik yuz berdi.", + "error.page.description.localServerStartup": "Mahalliy serverni ishga tushirishda xatolik yuz berdi.", + "error.page.details.label": "Xato tafsilotlari", + "error.page.action.restart": "Qayta ishga tushirish", + "error.page.action.report": "Xato haqida xabar berish", + "error.page.action.reported": "Xato haqida xabar berildi", + "error.page.action.exportLogs": "Jurnallarni eksport qilish", + "error.page.action.checking": "Tekshirilmoqda...", + "error.page.action.checkUpdates": "Yangilanishlarni tekshiring", + "error.page.action.updateTo": "{{version}} ga yangilang", + "error.page.circular": "[Diraviy]", + "error.page.report.prefix": "Iltimos, ushbu xato haqida OpenCode jamoasiga xabar bering", + "error.page.report.discord": "Discord-da", + "error.page.version": "Versiya: {{version}}", + "error.dev.rootNotFound": + "Ildiz element topilmadi. Uni index.html-ga qo'shishni unutdingizmi? Yoki id atributi noto'g'ri yozilgandir?", + "error.serverSync.connectFailed": "Serverga ulanib boʻlmadi. `{{url}}` da ishlaydigan server bormi?", + "error.serverSDK.noServerAvailable": "Hech qanday server mavjud emas", + "error.serverSDK.serverNotAvailable": "Server mavjud emas", + "error.childStore.persistedCacheCreateFailed": "Doimiy kesh yaratib bo'lmadi", + "error.childStore.persistedProjectMetadataCreateFailed": "Doimiy loyiha metamaʼlumotlarini yaratib boʻlmadi", + "error.childStore.persistedProjectIconCreateFailed": "Doimiy loyiha belgisini yaratib boʻlmadi", + "error.childStore.storeCreateFailed": "Doʻkon yaratib boʻlmadi", + "directory.error.invalidUrl": "URL da noto'g'ri katalog.", + "error.chain.unknown": "Noma'lum xato", + "error.server.invalidConfiguration": "Yaroqsiz konfiguratsiya", + "error.chain.causedBy": "Sabab:", + "error.chain.apiError": "API xatosi", + "error.chain.status": "Holati: {{status}}", + "error.chain.retryable": "Qayta urinish mumkin: {{retryable}}", + "error.chain.responseBody": "Javob organi:\n{{body}}", + "error.chain.didYouMean": "Buni nazarda tutdingizmi: {{suggestions}}", + "error.chain.modelNotFound": "Model topilmadi: {{provider}}/{{model}}", + "error.chain.checkConfig": "Konfiguratsiya (opencode.json) provayder/model nomlarini tekshiring", + "error.chain.mcpFailed": + "MCP \"{{name}}\" serveri muvaffaqiyatsiz tugadi. E'tibor bering, OpenCode hali MCP autentifikatsiyasini qo'llab-quvvatlamaydi.", + "error.chain.providerAuthFailed": "Provayder autentifikatsiyasi amalga oshmadi ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + '"{{provider}}" provayderini ishga tushirib boʻlmadi. Hisob ma\'lumotlari va konfiguratsiyani tekshiring.', + "error.chain.configJsonInvalid": "{{path}} da konfiguratsiya fayli yaroqsiz JSON(C)", + "error.chain.configJsonInvalidWithMessage": "{{path}} da konfiguratsiya fayli yaroqsiz JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} da "{{dir}}" katalogi yaroqsiz. Katalog nomini "{{suggestion}}" ga o\'zgartiring yoki uni olib tashlang. Bu keng tarqalgan yozuv xatosi.', + "error.chain.configFrontmatterError": "{{path}} da oldingi materialni tahlil qilib boʻlmadi:\n{{message}}", + "error.chain.configInvalid": "{{path}} da konfiguratsiya fayli yaroqsiz", + "error.chain.configInvalidWithMessage": "{{path}} da konfiguratsiya fayli notoʻgʻri: {{message}}", + "notification.permission.title": "Ruxsat talab qilinadi", + "notification.permission.description": "{{sessionTitle}} uchun {{projectName}} uchun ruxsat kerak", + "notification.question.title": "Savol", + "notification.question.description": "{{projectName}} da {{sessionTitle}} savol bor", + "notification.action.goToSession": "Seansga o'ting", + "notification.session.responseReady.title": "Javob tayyor", + "notification.session.error.title": "Seans xatosi", + "notification.session.error.fallbackDescription": "Xatolik yuz berdi", + "home.recentProjects": "So'nggi loyihalar", + "home.empty.title": "Yaqinda loyihalar yo'q", + "home.empty.description": "Mahalliy loyihani ochishdan boshlang", + "home.title": "Bosh sahifa", + "home.projects": "Loyihalar", + "home.project.add": "Loyiha qo'shish", + "home.recentlyClosed": "Yaqinda yopildi", + "home.server.collapse": "Server loyihalarini yig'ish", + "home.server.expand": "Server loyihalarini kengaytirish", + "home.sessions.search.placeholder": "Qidiruv seanslari", + "home.sessions.search.placeholder.scoped": "{{scope}} da seanslarni qidirish", + "home.sessions.search.sessions": "Seanslar", + "home.sessions.search.noResults": "{{query}} uchun seanslar topilmadi", + "home.sessions.empty": "Bu yerda hali hech narsa", + "home.sessions.empty.description": "Boshlash uchun sessiya yarating", + "home.sessions.group.today": "Bugun", + "home.sessions.group.yesterday": "Kecha", + "home.sessions.group.older": "Kattaroq", + "home.providerTip": + "Claude, GPT, Gemini va boshqalar kabi boshqa modellardan foydalanish uchun 75 dan ortiq provayderlarga ulaning.", + "session.tab.session": "Sessiya", + "session.tab.review": "Koʻrib chiqish", + "session.tab.context": "Kontekst", + "session.tab.unknown": "Noma'lum sessiya", + "session.panel.reviewAndFiles": "Ko'rib chiqish va fayllar", + "session.error.notFound": "Bu sessiya topilmadi", + "session.error.notFound.description": "Ushbu yorliq endi ushbu serverda mavjud bo'lmagan seansga ishora qiladi.", + "session.error.notFound.closeTab": "Tab ni yoping", + "session.error.serverConnection": "Bu serverga ulanib boʻlmadi", + "session.review.filesChanged": "Fayllar o'zgartirildi {{count}}", + "session.review.change.one": "O'zgartirish", + "session.review.change.other": "O'zgarishlar", + "session.review.loadingChanges": "Oʻzgarishlar yuklanmoqda...", + "session.review.empty": "Bu sessiyada hali oʻzgarishlar yoʻq", + "session.review.noVcs": "Git versiyasini boshqarish tizimi aniqlanmadi, o'zgarishlar ko'rsatilmadi", + "session.review.noVcs.createGit.title": "Git omborini yarating", + "session.review.noVcs.createGit.description": + "Ushbu loyihadagi oʻzgarishlarni kuzating, koʻrib chiqing va bekor qiling", + "session.review.noVcs.createGit.actionLoading": "Git ombori yaratilmoqda...", + "session.review.noVcs.createGit.action": "Git omborini yarating", + "session.review.noSnapshot": + "Snapshot kuzatish konfiguratsiyada oʻchirilgan, shuning uchun sessiya oʻzgarishlari mavjud emas", + "session.review.noChanges": "Oʻzgarishlar yoʻq", + "session.review.noUncommittedChanges": "Hali tasdiqlanmagan o'zgarishlar yo'q", + "session.review.noBranchChanges": "Hozircha filial o'zgarmadi", + "session.files.selectToOpen": "Ochish uchun faylni tanlang", + "session.files.all": "Barcha fayllar", + "session.files.empty": "Fayl yo'q", + "session.files.binaryContent": "Ikkilik fayl (tarkibni ko'rsatib bo'lmaydi)", + "session.messages.renderEarlier": "Oldingi xabarlarni ko'rsatish", + "session.messages.loadingEarlier": "Avvalgi xabarlar yuklanmoqda...", + "session.messages.loadEarlier": "Avvalgi xabarlarni yuklang", + "session.messages.loading": "Xabarlar yuklanmoqda...", + "session.messages.jumpToLatest": "Eng oxirgisiga oʻtish", + "session.context.addToContext": "Kontekstga {{selection}} qo'shing", + "session.todo.title": "Vazifalar", + "session.todo.collapse": "Yiqilish", + "session.todo.expand": "Kengaytirish", + "session.todo.progress": "{{done}} {{total}} todos yakunlandi", + "session.question.progress": "{{total}} savollarining {{current}}", + "session.question.minimize": "Savolni minimallashtirish", + "session.question.restore": "Savolni tiklash", + "session.question.pending.one": "{{count}} kutilayotgan savol", + "session.question.pending.other": "{{count}} kutilayotgan savollar", + "session.followupDock.summary.one": "{{count}} navbatdagi xabar", + "session.followupDock.summary.other": "{{count}} navbatdagi xabarlar", + "session.followupDock.sendNow": "Hozir yuboring", + "session.followupDock.edit": "Tahrirlash", + "session.followupDock.collapse": "Navbatdagi xabarlarni yig'ish", + "session.followupDock.expand": "Navbatdagi xabarlarni yoyish", + "session.revertDock.summary.one": "{{count}} xabarni orqaga qaytardi", + "session.revertDock.summary.other": "{{count}} xabarlarni orqaga qaytardi", + "session.revertDock.collapse": "Orqaga qaytarilgan xabarlarni yig'ish", + "session.revertDock.expand": "Orqaga qaytarilgan xabarlarni yoyish", + "session.revertDock.restore": "Xabarni tiklash", + "session.new.title": "Har qanday narsani qurish", + "session.new.project.new": "Yangi loyiha", + "session.new.project.search": "Loyihalarni qidirish", + "session.new.project.add": "Loyiha qo'shish", + "session.new.worktree.main": "Asosiy filial", + "session.new.worktree.mainWithBranch": "Asosiy filial ({{branch}})", + "session.new.worktree.create": "Yangi ishchi daraxt yarating", + "session.new.workspace.runIn": "Seansni ishga tushirish", + "session.new.workspace.triggerLocal": "Mahalliy", + "session.new.workspace.local": "Mahalliy ombor", + "session.new.workspace.existing": "Ish maydoni…", + "session.new.git.none": "Git yo'q", + "session.new.lastModified": "Oxirgi tahrirlangan", + "session.header.search.placeholder": "Qidiruv {{project}}", + "session.header.searchFiles": "Fayllarni qidirish", + "session.header.openIn": "Ochish", + "session.header.open.action": "{{app}}-ni oching", + "session.header.open.ariaLabel": "{{app}} da oching", + "session.header.open.menu": "Ochiq variantlar", + "session.header.open.copyPath": "Yo'lni nusxalash", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "Fayl kuzatuvchisi", + "session.header.open.fileManager": "Fayl menejeri", + "session.header.reveal.finder": "Finder da oshkor qiling", + "session.header.reveal.fileExplorer": "File Explorer da oshkor qiling", + "session.header.reveal.containingFolder": "O'z ichiga olgan jildni oching", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Holat", + "status.popover.ariaLabel": "Server konfiguratsiyasi", + "status.popover.tab.servers": "Serverlar", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plaginlar", + "status.popover.action.manageServers": "Serverlarni boshqarish", + "session.share.popover.title": "Internetda nashr qilish", + "session.share.popover.description.shared": + "Bu seans internetda ochiq. U havolaga ega bo'lgan har bir kishi uchun ochiq.", + "session.share.popover.description.unshared": + "Seansni internetda hammaga ochiq baham ko'ring. U havolaga ega bo'lgan har bir kishi uchun ochiq bo'ladi.", + "session.share.action.share": "Ulashish", + "session.share.action.publish": "Nashr qilish", + "session.share.action.publishing": "Chop etilmoqda...", + "session.share.action.unpublish": "Nashrni bekor qilish", + "session.share.action.unpublishing": "Nashr bekor qilinmoqda...", + "session.share.action.view": "Ko'rish", + "session.share.copy.copied": "Koʻchirildi", + "session.share.copy.copyLink": "Havolani nusxalash", + "lsp.tooltip.none": "LSP serverlari yo'q", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Tezkor yuklanmoqda...", + "terminal.loading": "Terminal yuklanmoqda...", + "terminal.title": "Terminal", + "terminal.title.numbered": "{{number}} terminali", + "terminal.close": "Terminalni yoping", + "terminal.connectionLost.title": "Ulanish uzildi", + "terminal.connectionLost.abnormalClose": "WebSocket anormal tarzda yopildi: {{code}}", + "terminal.connectionLost.description": + "Terminal aloqasi uzildi. Bu server qayta ishga tushirilganda sodir bo'lishi mumkin.", + "terminal.connectTicket.csrfError": + "PTY ulanish chiptasi kelib chiqishi yoki CSRF tekshiruvlari tomonidan rad etilgan. CORS server konfiguratsiyasini tekshiring.", + "terminal.connectTicket.statusError": "PTY ulanish chiptasi {{status}} bilan amalga oshmadi", + "titlebar.update": "Yangilash", + "titlebar.updateVersion": "{{version}}-ni yangilang", + "common.closeTab": "Tabni yoping", + "common.dismiss": "Rad etish", + "common.moreCountSuffix": " (+{{count}} ko'proq)", + "common.requestFailed": "Soʻrov bajarilmadi", + "common.moreOptions": "Boshqa variantlar", + "common.learnMore": "Batafsil maʼlumot", + "common.rename": "Nomini o'zgartirish", + "common.export": "Eksport", + "common.reset": "Qayta tiklash", + "common.archive": "Arxiv", + "common.delete": "Oʻchirish", + "common.close": "Yopish", + "common.edit": "Tahrirlash", + "common.loadMore": "Ko'proq yuklash", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Space", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "noma'lum", + "common.time.justNow": "Hozir", + "common.time.minutesAgo.short": "{{count}}m oldin", + "common.time.hoursAgo.short": "{{count}}h oldin", + "common.time.daysAgo.short": "{{count}}d oldin", + "sidebar.menu.toggle": "Menyuni almashtirish", + "sidebar.nav.projectsAndSessions": "Loyihalar va sessiyalar", + "sidebar.settings": "Sozlamalar", + "sidebar.help": "Yordam", + "sidebar.workspaces.enable": "Ish joylarini yoqish", + "sidebar.workspaces.disable": "Ish joylarini o'chirib qo'ying", + "sidebar.gettingStarted.title": "Boshlanmoqda", + "sidebar.gettingStarted.line1": + "OpenCode bepul modellarni o'z ichiga oladi, shuning uchun siz darhol boshlashingiz mumkin.", + "sidebar.gettingStarted.line2": + "Modellardan foydalanish uchun har qanday provayderni ulang, inc. Claude, GPT, Gemini va boshqalar.", + "sidebar.project.recentSessions": "Oxirgi sessiyalar", + "sidebar.project.viewAllSessions": "Barcha seanslarni ko'rish", + "sidebar.project.clearNotifications": "Bildirishnomalarni tozalash", + "sidebar.empty.title": "Hech qanday loyiha ochiq emas", + "sidebar.empty.description": "Boshlash uchun loyihani oching", + "debugBar.ariaLabel": "Rivojlanish samaradorligi diagnostikasi", + "debugBar.na": "yo'q", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Seans sahifasiga tegib oxirgi tugallangan marshrutga o'tish, marshrutizator boshlanishidan boshlab u o'rnatgandan keyingi birinchi bo'yoqgacha o'lchanadi.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Oxirgi 5 soniya ichida sekundiga aylanayotgan kadrlar.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Oxirgi 5 soniya ichida eng yomon kadr vaqti.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Oxirgi 5 soniyada 32 ms dan ortiq kadrlar.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Bloklangan vaqt va oxirgi 5 soniyadagi uzoq topshiriqlar soni. Maksimal vazifa: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Oxirgi 5 soniya ichida kuzatilgan eng yomon kiritish kechikishi.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Oxirgi 5 soniya davomida o'zaro ta'sir qilishning taxminiy davomiyligi. Bu INP-ga o'xshaydi, rasmiy Web Vitals INP emas.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Joriy ilovaning ishlash muddati uchun joylashuvning umumiy siljishi.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Ishlatilgan JS to'plami va to'p chegarasi. Faqat Chromium.", + "debugBar.mem.tip": "Ishlatilgan JS to'plami va to'p chegarasi. {{used}}, {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Fokus uslublarini barcha interaktiv elementlarga majburlash", + "debugBar.focus.on": "ON", + "debugBar.focus.off": "OFF", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Tanlangan tilni oʻzgartirmasdan ilovaning toʻliq joylashuvi yoʻnalishini majburlang", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Ish stoli", + "settings.section.server": "Server", + "settings.tab.general": "Umumiy", + "settings.tab.shortcuts": "Yorliqlar", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL integratsiyasi", + "settings.desktop.wsl.description": "Windows da WSL ichidagi OpenCode serverini ishga tushiring.", + "settings.general.section.appearance": "Tashqi ko'rinish", + "settings.general.section.advanced": "Murakkab", + "settings.general.section.notifications": "Tizim bildirishnomalari", + "settings.general.section.updates": "Yangilanishlar", + "settings.general.section.sounds": "Ovoz effektlari", + "settings.general.section.feed": "Oziqlantirish", + "settings.general.section.display": "Displey", + "settings.general.row.language.title": "Til", + "settings.general.row.language.description": "OpenCode uchun ekran tilini o'zgartiring", + "settings.general.row.shell.title": "Terminal qobig'i", + "settings.general.row.shell.description": "Terminal va agent vositalari tomonidan ishlatiladigan qobiq", + "settings.general.row.shell.autoDefault": "Avtomatik (standart)", + "settings.general.row.shell.terminalOnly": "faqat terminal", + "settings.general.row.appearance.title": "Tashqi ko'rinish", + "settings.general.row.appearance.description": "OpenCode qurilmangizda qanday koʻrinishini sozlang", + "settings.general.row.colorScheme.title": "Rang sxemasi", + "settings.general.row.colorScheme.description": + "OpenCode tizimga, yorugʻlik yoki qorongʻi mavzuga amal qilishini tanlang", + "settings.general.row.theme.title": "Mavzu", + "settings.general.row.theme.description": "OpenCode mavzusini sozlash.", + "settings.general.row.font.title": "Kod shrifti", + "settings.general.row.font.description": "Kod bloklarida ishlatiladigan shriftni moslashtiring", + "settings.general.row.terminalFont.title": "Terminal shrifti", + "settings.general.row.terminalFont.description": "Terminalda ishlatiladigan shriftni sozlang", + "settings.general.row.uiFont.title": "UI shrifti", + "settings.general.row.uiFont.description": "Interfeys davomida ishlatiladigan shriftni moslashtiring", + "settings.general.row.followup.title": "Kuzatuv xatti-harakati", + "settings.general.row.followup.description": + "Kuzatuv soʻrovlari darhol boshqarilishini yoki navbatda kutishini tanlang", + "settings.general.row.followup.option.queue": "Navbat", + "settings.general.row.followup.option.steer": "Boshqarmoq", + "settings.general.row.showFileTree.title": "Fayl daraxti", + "settings.general.row.showFileTree.description": "Seanslarda fayl daraxti panelini ko'rsatish", + "settings.general.row.showNavigation.title": "Navigatsiya boshqaruvlari", + "settings.general.row.showNavigation.description": + "Ish stolining sarlavha satrida orqaga va oldinga tugmalarini ko'rsating", + "settings.general.row.showSearch.title": "Buyruqlar palitrasi", + "settings.general.row.showSearch.description": + "Sarlavha satrida qidirish va buyruqlar palitrasi tugmasini ko'rsating", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Ish stolining sarlavha satrida terminal tugmachasini ko'rsating", + "settings.general.row.showStatus.title": "Server holati", + "settings.general.row.showStatus.description": "Sarlavha satrida server holati tugmasini ko'rsating", + "settings.general.row.mobileTitlebarBottom.title": "Pastki navigatsiya", + "settings.general.row.mobileTitlebarBottom.description": + "Sarlavha satrini va seans yorliqlarini mobil telefonda ekranning pastki qismiga joylashtiring", + "settings.general.row.showCustomAgents.title": "Agentni ko'rsatish", + "settings.general.row.showCustomAgents.description": + "Kompozitorda agentlar o'rtasida almashish. Yashirin bo'lsa, birlamchi Build agenti bo'ladi.", + "settings.general.row.reasoningSummaries.title": "Fikrlash xulosalarini ko'rsating", + "settings.general.row.reasoningSummaries.description": "Vaqt jadvalida model asoslarini ko'rsatish", + "settings.general.row.shellToolPartsExpanded.title": "Qobiq asboblari qismlarini kengaytiring", + "settings.general.row.shellToolPartsExpanded.description": + "Xronologiyada sukut bo'yicha kengaytirilgan qobiq asboblari qismlarini ko'rsatish", + "settings.general.row.editToolPartsExpanded.title": "Tahrirlash vositasi qismlarini kengaytiring", + "settings.general.row.editToolPartsExpanded.description": + "Vaqt jadvalida sukut boʻyicha kengaytirilgan tahrirlash, yozish va tuzatish vositalari qismlarini koʻrsatish", + "settings.general.row.newInterface.title": "Yangi tartib", + "settings.general.row.newInterface.badge": "Yangi", + "settings.general.row.newInterface.description": + "Yangi yorliqlar va uy tartibidan foydalaning. Cheklangan vaqt uchun tartiblar o'rtasida almashinish.", + "settings.general.row.newInterfaceNotice.title": "Siz hozir yangi tartibdan foydalanmoqdasiz", + "settings.general.row.newInterfaceNotice.description": "Oldingi tartib endi mavjud emas", + "settings.general.row.newInterfaceNotice.dismiss": "Rad etish", + "settings.general.row.pinchZoom.title": "Kattalashtirish uchun chimchilang", + "settings.general.row.pinchZoom.description": + "Kattalashtirish uchun trekpadni chimchilash va Ctrl-aylantirish imo-ishoralariga ruxsat bering", + "settings.general.row.wayland.title": "Mahalliy Wayland dan foydalaning", + "settings.general.row.wayland.description": + "Wayland da X11 zaxirasini o'chirib qo'ying. Qayta ishga tushirishni talab qiladi.", + "settings.general.row.wayland.tooltip": + "Aralashtirilgan yangilanish tezligi monitorlari bilan Linux da mahalliy Wayland barqarorroq bo'lishi mumkin.", + "settings.general.row.releaseNotes.title": "Chiqarish qaydlari", + "settings.general.row.releaseNotes.description": + "Yangilanishlardan so'ng nimalar yangi qalqib chiquvchi oynalarni ko'rsating", + "settings.updates.row.startup.title": "Ishga tushganda yangilanishlarni tekshiring", + "settings.updates.row.startup.description": "OpenCode ishga tushganda avtomatik ravishda yangilanishlarni tekshiring", + "settings.updates.row.check.title": "Yangilanishlarni tekshiring", + "settings.updates.row.check.description": "Yangilanishlarni qo'lda tekshiring va mavjud bo'lsa o'rnating", + "settings.updates.action.checkNow": "Hozir tekshiring", + "settings.updates.action.checking": "Tekshirilmoqda...", + "settings.updates.action.downloading": "Yuklab olinmoqda...", + "settings.updates.action.installing": "Oʻrnatilmoqda...", + "settings.updates.toast.latest.title": "Siz yangilangansiz", + "settings.updates.toast.latest.description": "Siz OpenCode oxirgi versiyasini ishga tushiryapsiz.", + "sound.option.none": "Yo'q", + "sound.option.alert01": "Ogohlantirish 01", + "sound.option.alert02": "Ogohlantirish 02", + "sound.option.alert03": "Ogohlantirish 03", + "sound.option.alert04": "Ogohlantirish 04", + "sound.option.alert05": "Ogohlantirish 05", + "sound.option.alert06": "Ogohlantirish 06", + "sound.option.alert07": "Ogohlantirish 07", + "sound.option.alert08": "Ogohlantirish 08", + "sound.option.alert09": "Ogohlantirish 09", + "sound.option.alert10": "Ogohlantirish 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Yo'q 01", + "sound.option.nope02": "Yo'q 02", + "sound.option.nope03": "Yo'q 03", + "sound.option.nope04": "Yo'q 04", + "sound.option.nope05": "Yo'q 05", + "sound.option.nope06": "Yo'q 06", + "sound.option.nope07": "Yo'q 07", + "sound.option.nope08": "Yo'q 08", + "sound.option.nope09": "Yo'q 09", + "sound.option.nope10": "Yo'q 10", + "sound.option.nope11": "Yo'q 11", + "sound.option.nope12": "Yo'q 12", + "sound.option.yup01": "Ha 01", + "sound.option.yup02": "Ha 02", + "sound.option.yup03": "Ha 03", + "sound.option.yup04": "Ha 04", + "sound.option.yup05": "Ha 05", + "sound.option.yup06": "Ha 06", + "settings.general.notifications.agent.title": "Agent", + "settings.general.notifications.agent.description": + "Agent tugallanganda yoki unga e'tibor kerak bo'lganda tizim bildirishnomasini ko'rsatish", + "settings.general.notifications.permissions.title": "Ruxsatlar", + "settings.general.notifications.permissions.description": "Ruxsat kerak bo'lganda tizim bildirishnomasini ko'rsatish", + "settings.general.notifications.errors.title": "Xatolar", + "settings.general.notifications.errors.description": "Xatolik yuz berganda tizim bildirishnomasini ko'rsatish", + "settings.general.sounds.agent.title": "Agent", + "settings.general.sounds.agent.description": "Agent tugallanganda yoki e'tiborga muhtoj bo'lganida ovozni ijro eting", + "settings.general.sounds.permissions.title": "Ruxsatlar", + "settings.general.sounds.permissions.description": "Ruxsat kerak bo'lganda ovozni ijro etish", + "settings.general.sounds.errors.title": "Xatolar", + "settings.general.sounds.errors.description": "Xatolik yuz berganda ovozni ijro eting", + "settings.shortcuts.title": "Klaviatura yorliqlari", + "settings.shortcuts.reset.button": "Standart sozlamalarni tiklash", + "settings.shortcuts.reset.toast.title": "Yorliqlar qayta tiklandi", + "settings.shortcuts.reset.toast.description": "Klaviatura yorliqlari standart sozlamalarga qaytarildi.", + "settings.shortcuts.conflict.title": "Yorliq allaqachon ishlatilmoqda", + "settings.shortcuts.conflict.description": "{{keybind}} allaqachon {{titles}} ga tayinlangan.", + "settings.shortcuts.unassigned": "Tayinlanmagan", + "settings.shortcuts.pressKeys": "Tugmalarni bosing", + "settings.shortcuts.search.placeholder": "Yorliqlarni qidirish", + "settings.shortcuts.search.empty": "Hech qanday yorliq topilmadi", + "settings.shortcuts.group.general": "Umumiy", + "settings.shortcuts.group.session": "Sessiya", + "settings.shortcuts.group.navigation": "Navigatsiya", + "settings.shortcuts.group.modelAndAgent": "Model va agent", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Tezkor", + "settings.providers.title": "Provayderlar", + "settings.providers.description": "Bu yerda provayder sozlamalari sozlanishi mumkin.", + "settings.providers.section.connected": "Ulangan provayderlar", + "settings.providers.connected.empty": "Ulangan provayderlar yo'q", + "settings.providers.connected.environmentDescription": "Atrof-muhit o'zgaruvchilaringizdan ulangan", + "settings.providers.section.popular": "Mashhur provayderlar", + "settings.providers.custom.description": "URL bazasi bo'yicha OpenAI-mos provayderni qo'shing.", + "settings.providers.tag.environment": "Atrof-muhit", + "settings.providers.tag.config": "Konfiguratsiya", + "settings.providers.tag.custom": "Maxsus", + "settings.providers.tag.other": "Boshqa", + "settings.models.title": "Modellar", + "settings.models.description": "Model sozlamalari bu erda sozlanishi mumkin.", + "settings.agents.title": "Agentlar", + "settings.agents.description": "Bu yerda agent sozlamalari sozlanishi mumkin.", + "settings.commands.title": "Buyruqlar", + "settings.commands.description": "Buyruq sozlamalari bu yerda sozlanishi mumkin.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP sozlamalari bu yerda sozlanishi mumkin.", + "settings.permissions.title": "Ruxsatlar", + "settings.permissions.description": "Server sukut bo'yicha qanday vositalardan foydalanishi mumkinligini boshqaring.", + "settings.permissions.section.tools": "Asboblar", + "settings.permissions.toast.updateFailed.title": "Ruxsatlarni yangilab boʻlmadi", + "settings.permissions.action.allow": "Ruxsat bering", + "settings.permissions.action.ask": "So'rang", + "settings.permissions.action.deny": "Rad etish", + "settings.permissions.tool.read.title": "O'qing", + "settings.permissions.tool.read.description": "Faylni o'qish (fayl yo'liga mos keladi)", + "settings.permissions.tool.edit.title": "Tahrirlash", + "settings.permissions.tool.edit.description": "Fayllarni o'zgartirish, jumladan tahrirlash, yozish va tuzatishlar", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Glob naqshlari yordamida fayllarni moslashtiring", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Oddiy iboralar yordamida fayl tarkibini qidiring", + "settings.permissions.tool.list.title": "Roʻyxat", + "settings.permissions.tool.list.description": "Katalog ichidagi fayllarni ro'yxatlash", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Shell buyruqlarini ishga tushiring", + "settings.permissions.tool.task.title": "Vazifa", + "settings.permissions.tool.task.description": "Sub-agentlarni ishga tushiring", + "settings.permissions.tool.skill.title": "Malaka", + "settings.permissions.tool.skill.description": "Nomi bo'yicha mahorat yuklang", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Til serveri so'rovlarini ishga tushirish", + "settings.permissions.tool.todowrite.title": "Todo Write", + "settings.permissions.tool.todowrite.description": "Ishlar ro'yxatini yangilang", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "URL dan kontentni olish", + "settings.permissions.tool.websearch.title": "Veb-qidiruv", + "settings.permissions.tool.websearch.description": "Internetda qidiring", + "settings.permissions.tool.external_directory.title": "Tashqi katalog", + "settings.permissions.tool.external_directory.description": "Loyiha katalogidan tashqaridagi fayllarga kirish", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": + "Bir xil kirish bilan takrorlangan asboblar qo'ng'iroqlarini aniqlang", + "session.delete.failed.title": "Seansni oʻchirib boʻlmadi", + "session.delete.title": "Seansni o'chirish", + "session.delete.confirm": '"{{name}}" seansi o‘chirilsinmi?', + "session.delete.button": "Seansni o'chirish", + "workspace.new": "Yangi ish maydoni", + "workspace.type.local": "mahalliy", + "workspace.type.sandbox": "qum qutisi", + "workspace.create.failed.title": "Ish maydoni yaratib boʻlmadi", + "workspace.delete.failed.title": "Ish joyini oʻchirib boʻlmadi", + "workspace.resetting.title": "Ish joyini tiklash", + "workspace.resetting.description": "Bu bir daqiqa vaqt olishi mumkin.", + "workspace.reset.failed.title": "Ish joyini qayta oʻrnatib boʻlmadi", + "workspace.reset.success.title": "Ish joyini tiklash", + "workspace.reset.success.description": "Ish maydoni endi standart filialga mos keladi.", + "workspace.error.stillPreparing": "Ish maydoni hali ham tayyorlanmoqda", + "workspace.status.checking": "Birlashtirilmagan o'zgarishlar tekshirilmoqda...", + "workspace.status.error": "Git holatini tekshirib boʻlmadi.", + "workspace.status.clean": "Birlashtirilmagan o'zgarishlar aniqlanmadi.", + "workspace.status.dirty": "Ushbu ish maydonida birlashtirilmagan o'zgarishlar aniqlandi.", + "workspace.delete.title": "Ish maydonini o'chirish", + "workspace.delete.confirm": '"{{name}}" ish maydoni o‘chirilsinmi?', + "workspace.delete.button": "Ish maydonini o'chirish", + "workspace.reset.title": "Ish joyini tiklash", + "workspace.reset.confirm": '"{{name}}" ish maydoni qayta tiklansinmi?', + "workspace.reset.button": "Ish joyini tiklash", + "workspace.reset.archived.none": "Hech qanday faol seans arxivlanmaydi.", + "workspace.reset.archived.one": "1 seans arxivlanadi.", + "workspace.reset.archived.many": "{{count}} seanslari arxivlanadi.", + "workspace.reset.note": "Bu standart filialga mos keladigan ish maydonini tiklaydi.", + "dialog.usageExceeded.dontShowAgain": "Boshqa ko‘rsatma", +} diff --git a/packages/app/src/i18n/vi.ts b/packages/app/src/i18n/vi.ts new file mode 100644 index 0000000000000000000000000000000000000000..06fda4ce707c1c7ee31650edc787c3c7049462d1 --- /dev/null +++ b/packages/app/src/i18n/vi.ts @@ -0,0 +1,1169 @@ +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "Tệp", + "desktop.menu.edit": "Chỉnh sửa", + "desktop.menu.view": "Xem", + "desktop.menu.go": "Đi", + "desktop.menu.window": "Cửa sổ", + "desktop.menu.help": "Trợ giúp", + "desktop.menu.checkForUpdates": "Kiểm tra bản cập nhật...", + "desktop.menu.settings": "Cài đặt", + "desktop.menu.reloadWebview": "Tải lại Webview", + "desktop.menu.restart": "Khởi động lại", + "desktop.menu.exportLogs": "Xuất nhật ký...", + "desktop.menu.newSession": "Phiên mới", + "desktop.menu.openProject": "Mở dự án...", + "desktop.menu.newWindow": "Cửa sổ mới", + "desktop.menu.closeWindow": "Đóng cửa sổ", + "desktop.menu.undo": "Hoàn tác", + "desktop.menu.redo": "Làm lại", + "desktop.menu.cut": "Cắt", + "desktop.menu.copy": "Sao chép", + "desktop.menu.paste": "Dán", + "desktop.menu.delete": "Xóa", + "desktop.menu.selectAll": "Chọn tất cả", + "desktop.menu.toggleSidebar": "Bật/tắt thanh bên", + "desktop.menu.toggleTerminal": "Bật/tắt terminal", + "desktop.menu.toggleFileTree": "Bật/tắt cây tệp", + "desktop.menu.reload": "Tải lại", + "desktop.menu.toggleDeveloperTools": "Bật/tắt công cụ dành cho nhà phát triển", + "desktop.menu.actualSize": "Kích thước thực", + "desktop.menu.zoomIn": "Phóng to", + "desktop.menu.zoomOut": "Thu nhỏ", + "desktop.menu.toggleFullScreen": "Bật/tắt toàn màn hình", + "desktop.menu.back": "Quay lại", + "desktop.menu.forward": "Tiến tới", + "desktop.menu.previousSession": "Phiên trước", + "desktop.menu.nextSession": "Phiên tiếp theo", + "desktop.menu.previousProject": "Dự án trước", + "desktop.menu.nextProject": "Dự án tiếp theo", + "desktop.menu.minimize": "Thu nhỏ", + "desktop.menu.maximize": "Phóng to", + "desktop.menu.documentation": "Tài liệu OpenCode", + "desktop.menu.supportForum": "Diễn đàn hỗ trợ", + "desktop.menu.shareFeedback": "Chia sẻ phản hồi", + "desktop.menu.reportBug": "Báo cáo lỗi", + "desktop.menu.ariaLabel": "Menu OpenCode", + + "desktop.updater.dialog.checkFailed.message": "Không kiểm tra được bản cập nhật.", + "desktop.updater.dialog.checkFailed.title": "Lỗi cập nhật", + "desktop.updater.dialog.upToDate.message": "Bạn đang dùng phiên bản mới nhất.", + "desktop.updater.dialog.upToDate.title": "Không có bản cập nhật", + "desktop.updater.dialog.ready.message": "Đã tải xuống bản cập nhật {{version}}. Khởi động lại ngay?", + "desktop.updater.dialog.ready.title": "Bản cập nhật đã sẵn sàng", + "desktop.updater.dialog.restart": "Khởi động lại", + "desktop.updater.dialog.later": "Để sau", + + "desktop.recovery.action.relaunch": "Khởi chạy lại", + "desktop.recovery.action.exportLogs": "Xuất nhật ký", + "desktop.recovery.action.keepWaiting": "Tiếp tục chờ", + "desktop.recovery.action.quit": "Thoát", + "desktop.recovery.loadFailed": "Không tải được OpenCode", + "desktop.recovery.terminated": "Cửa sổ OpenCode đã bị đóng đột ngột", + "desktop.recovery.unresponsive": "OpenCode không phản hồi", + "desktop.recovery.unresponsive.detail": "Bạn có thể khởi chạy lại ứng dụng, mở nhật ký hoặc tiếp tục chờ.", + "desktop.recovery.loadFailed.detail": "Cửa sổ: {{window}}\nURL: {{url}}\nLỗi: {{code}} {{description}}", + "desktop.recovery.terminated.detail": "Cửa sổ: {{window}}\nLý do: {{reason}}\nMã: {{code}}", + "desktop.recovery.unknown": "", + + "desktop.dialog.chooseFolder": "Chọn thư mục", + "desktop.dialog.chooseFile": "Chọn tệp", + "desktop.dialog.saveFile": "Lưu tệp", + "desktop.dialog.files": "Tệp", + "desktop.server.local": "Máy chủ cục bộ", + + "desktop.wsl.error.windowsOnly": "WSL chỉ khả dụng trên Windows", + "desktop.wsl.error.unavailable": "WSL không khả dụng", + "desktop.wsl.error.listInstalled": "Không liệt kê được các bản phân phối WSL đã cài đặt", + "desktop.wsl.error.listOnline": "Không liệt kê được các bản phân phối WSL trực tuyến", + "desktop.wsl.error.executeDistro": "Không thể thực thi lệnh trong bản phân phối", + "desktop.wsl.error.installWsl": "Không cài đặt được WSL", + "desktop.wsl.error.installDistro": "Không cài đặt được bản phân phối: {{distro}}", + "desktop.wsl.error.installOpencode": "Không cài đặt được OpenCode", + "desktop.wsl.error.alreadyAdded": "{{distro}} đã được thêm", + "desktop.wsl.error.opencodeMissing": "opencode chưa được cài đặt trong bản phân phối này", + "desktop.wsl.error.opencodeCannotRun": "opencode đã được cài đặt nhưng không thể chạy", + "desktop.wsl.error.opencodeNotInstalled": "OpenCode chưa được cài đặt trong {{distro}}", + "desktop.wsl.error.updateVersion": + "Đã cập nhật OpenCode nhưng {{distro}} vẫn báo phiên bản {{installed}}; phiên bản dự kiến là {{expected}}", + "desktop.wsl.error.noVersion": "không có phiên bản", + "desktop.wsl.error.serverExited": "Máy chủ WSL đã thoát sau khi khởi động (mã={{code}} tín hiệu={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "Máy chủ WSL đã thoát trước khi sẵn sàng (mã={{code}} tín hiệu={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": + "Sidecar kiểm tra tình trạng của {{distro}} đã hết thời gian chờ sau {{timeout}}ms", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} đã hết thời gian chờ sau {{timeout}}ms", + "desktop.wsl.error.failedPort": "Không lấy được cổng", + + "desktop.picker.error.notSelected": "Không có tệp nào được chọn trong bộ chọn tệp", + "desktop.picker.error.sizeLimit": "Các tệp đính kèm đã chọn vượt quá giới hạn {{limit}} MB", + + "command.category.suggested": "Đề xuất", + "command.category.view": "Xem", + "command.category.project": "Dự án", + "command.category.provider": "Nhà cung cấp", + "command.category.server": "Máy chủ", + "command.category.session": "Phiên", + "command.category.theme": "Chủ đề", + "command.category.language": "Ngôn ngữ", + "command.category.file": "Tệp", + "command.category.context": "Ngữ cảnh", + "command.category.terminal": "Terminal", + "command.category.model": "Mô hình", + "command.category.mcp": "MCP", + "command.category.agent": "Tác nhân", + "command.category.permissions": "Quyền", + "command.category.workspace": "Không gian làm việc", + "command.category.settings": "Cài đặt", + "command.logs.export": "Xuất nhật ký", + "theme.scheme.system": "Hệ thống", + "theme.scheme.light": "Sáng", + "theme.scheme.dark": "Tối", + "command.sidebar.toggle": "Bật/tắt thanh bên", + "command.project.open": "Mở dự án", + "command.project.previous": "Dự án trước đó", + "command.project.next": "Dự án tiếp theo", + "command.project.index": "Chuyển sang dự án {{index}}", + "command.provider.connect": "Kết nối nhà cung cấp", + "command.server.switch": "Chuyển máy chủ", + "command.settings.open": "Mở cài đặt", + "command.session.previous": "Phiên trước", + "command.session.next": "Phiên tiếp theo", + "command.session.previous.unseen": "Phiên chưa đọc trước đó", + "command.session.next.unseen": "Phiên chưa đọc tiếp theo", + "command.session.archive": "Lưu trữ phiên", + "command.palette": "Bảng lệnh", + "command.theme.cycle": "Chuyển chủ đề", + "command.theme.set": "Sử dụng chủ đề: {{theme}}", + "command.theme.scheme.cycle": "Chuyển chế độ màu", + "command.theme.scheme.set": "Sử dụng bảng màu: {{scheme}}", + "command.language.cycle": "Chuyển ngôn ngữ", + "command.language.set": "Sử dụng ngôn ngữ: {{language}}", + "command.session.new": "Phiên mới", + "command.file.open": "Mở tệp", + "command.tab.close": "Đóng tab", + "command.tab.reopenClosed": "Mở lại tab đã đóng", + "command.context.addSelection": "Thêm lựa chọn vào ngữ cảnh", + "command.context.addSelection.description": "Thêm các dòng đã chọn từ tệp hiện tại", + "command.input.focus": "Tập trung vào ô nhập", + "command.terminal.toggle": "Bật/tắt terminal", + "command.fileTree.toggle": "Bật/tắt cây tệp", + "command.review.toggle": "Bật/tắt phần xem lại", + "command.terminal.new": "Terminal mới", + "command.terminal.new.description": "Tạo một tab terminal mới", + "command.steps.toggle": "Bật/tắt các bước", + "command.steps.toggle.description": "Hiển thị hoặc ẩn các bước cho tin nhắn hiện tại", + "command.message.previous": "Tin nhắn trước đó", + "command.message.previous.description": "Đi tới tin nhắn người dùng trước đó", + "command.message.next": "Tin nhắn tiếp theo", + "command.message.next.description": "Chuyển tới tin nhắn người dùng tiếp theo", + "command.model.choose": "Chọn mô hình", + "command.model.choose.description": "Chọn một mô hình khác", + "command.mcp.toggle": "Bật/tắt MCP", + "command.mcp.toggle.description": "Bật hoặc tắt MCP", + "command.agent.cycle": "Chuyển tác nhân", + "command.agent.cycle.description": "Chuyển sang tác nhân tiếp theo", + "command.agent.cycle.reverse": "Chuyển tác nhân theo chiều ngược", + "command.agent.cycle.reverse.description": "Chuyển sang tác nhân trước đó", + "command.model.variant.cycle": "Chuyển mức độ suy luận", + "command.model.variant.cycle.description": "Chuyển sang mức nỗ lực tiếp theo", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Lời nhắc", + "command.permissions.autoaccept.enable": "Tự động chấp nhận quyền", + "command.permissions.autoaccept.disable": "Dừng tự động chấp nhận quyền", + "command.workspace.toggle": "Bật/tắt không gian làm việc", + "command.workspace.toggle.description": "Bật hoặc tắt nhiều không gian làm việc trong thanh bên", + "command.session.undo": "Hoàn tác", + "command.session.undo.description": "Hoàn tác tin nhắn cuối cùng", + "command.session.redo": "Làm lại", + "command.session.redo.description": "Làm lại tin nhắn đã hoàn tác gần đây nhất", + "command.session.compact": "Nén phiên", + "command.session.compact.description": "Tóm tắt phiên để giảm kích thước ngữ cảnh", + "command.session.fork": "Tách nhánh từ tin nhắn", + "command.session.fork.description": "Tạo phiên mới từ tin nhắn trước đó", + "command.session.share": "Chia sẻ phiên", + "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", + "palette.group.commands": "Lệnh", + "palette.group.files": "Tệp", + "dialog.provider.search.placeholder": "Tìm kiếm nhà cung cấp", + "dialog.provider.empty": "Không tìm thấy nhà cung cấp nào", + "dialog.provider.group.popular": "Phổ biến", + "dialog.provider.group.other": "Khác", + "dialog.provider.custom.label": "Nhà cung cấp tương thích OpenAI tùy chỉnh", + "dialog.provider.tag.recommended": "Đề xuất", + "dialog.provider.opencode.note": "Các mô hình được tuyển chọn, gồm Claude, GPT, Gemini và nhiều mô hình khác", + "dialog.provider.opencode.tagline": "Các mô hình tối ưu hóa đáng tin cậy", + "dialog.provider.opencodeGo.tagline": "Đăng ký chi phí thấp cho mọi người", + "dialog.provider.anthropic.note": "Truy cập trực tiếp các mô hình Claude, bao gồm Pro và Max", + "dialog.provider.copilot.note": "Các mô hình AI hỗ trợ lập trình qua GitHub Copilot", + "dialog.provider.openai.note": "Các mô hình GPT nhanh và mạnh mẽ cho tác vụ AI đa dụng", + "dialog.provider.google.note": "Các mô hình Gemini cho phản hồi nhanh, có cấu trúc", + "dialog.provider.openrouter.note": "Truy cập tất cả các mô hình được hỗ trợ từ một nhà cung cấp", + "dialog.provider.vercel.note": "Truy cập thống nhất vào các mô hình AI với tính năng định tuyến thông minh", + "dialog.model.select.title": "Chọn mô hình", + "dialog.model.search.placeholder": "Tìm kiếm mô hình", + "dialog.model.empty": "Không có kết quả mô hình", + "dialog.model.manage": "Quản lý mô hình", + "dialog.model.manage.description": "Tùy chỉnh mô hình nào xuất hiện trong bộ chọn mô hình.", + "dialog.model.manage.provider.toggle": "Bật/tắt tất cả mô hình {{provider}}", + "dialog.model.unpaid.freeModels.title": "Các mô hình miễn phí do OpenCode cung cấp", + "dialog.model.unpaid.addMore.title": "Thêm nhiều mô hình từ các nhà cung cấp phổ biến", + "dialog.model.unpaid.viewMoreProviders": "Xem hơn 70 nhà cung cấp khác", + "dialog.provider.viewAll": "Hiển thị thêm nhà cung cấp", + "provider.connect.title": "Kết nối {{provider}}", + "provider.connect.title.anthropicProMax": "Đăng nhập bằng Claude Pro/Max", + "provider.connect.selectMethod": "Chọn phương thức đăng nhập cho {{provider}}.", + "provider.connect.method.apiKey": "Khóa API", + "provider.connect.method.browser": "Trình duyệt", + "provider.connect.method.headless": "Không có giao diện người dùng", + "provider.connect.status.inProgress": "Đang ủy quyền...", + "provider.connect.status.waiting": "Đang chờ ủy quyền...", + "provider.connect.status.failed": "Ủy quyền thất bại: {{error}}", + "provider.connect.apiKey.description": + "Nhập khóa API {{provider}} để kết nối tài khoản và sử dụng các mô hình {{provider}} trong OpenCode.", + "provider.connect.apiKey.label": "Khóa API {{provider}}", + "provider.connect.apiKey.placeholder": "khóa API", + "provider.connect.apiKey.required": "Cần có khóa API", + "provider.connect.opencodeZen.line1": + "OpenCode Zen cho phép bạn truy cập một bộ mô hình được tuyển chọn, tối ưu hóa và đáng tin cậy dành cho tác nhân lập trình.", + "provider.connect.opencodeZen.line2": + "Chỉ với một khóa API, bạn sẽ có quyền truy cập vào các mô hình như Claude, GPT, Gemini, GLM và hơn thế nữa.", + "provider.connect.opencodeZen.visit.prefix": "Truy cập ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " để lấy khóa API của bạn.", + "provider.connect.oauth.code.visit.prefix": "Truy cập ", + "provider.connect.oauth.code.visit.link": "liên kết này", + "provider.connect.oauth.code.visit.suffix": + " để lấy mã ủy quyền, kết nối tài khoản và sử dụng các mô hình {{provider}} trong OpenCode.", + "provider.connect.oauth.code.label": "Mã ủy quyền {{method}}", + "provider.connect.oauth.code.placeholder": "Mã ủy quyền", + "provider.connect.oauth.code.required": "Cần có mã ủy quyền", + "provider.connect.oauth.code.invalid": "Mã ủy quyền không hợp lệ", + "provider.connect.oauth.auto.visit.prefix": "Truy cập ", + "provider.connect.oauth.auto.visit.link": "liên kết này", + "provider.connect.oauth.auto.visit.suffix": + " và nhập mã bên dưới để kết nối tài khoản của bạn và sử dụng các mô hình {{provider}} trong OpenCode.", + "provider.connect.oauth.auto.confirmationCode": "Mã xác nhận", + "provider.connect.toast.connected.title": "{{provider}} đã kết nối", + "provider.connect.toast.connected.description": "Các mô hình {{provider}} hiện đã sẵn sàng để sử dụng.", + "provider.custom.title": "Nhà cung cấp tùy chỉnh", + "provider.custom.unavailable": "Nhà cung cấp tùy chỉnh không khả dụng trên máy chủ này", + "provider.custom.description.prefix": "Định cấu hình nhà cung cấp tương thích OpenAI. Xem ", + "provider.custom.description.link": "tài liệu cấu hình nhà cung cấp", + "provider.custom.description.suffix": ".", + "provider.custom.field.providerID.label": "ID nhà cung cấp", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "Chữ thường, số, dấu gạch nối hoặc dấu gạch dưới", + "provider.custom.field.name.label": "Tên hiển thị", + "provider.custom.field.name.placeholder": "Nhà cung cấp AI của tôi", + "provider.custom.field.baseURL.label": "URL cơ sở", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "Khóa API", + "provider.custom.field.apiKey.placeholder": "khóa API", + "provider.custom.field.apiKey.description": "Không bắt buộc. Để trống nếu bạn quản lý xác thực thông qua tiêu đề.", + "provider.custom.models.label": "Mô hình", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "Tên", + "provider.custom.models.name.placeholder": "Tên hiển thị", + "provider.custom.models.remove": "Xóa mô hình", + "provider.custom.models.add": "Thêm mô hình", + "provider.custom.headers.label": "Header (tùy chọn)", + "provider.custom.headers.key.label": "Header", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "Giá trị", + "provider.custom.headers.value.placeholder": "giá trị", + "provider.custom.headers.remove": "Xóa header", + "provider.custom.headers.add": "Thêm header", + "provider.custom.error.providerID.required": "Bắt buộc phải có ID nhà cung cấp", + "provider.custom.error.providerID.format": "Sử dụng chữ cái viết thường, số, dấu gạch nối hoặc dấu gạch dưới", + "provider.custom.error.providerID.exists": "ID nhà cung cấp này đã tồn tại", + "provider.custom.error.name.required": "Tên hiển thị là bắt buộc", + "provider.custom.error.baseURL.required": "Bắt buộc phải có URL cơ sở", + "provider.custom.error.baseURL.format": "Phải bắt đầu bằng http:// hoặc https://", + "provider.custom.error.required": "Bắt buộc", + "provider.custom.error.duplicate": "Trùng lặp", + "provider.disconnect.toast.disconnected.title": "{{provider}} bị ngắt kết nối", + "provider.disconnect.toast.disconnected.description": "Các mô hình {{provider}} không còn khả dụng.", + "model.tag.free": "Miễn phí", + "model.tag.latest": "Mới nhất", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "văn bản", + "model.input.image": "hình ảnh", + "model.input.audio": "âm thanh", + "model.input.video": "video", + "model.input.pdf": "pdf", + "model.tooltip.allows": "Cho phép: {{inputs}}", + "model.tooltip.context.label": "Ngữ cảnh", + "model.tooltip.inputs": "Đầu vào", + "model.tooltip.model": "Mô hình", + "model.tooltip.provider": "Nhà cung cấp", + "model.tooltip.reasoning": "Lý luận", + "model.tooltip.reasoning.allowed": "Cho phép suy luận", + "model.tooltip.reasoning.none": "Không hỗ trợ suy luận", + "model.tooltip.context": "Giới hạn ngữ cảnh {{limit}}", + "common.search.placeholder": "Tìm kiếm", + "common.goBack": "Quay lại", + "common.goForward": "Tiến tới", + "common.loading": "Đang tải", + "common.loading.ellipsis": "...", + "common.cancel": "Hủy", + "common.clear": "Xóa", + "common.open": "Mở", + "common.connect": "Kết nối", + "common.disconnect": "Ngắt kết nối", + "common.continue": "Tiếp tục", + "common.submit": "Gửi", + "common.save": "Lưu", + "common.saving": "Đang lưu...", + "common.default": "Mặc định", + "common.attachment": "tệp đính kèm", + "prompt.placeholder.shell": "Nhập lệnh shell... {{example}}", + "prompt.placeholder.normal": 'Hỏi bất cứ điều gì... "{{example}}"', + "prompt.placeholder.simple": "Hỏi bất cứ điều gì...", + "prompt.placeholder.summarizeComments": "Tóm tắt các bình luận…", + "prompt.placeholder.summarizeComment": "Tóm tắt bình luận…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Lời nhắc", + "prompt.mode.shell.exit": "esc để thoát", + "session.child.promptDisabled": "Không thể gửi lời nhắc trong phiên tác nhân phụ.", + "session.child.backToParent": "Quay lại phiên chính.", + "prompt.example.1": "Sửa TODO trong cơ sở mã", + "prompt.example.2": "Ngăn xếp công nghệ của dự án này là gì?", + "prompt.example.3": "Sửa các bài kiểm tra bị hỏng", + "prompt.example.4": "Giải thích cách xác thực hoạt động", + "prompt.example.5": "Tìm và sửa các lỗ hổng bảo mật", + "prompt.example.6": "Thêm bài kiểm tra đơn vị cho dịch vụ người dùng", + "prompt.example.7": "Tái cấu trúc hàm này để dễ đọc hơn", + "prompt.example.8": "Lỗi này có nghĩa là gì?", + "prompt.example.9": "Giúp tôi gỡ lỗi vấn đề này", + "prompt.example.10": "Tạo tài liệu API", + "prompt.example.11": "Tối ưu hóa truy vấn cơ sở dữ liệu", + "prompt.example.12": "Thêm xác thực đầu vào", + "prompt.example.13": "Tạo một thành phần mới cho...", + "prompt.example.14": "Làm cách nào để triển khai dự án này?", + "prompt.example.15": "Xem lại mã của tôi để biết các phương pháp hay nhất", + "prompt.example.16": "Thêm xử lý lỗi vào hàm này", + "prompt.example.17": "Giải thích mẫu biểu thức chính quy này", + "prompt.example.18": "Chuyển đổi cái này thành TypeScript", + "prompt.example.19": "Thêm ghi nhật ký trong toàn bộ cơ sở mã", + "prompt.example.20": "Những phụ thuộc nào đã lỗi thời?", + "prompt.example.21": "Giúp tôi viết kịch bản di chuyển", + "prompt.example.22": "Triển khai bộ nhớ đệm cho điểm cuối này", + "prompt.example.23": "Thêm phân trang vào danh sách này", + "prompt.example.24": "Tạo lệnh CLI cho...", + "prompt.example.25": "Các biến môi trường hoạt động như thế nào ở đây?", + "prompt.popover.emptyResults": "Không có kết quả phù hợp", + "prompt.popover.emptyCommands": "Không có lệnh phù hợp", + "prompt.dropzone.label": "Thả hình ảnh, tệp PDF hoặc tệp văn bản vào đây", + "prompt.dropzone.file.label": "Thả để @mention tệp", + "prompt.slash.badge.custom": "tùy chỉnh", + "prompt.slash.badge.skill": "kỹ năng", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "hiện hoạt", + "prompt.context.includeActiveFile": "Bao gồm tệp hiện hoạt", + "prompt.context.removeActiveFile": "Xóa tệp hiện hoạt khỏi ngữ cảnh", + "prompt.context.removeFile": "Xóa tệp khỏi ngữ cảnh", + "prompt.action.attachFile": "Thêm tệp", + "prompt.menu.addImagesAndFiles": "Thêm tệp và nội dung khác", + "prompt.menu.imagesAndFiles": "Hình ảnh và tệp", + "prompt.menu.commands": "Lệnh", + "prompt.menu.context": "Ngữ cảnh", + "prompt.menu.shellCommand": "Lệnh shell", + "prompt.attachment.remove": "Xóa tệp đính kèm", + "prompt.action.send": "Gửi", + "prompt.action.stop": "Dừng", + "prompt.toast.pasteUnsupported.title": "Tệp đính kèm không được hỗ trợ", + "prompt.toast.pasteUnsupported.description": "Chỉ có thể đính kèm hình ảnh, tệp PDF hoặc tệp văn bản ở đây.", + "prompt.toast.attachmentDuplicate.title": "Tệp này đã được tải lên", + "prompt.toast.modelAgentRequired.title": "Chọn tác nhân và mô hình", + "prompt.toast.modelAgentRequired.description": "Chọn một tác nhân và mô hình trước khi gửi lời nhắc.", + "prompt.toast.worktreeCreateFailed.title": "Không tạo được worktree", + "prompt.toast.sessionCreateFailed.title": "Không tạo được phiên", + "prompt.toast.shellSendFailed.title": "Không gửi được lệnh shell", + "prompt.toast.commandSendFailed.title": "Không gửi được lệnh", + "prompt.toast.promptSendFailed.title": "Không gửi được lời nhắc", + "prompt.toast.promptSendFailed.description": "Không thể truy xuất phiên", + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "{{enabled}} trong số {{total}} đã bật", + "dialog.mcp.empty": "Không có MCP nào được định cấu hình", + "dialog.lsp.empty": "LSP được tự động phát hiện từ các loại tệp", + "dialog.plugins.empty": "Các plugin được định cấu hình trong opencode.json", + "mcp.status.connected": "đã kết nối", + "mcp.status.failed": "thất bại", + "mcp.status.needs_auth": "cần xác thực", + "mcp.status.disabled": "đã tắt", + "mcp.auth.clickToAuthenticate": "Bấm để xác thực", + "dialog.fork.empty": "Không có tin nhắn nào để phân nhánh", + "dialog.directory.search.placeholder": "Tìm kiếm thư mục", + "dialog.directory.empty": "Không tìm thấy thư mục nào", + "dialog.directory.action.selectFile": "Chọn tệp", + "dialog.directory.action.selectFolder": "Chọn thư mục", + "dialog.directory.root": "Gốc", + "dialog.directory.parent": "Thư mục mẹ", + "dialog.directory.readError": "Không thể đọc thư mục này", + "app.server.unreachable": "Không thể đạt tới {{server}}", + "app.server.retrying": "Đang tự động thử lại...", + "app.server.otherServers": "Máy chủ khác", + "dialog.server.title": "Máy chủ", + "dialog.server.description": "Chuyển máy chủ OpenCode mà ứng dụng này kết nối tới.", + "dialog.server.search.placeholder": "Tìm kiếm máy chủ", + "dialog.server.empty": "Chưa có máy chủ", + "dialog.server.add.title": "Thêm máy chủ", + "dialog.server.add.url": "Địa chỉ máy chủ", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "Không thể kết nối với máy chủ", + "dialog.server.add.checking": "Đang kiểm tra...", + "dialog.server.add.button": "Thêm máy chủ", + "dialog.server.add.name": "Tên máy chủ (tùy chọn)", + "dialog.server.add.namePlaceholder": "Máy chủ cục bộ", + "dialog.server.add.username": "Tên người dùng (tùy chọn)", + "dialog.server.add.usernamePlaceholder": "tên người dùng", + "dialog.server.add.password": "Mật khẩu (tùy chọn)", + "dialog.server.add.passwordPlaceholder": "mật khẩu", + "dialog.server.edit.title": "Chỉnh sửa máy chủ", + "dialog.server.default.title": "Máy chủ mặc định", + "dialog.server.default.description": + "Kết nối với máy chủ này khi khởi chạy ứng dụng thay vì khởi động máy chủ cục bộ. Yêu cầu khởi động lại.", + "dialog.server.default.none": "Không có máy chủ nào được chọn", + "dialog.server.default.set": "Đặt máy chủ hiện tại làm mặc định", + "dialog.server.default.clear": "Xóa", + "dialog.server.action.remove": "Xóa máy chủ", + "dialog.server.menu.edit": "Chỉnh sửa", + "dialog.server.menu.default": "Đặt làm mặc định", + "dialog.server.menu.defaultRemove": "Xóa mặc định", + "dialog.server.menu.delete": "Xóa", + "dialog.server.current": "Máy chủ hiện tại", + "dialog.server.status.default": "Mặc định", + "wsl.server.add": "Thêm máy chủ WSL", + "wsl.server.addShort": "Thêm WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "Máy chủ WSL", + "wsl.server.retryStart": "Thử khởi động lại", + "wsl.server.updating": "Đang cập nhật...", + "wsl.onboarding.step.distro": "Chọn bản phân phối", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "Đang kiểm tra WSL...", + "wsl.onboarding.restartRequired": "Windows cần khởi động lại để hoàn tất cài đặt WSL.", + "wsl.onboarding.ready": "WSL đã sẵn sàng.", + "wsl.onboarding.required": "WSL là bắt buộc để tiếp tục.", + "wsl.onboarding.checkingDistros": "Đang kiểm tra các bản phân phối...", + "wsl.onboarding.installingDistro": "Đang cài đặt {{distro}}...", + "wsl.onboarding.checkingDistro": "Đang kiểm tra {{distro}}...", + "wsl.onboarding.listingDistros": "Liệt kê các bản phân phối...", + "wsl.onboarding.distroReady": "{{distro}} đã sẵn sàng.", + "wsl.onboarding.distroNotInstalled": "{{distro}} chưa được cài đặt.", + "wsl.onboarding.openDistroOnce": "Mở {{distro}} một lần để hoàn tất thiết lập.", + "wsl.onboarding.finishingDistro": "Đang hoàn tất thiết lập cho {{distro}}.", + "wsl.onboarding.pickDistro": "Chọn một bản phân phối hoặc cài đặt một bản dưới đây.", + "wsl.onboarding.checkingOpencode": "Đang kiểm tra OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "Đang kiểm tra OpenCode trong {{distro}}...", + "wsl.onboarding.updatingOpencode": "Đang cập nhật OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "Đang cập nhật OpenCode trong {{distro}}...", + "wsl.onboarding.updateOpencodeIn": "Cập nhật OpenCode trong {{distro}}.", + "wsl.onboarding.updateOpencode": "Cập nhật OpenCode", + "wsl.onboarding.opencodeReadyIn": "OpenCode đã sẵn sàng trong {{distro}}.", + "wsl.onboarding.opencodeReady": "OpenCode đã sẵn sàng.", + "wsl.onboarding.installOpencodeIn": "Cài đặt OpenCode trong {{distro}}.", + "wsl.onboarding.installOpencode": "Cài đặt OpenCode", + "wsl.onboarding.chooseDistroFirst": "Chọn một bản phân phối đầu tiên.", + "wsl.onboarding.loadFailed": "Không tải được trạng thái WSL.", + "wsl.onboarding.loading": "Đang tải...", + "wsl.onboarding.installedDistros": "Các bản phân phối đã cài đặt", + "wsl.onboarding.checkAgain": "Kiểm tra lại", + "wsl.onboarding.distroStatus.ready": "Sẵn sàng", + "wsl.onboarding.distroStatus.checking": "Đang kiểm tra...", + "wsl.onboarding.distroStatus.opencodeMissing": "OpenCode chưa được cài đặt", + "wsl.onboarding.distroStatus.missingTools": "Thiếu bash, curl", + "wsl.onboarding.distroStatus.unsupported": "Không được hỗ trợ · Sử dụng WSL 2", + "wsl.onboarding.needAnotherDistro": "Cần một bản phân phối khác?", + "wsl.onboarding.needAnotherDistroHint": "Cài đặt bản phân phối Linux từ danh mục WSL", + "wsl.onboarding.wslNotInstalled.title": "WSL chưa được cài đặt", + "wsl.onboarding.wslNotInstalled.description": + "Cần có WSL (Hệ thống con Windows dành cho Linux) trước khi OpenCode có thể thêm máy chủ WSL", + "wsl.onboarding.wslUnavailable.title": "WSL không có sẵn", + "wsl.onboarding.wslUnavailable.description": "OpenCode không thể xác minh WSL trên máy này.", + "wsl.onboarding.installWsl": "Cài đặt WSL", + "wsl.onboarding.windowsRestartRequired": "Khởi động lại Windows để hoàn tất cài đặt WSL, sau đó mở lại OpenCode.", + "wsl.onboarding.next": "Tiếp theo", + "wsl.onboarding.refresh": "Làm mới", + "wsl.onboarding.allDistrosAdded": "Tất cả các bản phân phối đã cài đặt đều đã được thêm vào.", + "wsl.onboarding.noDistros": "Chưa phát hiện thấy bản phân phối nào.", + "wsl.onboarding.install": "Cài đặt", + "wsl.onboarding.installing": "Đang cài đặt...", + "wsl.onboarding.installDistro": "Cài đặt bản phân phối", + "wsl.onboarding.searchDistros": "Tìm kiếm bản phân phối", + "wsl.onboarding.wsl2Required": "WSL 2 là bắt buộc.", + "wsl.onboarding.toolsRequired": "Bản phân phối này cần bash và curl.", + "wsl.onboarding.openTerminal": "Mở terminal", + "wsl.onboarding.path": "Đường dẫn: {{path}}", + "wsl.onboarding.notFound": "không tìm thấy", + "wsl.onboarding.version": "Phiên bản: {{version}}", + "wsl.onboarding.unknown": "không rõ", + "wsl.onboarding.desktopVersion": "Desktop {{version}}", + "wsl.onboarding.versionMismatch": "Phiên bản đã cài đặt không khớp với phiên bản ứng dụng desktop.", + "wsl.onboarding.adding": "Đang thêm...", + "help.tabs.toast.ariaLabel": "Giới thiệu về tab. Sắp xếp công việc và các phiên đang hoạt động bằng tab", + "help.tabs.toast.dismiss": "Bỏ qua thông tin về tab", + "help.tabs.title": "Giới thiệu về tab", + "help.tabs.description": "Sắp xếp công việc và các phiên đang hoạt động bằng tab", + "help.tabs.date": "14 tháng 7", + "help.tabs.introduction": "OpenCode Desktop nay được xây dựng xoay quanh các tab.", + "help.tabs.sessions": + "Bắt đầu phiên mới trong một tab hoặc mở phiên hiện có từ bất kỳ dự án nào của bạn. Hãy mở tab mới khi bắt đầu công việc mới và đóng tab khi hoàn tất.", + "help.tabs.organize": + "Việc mở một vài tab giúp bạn dễ dàng sắp xếp các phiên đang hoạt động. Hãy đổi tên tab thành tên dễ nhớ nếu bạn định giữ tab đó lâu dài.", + "help.tabs.home": + "Bạn sẽ tìm thấy tất cả các phiên và dự án trên màn hình Trang chủ mới. Khi bạn chọn một phiên, phiên đó sẽ mở trong một tab.", + "help.tabs.persistence": "Khi bạn mở lại ứng dụng, các tab vẫn mở.", + "help.tabs.worktrees": + "Thiết kế mới chưa hỗ trợ Git Worktrees, nhưng tính năng này sẽ sớm ra mắt. Vì vậy, nếu muốn tiếp tục sử dụng bố cục trước đó, bạn có thể chuyển đổi giữa các bố cục trong Cài đặt. Xin lưu ý rằng bố cục mới sẽ trở thành cố định sau vài tuần nữa.", + "server.row.noUsername": "không có tên người dùng", + "dialog.project.edit.title": "Chỉnh sửa dự án", + "dialog.project.edit.name": "Tên", + "dialog.project.edit.icon": "Biểu tượng", + "dialog.project.edit.icon.alt": "Biểu tượng dự án", + "dialog.project.edit.icon.hint": "Nhấp hoặc kéo hình ảnh", + "dialog.project.edit.icon.recommended": "Khuyến nghị: 128x128px", + "dialog.project.edit.color": "Màu sắc", + "dialog.project.edit.color.select": "Chọn màu {{color}}", + "dialog.project.edit.worktree.startup": "Tập lệnh khởi động không gian làm việc", + "dialog.project.edit.worktree.startup.description": "Chạy sau khi tạo không gian làm việc mới (worktree).", + "dialog.project.edit.worktree.startup.placeholder": "ví dụ: bun install", + "dialog.releaseNotes.action.getStarted": "Bắt đầu", + "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.', + "context.breakdown.system": "Hệ thống", + "context.breakdown.user": "Người dùng", + "context.breakdown.assistant": "Trợ lý", + "context.breakdown.tool": "Lệnh gọi công cụ", + "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", + "context.stats.model": "Mô hình", + "context.stats.limit": "Giới hạn ngữ cảnh", + "context.stats.totalTokens": "Tổng số token", + "context.stats.usage": "Mức sử dụng", + "context.stats.inputTokens": "Token đầu vào", + "context.stats.outputTokens": "Token đầu ra", + "context.stats.reasoningTokens": "Token lý luận", + "context.stats.cacheTokens": "Token bộ đệm (đọc/ghi)", + "context.stats.userMessages": "Tin nhắn của người dùng", + "context.stats.assistantMessages": "Tin nhắn trợ lý", + "context.stats.totalCost": "Tổng chi phí", + "context.stats.sessionCreated": "Đã tạo phiên", + "context.stats.lastActivity": "Hoạt động cuối cùng", + "context.usage.tokens": "Token", + "context.usage.usage": "Mức sử dụng", + "context.usage.cost": "Chi phí", + "context.usage.clickToView": "Nhấn vào đây để xem ngữ cảnh", + "context.usage.view": "Xem cách sử dụng ngữ cảnh", + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + "toast.language.title": "Ngôn ngữ", + "toast.language.description": "Đã chuyển sang {{language}}", + "toast.theme.title": "Đã chuyển chủ đề", + "toast.scheme.title": "Cách phối màu", + "toast.workspace.enabled.title": "Đã bật không gian làm việc", + "toast.workspace.enabled.description": "Nhiều worktree hiện được hiển thị trên thanh bên", + "toast.workspace.disabled.title": "Đã tắt không gian làm việc", + "toast.workspace.disabled.description": "Chỉ worktree chính được hiển thị trong thanh bên", + "toast.permissions.autoaccept.on.title": "Tự động chấp nhận quyền", + "toast.permissions.autoaccept.on.description": "Yêu cầu quyền sẽ được tự động phê duyệt", + "toast.permissions.autoaccept.off.title": "Đã dừng quyền tự động chấp nhận", + "toast.permissions.autoaccept.off.description": "Yêu cầu quyền sẽ cần được phê duyệt", + "toast.model.none.title": "Không có mô hình nào được chọn", + "toast.model.none.description": "Kết nối nhà cung cấp để tóm tắt phiên này", + "toast.file.loadFailed.title": "Không tải được tệp", + "toast.file.listFailed.title": "Không thể liệt kê các tệp", + "toast.context.noLineSelection.title": "Không có lựa chọn dòng", + "toast.context.noLineSelection.description": "Trước tiên, hãy chọn một phạm vi dòng trong tab tệp.", + "toast.session.share.copyFailed.title": "Không sao chép được URL vào bảng tạm", + "toast.session.share.success.title": "Đã chia sẻ phiên", + "toast.session.share.success.description": "Đã sao chép URL chia sẻ vào bảng tạm!", + "toast.session.share.failed.title": "Không thể chia sẻ phiên", + "toast.session.share.failed.description": "Đã xảy ra lỗi khi chia sẻ phiên", + "toast.session.unshare.success.title": "Đã ngừng chia sẻ phiên", + "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", + "toast.update.description": "Hiện đã có phiên bản mới của OpenCode ({{version}}) để cài đặt.", + "toast.update.action.installRestart": "Cài đặt và khởi động lại", + "toast.update.action.notYet": "Chưa", + "error.page.title": "Đã xảy ra lỗi", + "error.page.description": "Đã xảy ra lỗi khi tải ứng dụng.", + "error.page.description.localServerStartup": "Đã xảy ra lỗi khi khởi động máy chủ cục bộ.", + "error.page.details.label": "Chi tiết lỗi", + "error.page.action.restart": "Khởi động lại", + "error.page.action.report": "Báo cáo lỗi", + "error.page.action.reported": "Đã báo lỗi", + "error.page.action.exportLogs": "Xuất nhật ký", + "error.page.action.checking": "Đang kiểm tra...", + "error.page.action.checkUpdates": "Kiểm tra các bản cập nhật", + "error.page.action.updateTo": "Cập nhật lên {{version}}", + "error.page.circular": "[Vòng lặp]", + "error.page.report.prefix": "Vui lòng báo cáo lỗi này cho nhóm OpenCode", + "error.page.report.discord": "trên Discord", + "error.page.version": "Phiên bản: {{version}}", + "error.dev.rootNotFound": + "Không tìm thấy phần tử gốc. Bạn đã quên thêm nó vào index.html của mình? Hoặc có thể thuộc tính id bị sai chính tả?", + "error.serverSync.connectFailed": "Không thể kết nối với máy chủ. Có máy chủ nào đang chạy ở `{{url}}` không?", + "error.serverSDK.noServerAvailable": "Không có máy chủ nào", + "error.serverSDK.serverNotAvailable": "Máy chủ không có sẵn", + "error.childStore.persistedCacheCreateFailed": "Không tạo được bộ nhớ đệm bền vững", + "error.childStore.persistedProjectMetadataCreateFailed": "Không thể tạo siêu dữ liệu dự án bền vững", + "error.childStore.persistedProjectIconCreateFailed": "Không thể tạo biểu tượng dự án bền vững", + "error.childStore.storeCreateFailed": "Không tạo được kho lưu trữ", + "directory.error.invalidUrl": "Thư mục không hợp lệ trong URL.", + "error.chain.unknown": "Lỗi không xác định", + "error.server.invalidConfiguration": "Cấu hình không hợp lệ", + "error.chain.causedBy": "Gây ra bởi:", + "error.chain.apiError": "Lỗi API", + "error.chain.status": "Trạng thái: {{status}}", + "error.chain.retryable": "Có thể thử lại: {{retryable}}", + "error.chain.responseBody": "Nội dung phản hồi:\n{{body}}", + "error.chain.didYouMean": "Có phải ý bạn là: {{suggestions}}", + "error.chain.modelNotFound": "Không tìm thấy mô hình: {{provider}}/{{model}}", + "error.chain.checkConfig": "Kiểm tra tên nhà cung cấp/mô hình cấu hình (opencode.json) của bạn", + "error.chain.mcpFailed": 'Máy chủ MCP "{{name}}" gặp lỗi. Lưu ý: OpenCode chưa hỗ trợ xác thực MCP.', + "error.chain.providerAuthFailed": "Không xác thực được nhà cung cấp ({{provider}}): {{message}}", + "error.chain.providerInitFailed": + 'Không khởi tạo được nhà cung cấp "{{provider}}". Kiểm tra thông tin xác thực và cấu hình.', + "error.chain.configJsonInvalid": "Tệp cấu hình tại {{path}} không phải là JSON(C) hợp lệ", + "error.chain.configJsonInvalidWithMessage": "Tệp cấu hình tại {{path}} không phải là JSON(C) hợp lệ: {{message}}", + "error.chain.configDirectoryTypo": + 'Thư mục "{{dir}}" trong {{path}} không hợp lệ. Đổi tên thư mục thành "{{suggestion}}" hoặc xóa nó. Đây là một lỗi đánh máy phổ biến.', + "error.chain.configFrontmatterError": "Không thể phân tích cú pháp frontmatter trong {{path}}:\n{{message}}", + "error.chain.configInvalid": "Tệp cấu hình tại {{path}} không hợp lệ", + "error.chain.configInvalidWithMessage": "Tệp cấu hình tại {{path}} không hợp lệ: {{message}}", + "notification.permission.title": "Cần có sự cho phép", + "notification.permission.description": "{{sessionTitle}} trong {{projectName}} cần có sự cho phép", + "notification.question.title": "Câu hỏi", + "notification.question.description": "{{sessionTitle}} trong {{projectName}} có câu hỏi", + "notification.action.goToSession": "Đi đến phiên", + "notification.session.responseReady.title": "Đã sẵn sàng phản hồi", + "notification.session.error.title": "Lỗi phiên", + "notification.session.error.fallbackDescription": "Đã xảy ra lỗi", + "home.recentProjects": "dự án gần đây", + "home.empty.title": "Không có dự án gần đây", + "home.empty.description": "Bắt đầu bằng cách mở một dự án cục bộ", + "home.title": "Trang chủ", + "home.projects": "Dự án", + "home.project.add": "Thêm dự án", + "home.recentlyClosed": "Đã đóng gần đây", + "home.server.collapse": "Thu gọn các dự án máy chủ", + "home.server.expand": "Mở rộng dự án máy chủ", + "home.sessions.search.placeholder": "Tìm kiếm phiên", + "home.sessions.search.placeholder.scoped": "Tìm kiếm phiên trong {{scope}}", + "home.sessions.search.sessions": "Phiên", + "home.sessions.search.noResults": "Không tìm thấy phiên nào cho {{query}}", + "home.sessions.empty": "Chưa có gì ở đây", + "home.sessions.empty.description": "Tạo một phiên để bắt đầu", + "home.sessions.group.today": "Hôm nay", + "home.sessions.group.yesterday": "Hôm qua", + "home.sessions.group.older": "Cũ hơn", + "home.providerTip": "Kết nối với hơn 75 nhà cung cấp để sử dụng các mô hình khác, bao gồm Claude, GPT, Gemini, v.v.", + "session.tab.session": "Phiên", + "session.tab.review": "Xem lại", + "session.tab.context": "Ngữ cảnh", + "session.tab.unknown": "Phiên không xác định", + "session.panel.reviewAndFiles": "Xem lại và tệp", + "session.error.notFound": "Không thể tìm thấy phiên này", + "session.error.notFound.description": "Tab này trỏ đến một phiên không còn tồn tại trên máy chủ này.", + "session.error.notFound.closeTab": "Đóng tab", + "session.error.serverConnection": "Không thể kết nối với máy chủ này", + "session.review.filesChanged": "Số tệp đã thay đổi: {{count}}", + "session.review.change.one": "Thay đổi", + "session.review.change.other": "Thay đổi", + "session.review.loadingChanges": "Đang tải các thay đổi...", + "session.review.empty": "Chưa có thay đổi nào trong phiên này", + "session.review.noVcs": "Không phát hiện hệ thống quản lý phiên bản Git nên không thể hiển thị thay đổi", + "session.review.noVcs.createGit.title": "Tạo kho lưu trữ Git", + "session.review.noVcs.createGit.description": "Theo dõi, xem xét và hoàn tác các thay đổi trong dự án này", + "session.review.noVcs.createGit.actionLoading": "Đang tạo kho lưu trữ Git...", + "session.review.noVcs.createGit.action": "Tạo kho lưu trữ Git", + "session.review.noSnapshot": + "Theo dõi ảnh chụp nhanh bị tắt trong cấu hình, do đó các thay đổi về phiên không khả dụng", + "session.review.noChanges": "Không có thay đổi", + "session.review.noUncommittedChanges": "Chưa có thay đổi nào chưa commit", + "session.review.noBranchChanges": "Chưa có thay đổi nhánh", + "session.files.selectToOpen": "Chọn một tệp để mở", + "session.files.all": "Tất cả tệp", + "session.files.empty": "Không có tệp", + "session.files.binaryContent": "Tệp nhị phân (không thể hiển thị nội dung)", + "session.messages.renderEarlier": "Hiển thị tin nhắn trước đó", + "session.messages.loadingEarlier": "Đang tải tin nhắn trước đó...", + "session.messages.loadEarlier": "Tải tin nhắn trước đó", + "session.messages.loading": "Đang tải tin nhắn...", + "session.messages.jumpToLatest": "Chuyển đến mới nhất", + "session.context.addToContext": "Thêm {{selection}} vào ngữ cảnh", + "session.todo.title": "việc cần làm", + "session.todo.collapse": "Thu gọn", + "session.todo.expand": "Mở rộng", + "session.todo.progress": "{{done}} trong số {{total}} việc cần làm đã hoàn thành", + "session.question.progress": "{{current}} trong số {{total}} câu hỏi", + "session.question.minimize": "Giảm thiểu câu hỏi", + "session.question.restore": "Khôi phục câu hỏi", + "session.question.pending.one": "{{count}} câu hỏi đang chờ xử lý", + "session.question.pending.other": "{{count}} câu hỏi đang chờ xử lý", + "session.followupDock.summary.one": "{{count}} tin nhắn đang chờ", + "session.followupDock.summary.other": "{{count}} tin nhắn đang chờ", + "session.followupDock.sendNow": "Gửi ngay bây giờ", + "session.followupDock.edit": "Chỉnh sửa", + "session.followupDock.collapse": "Thu gọn tin nhắn đang chờ", + "session.followupDock.expand": "Mở rộng tin nhắn đang chờ", + "session.revertDock.summary.one": "{{count}} tin nhắn đã hoàn tác", + "session.revertDock.summary.other": "{{count}} tin nhắn đã hoàn tác", + "session.revertDock.collapse": "Thu gọn tin nhắn đã hoàn tác", + "session.revertDock.expand": "Mở rộng tin nhắn đã hoàn tác", + "session.revertDock.restore": "Khôi phục tin nhắn", + "session.new.title": "Xây dựng bất cứ thứ gì", + "session.new.project.new": "Dự án mới", + "session.new.project.search": "Tìm kiếm dự án", + "session.new.project.add": "Thêm dự án", + "session.new.worktree.main": "Nhánh chính", + "session.new.worktree.mainWithBranch": "Nhánh chính ({{branch}})", + "session.new.worktree.create": "Tạo mới worktree", + "session.new.workspace.runIn": "Chạy phiên trong", + "session.new.workspace.triggerLocal": "Cục bộ", + "session.new.workspace.local": "Kho lưu trữ cục bộ", + "session.new.workspace.existing": "Không gian làm việc…", + "session.new.git.none": "Không dùng Git", + "session.new.lastModified": "Sửa đổi lần cuối", + "session.header.search.placeholder": "Tìm kiếm {{project}}", + "session.header.searchFiles": "Tìm kiếm tệp", + "session.header.openIn": "Mở trong", + "session.header.open.action": "Mở {{app}}", + "session.header.open.ariaLabel": "Mở trong {{app}}", + "session.header.open.menu": "Tùy chọn mở", + "session.header.open.copyPath": "Sao chép đường dẫn", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "File Explorer", + "session.header.open.fileManager": "Trình quản lý tệp", + "session.header.reveal.finder": "Hiển thị trong Finder", + "session.header.reveal.fileExplorer": "Hiển thị trong File Explorer", + "session.header.reveal.containingFolder": "Mở thư mục chứa", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "Terminal", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "status.popover.trigger": "Trạng thái", + "status.popover.ariaLabel": "Cấu hình máy chủ", + "status.popover.tab.servers": "Máy chủ", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "Plugin", + "status.popover.action.manageServers": "Quản lý máy chủ", + "session.share.popover.title": "Xuất bản trên web", + "session.share.popover.description.shared": + "Phiên này được công khai trên web. Nó có thể truy cập được bởi bất kỳ ai có liên kết.", + "session.share.popover.description.unshared": + "Chia sẻ phiên công khai trên web. Nó sẽ có thể truy cập được bởi bất kỳ ai có liên kết.", + "session.share.action.share": "Chia sẻ", + "session.share.action.publish": "Xuất bản", + "session.share.action.publishing": "Đang xuất bản...", + "session.share.action.unpublish": "Hủy xuất bản", + "session.share.action.unpublishing": "Đang hủy xuất bản...", + "session.share.action.view": "Xem", + "session.share.copy.copied": "Đã sao chép", + "session.share.copy.copyLink": "Sao chép liên kết", + "lsp.tooltip.none": "Không có máy chủ LSP", + "lsp.label.connected": "{{count}} LSP", + "prompt.loading": "Đang tải lời nhắc...", + "terminal.loading": "Đang tải terminal...", + "terminal.title": "Terminal", + "terminal.title.numbered": "Terminal {{number}}", + "terminal.close": "Đóng terminal", + "terminal.connectionLost.title": "Mất kết nối", + "terminal.connectionLost.abnormalClose": "WebSocket đóng bất thường: {{code}}", + "terminal.connectionLost.description": + "Kết nối terminal bị gián đoạn. Điều này có thể xảy ra khi máy chủ khởi động lại.", + "terminal.connectTicket.csrfError": + "Vé kết nối PTY bị từ chối do kiểm tra nguồn gốc hoặc CSRF. Hãy kiểm tra cấu hình CORS của máy chủ.", + "terminal.connectTicket.statusError": "Vé kết nối PTY thất bại với trạng thái {{status}}", + "titlebar.update": "Cập nhật", + "titlebar.updateVersion": "Cập nhật {{version}}", + "common.closeTab": "Đóng tab", + "common.dismiss": "Bỏ qua", + "common.moreCountSuffix": " (+{{count}} thêm)", + "common.requestFailed": "Không thực hiện được yêu cầu", + "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", + "common.close": "Đóng", + "common.edit": "Chỉnh sửa", + "common.loadMore": "Tải thêm", + "common.key.esc": "ESC", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "Phím cách", + "common.key.backspace": "Backspace", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "không rõ", + "common.time.justNow": "Vừa rồi", + "common.time.minutesAgo.short": "{{count}} phút trước", + "common.time.hoursAgo.short": "{{count}} giờ trước", + "common.time.daysAgo.short": "{{count}} ngày trước", + "sidebar.menu.toggle": "Bật/tắt menu", + "sidebar.nav.projectsAndSessions": "Dự án và phiên", + "sidebar.settings": "Cài đặt", + "sidebar.help": "Trợ giúp", + "sidebar.workspaces.enable": "Bật không gian làm việc", + "sidebar.workspaces.disable": "Tắt không gian làm việc", + "sidebar.gettingStarted.title": "Bắt đầu", + "sidebar.gettingStarted.line1": "OpenCode bao gồm các mô hình miễn phí để bạn có thể bắt đầu ngay lập tức.", + "sidebar.gettingStarted.line2": + "Kết nối với bất kỳ nhà cung cấp nào để sử dụng các mô hình như Claude, GPT, Gemini, v.v.", + "sidebar.project.recentSessions": "Phiên gần đây", + "sidebar.project.viewAllSessions": "Xem tất cả các phiên", + "sidebar.project.clearNotifications": "Xóa thông báo", + "sidebar.empty.title": "Không có dự án nào mở", + "sidebar.empty.description": "Mở một dự án để bắt đầu", + "debugBar.ariaLabel": "Chẩn đoán hiệu suất phát triển", + "debugBar.na": "không có", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": + "Lần chuyển tuyến hoàn tất gần nhất có truy cập trang phiên, được đo từ lúc bộ định tuyến bắt đầu đến khung hình đầu tiên sau khi ổn định.", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "Số khung hình mỗi giây trong 5 giây qua.", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "Thời gian tạo khung hình tệ nhất trong 5 giây vừa qua.", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "Số khung hình mất hơn 32ms trong 5 giây qua.", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "Thời gian bị chặn và số lượng nhiệm vụ dài trong 5 giây qua. Nhiệm vụ tối đa: {{max}}.", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "Độ trễ đầu vào được quan sát tồi tệ nhất trong 5 giây qua.", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": + "Thời lượng tương tác gần đúng trong 5 giây qua. Đây giống như INP, không phải là Web Vitals chính thức INP.", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "Sự thay đổi bố cục tích lũy cho thời gian tồn tại của ứng dụng hiện tại.", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "Bộ nhớ heap JS đã dùng so với giới hạn heap. Chỉ hỗ trợ Chromium.", + "debugBar.mem.tip": "Đã sử dụng JS heap so với giới hạn heap. {{used}} của {{limit}}.", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "Buộc áp dụng kiểu tiêu điểm cho mọi phần tử tương tác", + "debugBar.focus.on": "BẬT", + "debugBar.focus.off": "TẮT", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "Buộc đặt hướng bố cục cho toàn bộ ứng dụng mà không thay đổi ngôn ngữ đã chọn", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "Desktop", + "settings.section.server": "Máy chủ", + "settings.tab.general": "Tổng quan", + "settings.tab.shortcuts": "Phím tắt", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL tích hợp", + "settings.desktop.wsl.description": "Chạy máy chủ OpenCode bên trong WSL trên Windows.", + "settings.general.section.appearance": "Giao diện", + "settings.general.section.advanced": "Nâng cao", + "settings.general.section.notifications": "Thông báo hệ thống", + "settings.general.section.updates": "Cập nhật", + "settings.general.section.sounds": "Hiệu ứng âm thanh", + "settings.general.section.feed": "Nguồn cấp", + "settings.general.section.display": "Hiển thị", + "settings.general.row.language.title": "Ngôn ngữ", + "settings.general.row.language.description": "Thay đổi ngôn ngữ hiển thị cho OpenCode", + "settings.general.row.shell.title": "Shell terminal", + "settings.general.row.shell.description": "Shell được terminal và các công cụ của tác nhân sử dụng", + "settings.general.row.shell.autoDefault": "Tự động (Mặc định)", + "settings.general.row.shell.terminalOnly": "chỉ terminal", + "settings.general.row.appearance.title": "Giao diện", + "settings.general.row.appearance.description": "Tùy chỉnh giao diện của OpenCode trên thiết bị", + "settings.general.row.colorScheme.title": "Cách phối màu", + "settings.general.row.colorScheme.description": "Chọn xem OpenCode tuân theo hệ thống, chủ đề sáng hay tối", + "settings.general.row.theme.title": "Chủ đề", + "settings.general.row.theme.description": "Tùy chỉnh chủ đề của OpenCode.", + "settings.general.row.font.title": "Phông chữ mã", + "settings.general.row.font.description": "Tùy chỉnh phông chữ được sử dụng trong khối mã", + "settings.general.row.terminalFont.title": "Phông chữ terminal", + "settings.general.row.terminalFont.description": "Tùy chỉnh phông chữ được sử dụng trong terminal", + "settings.general.row.uiFont.title": "Phông chữ giao diện", + "settings.general.row.uiFont.description": "Tùy chỉnh phông chữ dùng trong toàn bộ giao diện", + "settings.general.row.followup.title": "Cách xử lý lời nhắc tiếp theo", + "settings.general.row.followup.description": + "Chọn để lời nhắc tiếp theo điều hướng phiên ngay lập tức hoặc chờ trong hàng đợi", + "settings.general.row.followup.option.queue": "Xếp hàng", + "settings.general.row.followup.option.steer": "Điều hướng", + "settings.general.row.showFileTree.title": "Cây tệp", + "settings.general.row.showFileTree.description": "Hiển thị bảng cây tệp trong phiên", + "settings.general.row.showNavigation.title": "Điều khiển điều hướng", + "settings.general.row.showNavigation.description": + "Hiển thị các nút quay lại và tiến lên trên thanh tiêu đề của màn hình", + "settings.general.row.showSearch.title": "Bảng lệnh", + "settings.general.row.showSearch.description": "Hiển thị nút bảng tìm kiếm và lệnh trong thanh tiêu đề", + "settings.general.row.showTerminal.title": "Terminal", + "settings.general.row.showTerminal.description": "Hiển thị nút terminal trên thanh tiêu đề của ứng dụng desktop", + "settings.general.row.showStatus.title": "Trạng thái máy chủ", + "settings.general.row.showStatus.description": "Hiển thị nút trạng thái máy chủ trên thanh tiêu đề", + "settings.general.row.mobileTitlebarBottom.title": "Điều hướng dưới cùng", + "settings.general.row.mobileTitlebarBottom.description": + "Đặt thanh tiêu đề và tab phiên ở cuối màn hình trên thiết bị di động", + "settings.general.row.showCustomAgents.title": "Hiển thị tác nhân", + "settings.general.row.showCustomAgents.description": + "Chuyển đổi giữa các tác nhân trong trình soạn thảo. Khi ẩn, tác nhân mặc định là Build.", + "settings.general.row.reasoningSummaries.title": "Hiển thị tóm tắt lý luận", + "settings.general.row.reasoningSummaries.description": "Hiển thị tóm tắt lý luận mô hình trong dòng thời gian", + "settings.general.row.shellToolPartsExpanded.title": "Mở rộng các phần của công cụ shell", + "settings.general.row.shellToolPartsExpanded.description": + "Hiển thị các phần công cụ shell được mở rộng theo mặc định trong dòng thời gian", + "settings.general.row.editToolPartsExpanded.title": "Mở rộng các phần công cụ chỉnh sửa", + "settings.general.row.editToolPartsExpanded.description": + "Hiển thị các phần công cụ chỉnh sửa, viết và vá lỗi được mở rộng theo mặc định trong dòng thời gian", + "settings.general.row.newInterface.title": "Bố cục mới", + "settings.general.row.newInterface.badge": "Mới", + "settings.general.row.newInterface.description": + "Sử dụng các tab mới và bố cục trang chủ. Chuyển đổi giữa các bố cục trong một thời gian giới hạn.", + "settings.general.row.newInterfaceNotice.title": "Bạn hiện đang sử dụng bố cục mới", + "settings.general.row.newInterfaceNotice.description": "Bố cục trước đó không còn khả dụng", + "settings.general.row.newInterfaceNotice.dismiss": "Bỏ qua", + "settings.general.row.pinchZoom.title": "Chụm để thu phóng", + "settings.general.row.pinchZoom.description": "Cho phép chụm bàn di chuột và cử chỉ cuộn Ctrl để thu phóng", + "settings.general.row.wayland.title": "Sử dụng Wayland gốc", + "settings.general.row.wayland.description": "Tắt tính năng dự phòng X11 trên Wayland. Yêu cầu khởi động lại.", + "settings.general.row.wayland.tooltip": + "Trên Linux với màn hình có tốc độ làm mới hỗn hợp, Wayland gốc có thể ổn định hơn.", + "settings.general.row.releaseNotes.title": "Ghi chú phát hành", + "settings.general.row.releaseNotes.description": "Hiển thị cửa sổ bật lên Có gì mới sau khi cập nhật", + "settings.updates.row.startup.title": "Kiểm tra cập nhật khi khởi động", + "settings.updates.row.startup.description": "Tự động kiểm tra các bản cập nhật khi OpenCode khởi chạy", + "settings.updates.row.check.title": "Kiểm tra các bản cập nhật", + "settings.updates.row.check.description": "Kiểm tra thủ công các bản cập nhật và cài đặt nếu có", + "settings.updates.action.checkNow": "Kiểm tra ngay", + "settings.updates.action.checking": "Đang kiểm tra...", + "settings.updates.action.downloading": "Đang tải xuống...", + "settings.updates.action.installing": "Đang cài đặt...", + "settings.updates.toast.latest.title": "Đã cập nhật", + "settings.updates.toast.latest.description": "Bạn đang chạy phiên bản mới nhất của OpenCode.", + "sound.option.none": "Không có", + "sound.option.alert01": "Alert 01", + "sound.option.alert02": "Alert 02", + "sound.option.alert03": "Alert 03", + "sound.option.alert04": "Alert 04", + "sound.option.alert05": "Alert 05", + "sound.option.alert06": "Alert 06", + "sound.option.alert07": "Alert 07", + "sound.option.alert08": "Alert 08", + "sound.option.alert09": "Alert 09", + "sound.option.alert10": "Alert 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "Tác nhân", + "settings.general.notifications.agent.description": + "Hiển thị thông báo hệ thống khi tác nhân hoàn thành hoặc cần chú ý", + "settings.general.notifications.permissions.title": "Quyền", + "settings.general.notifications.permissions.description": "Hiển thị thông báo hệ thống khi cần có sự cho phép", + "settings.general.notifications.errors.title": "Lỗi", + "settings.general.notifications.errors.description": "Hiển thị thông báo hệ thống khi xảy ra lỗi", + "settings.general.sounds.agent.title": "Tác nhân", + "settings.general.sounds.agent.description": "Phát âm thanh khi tác nhân hoàn thành hoặc cần chú ý", + "settings.general.sounds.permissions.title": "Quyền", + "settings.general.sounds.permissions.description": "Phát âm thanh khi cần có sự cho phép", + "settings.general.sounds.errors.title": "Lỗi", + "settings.general.sounds.errors.description": "Phát âm thanh khi xảy ra lỗi", + "settings.shortcuts.title": "Phím tắt", + "settings.shortcuts.reset.button": "Đặt lại về mặc định", + "settings.shortcuts.reset.toast.title": "Đặt lại phím tắt", + "settings.shortcuts.reset.toast.description": "Phím tắt đã được đặt lại về mặc định.", + "settings.shortcuts.conflict.title": "Phím tắt đã được sử dụng", + "settings.shortcuts.conflict.description": "{{keybind}} đã được gán cho {{titles}}.", + "settings.shortcuts.unassigned": "Chưa được chỉ định", + "settings.shortcuts.pressKeys": "Nhấn phím", + "settings.shortcuts.search.placeholder": "Tìm kiếm phím tắt", + "settings.shortcuts.search.empty": "Không tìm thấy phím tắt", + "settings.shortcuts.group.general": "Tổng quan", + "settings.shortcuts.group.session": "Phiên", + "settings.shortcuts.group.navigation": "Điều hướng", + "settings.shortcuts.group.modelAndAgent": "Mô hình và tác nhân", + "settings.shortcuts.group.terminal": "Terminal", + "settings.shortcuts.group.prompt": "Lời nhắc", + "settings.providers.title": "Nhà cung cấp", + "settings.providers.description": "Cài đặt nhà cung cấp sẽ được cấu hình ở đây.", + "settings.providers.section.connected": "Nhà cung cấp được kết nối", + "settings.providers.connected.empty": "Không có nhà cung cấp được kết nối", + "settings.providers.connected.environmentDescription": "Đã kết nối từ các biến môi trường của bạn", + "settings.providers.section.popular": "Nhà cung cấp phổ biến", + "settings.providers.custom.description": "Thêm nhà cung cấp tương thích OpenAI bằng URL cơ sở.", + "settings.providers.tag.environment": "Môi trường", + "settings.providers.tag.config": "Cấu hình", + "settings.providers.tag.custom": "Tùy chỉnh", + "settings.providers.tag.other": "Khác", + "settings.models.title": "Mô hình", + "settings.models.description": "Cài đặt mô hình sẽ được cấu hình ở đây.", + "settings.agents.title": "Tác nhân", + "settings.agents.description": "Cài đặt tác nhân sẽ được cấu hình ở đây.", + "settings.commands.title": "Lệnh", + "settings.commands.description": "Cài đặt lệnh sẽ được cấu hình ở đây.", + "settings.mcp.title": "MCP", + "settings.mcp.description": "Có thể cấu hình cài đặt MCP tại đây.", + "settings.permissions.title": "Quyền", + "settings.permissions.description": "Kiểm soát những công cụ mà máy chủ có thể sử dụng theo mặc định.", + "settings.permissions.section.tools": "Công cụ", + "settings.permissions.toast.updateFailed.title": "Không cập nhật được quyền", + "settings.permissions.action.allow": "Cho phép", + "settings.permissions.action.ask": "Hỏi", + "settings.permissions.action.deny": "Từ chối", + "settings.permissions.tool.read.title": "Đọc", + "settings.permissions.tool.read.description": "Đọc một tệp (khớp với đường dẫn tệp)", + "settings.permissions.tool.edit.title": "Chỉnh sửa", + "settings.permissions.tool.edit.description": "Sửa đổi tệp, bao gồm chỉnh sửa, ghi và vá lỗi", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "Khớp tệp bằng mẫu glob", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "Tìm kiếm nội dung tệp bằng cách sử dụng biểu thức thông thường", + "settings.permissions.tool.list.title": "Danh sách", + "settings.permissions.tool.list.description": "Liệt kê các tệp trong một thư mục", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "Chạy lệnh shell", + "settings.permissions.tool.task.title": "Nhiệm vụ", + "settings.permissions.tool.task.description": "Khởi chạy tác nhân phụ", + "settings.permissions.tool.skill.title": "Kỹ năng", + "settings.permissions.tool.skill.description": "Tải một kỹ năng theo tên", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "Chạy truy vấn máy chủ ngôn ngữ", + "settings.permissions.tool.todowrite.title": "Ghi việc cần làm", + "settings.permissions.tool.todowrite.description": "Cập nhật danh sách việc cần làm", + "settings.permissions.tool.webfetch.title": "Tìm nạp web", + "settings.permissions.tool.webfetch.description": "Tìm nạp nội dung từ URL", + "settings.permissions.tool.websearch.title": "Tìm kiếm trên web", + "settings.permissions.tool.websearch.description": "Tìm kiếm trên web", + "settings.permissions.tool.external_directory.title": "Thư mục bên ngoài", + "settings.permissions.tool.external_directory.description": "Truy cập các tệp bên ngoài thư mục dự án", + "settings.permissions.tool.doom_loop.title": "Vòng lặp vô hạn", + "settings.permissions.tool.doom_loop.description": + "Phát hiện các lệnh gọi công cụ lặp lại với đầu vào giống hệt nhau", + "session.delete.failed.title": "Không thể xóa phiên", + "session.delete.title": "Xóa phiên", + "session.delete.confirm": 'Xóa phiên "{{name}}"?', + "session.delete.button": "Xóa phiên", + "workspace.new": "Không gian làm việc mới", + "workspace.type.local": "cục bộ", + "workspace.type.sandbox": "sandbox", + "workspace.create.failed.title": "Không tạo được không gian làm việc", + "workspace.delete.failed.title": "Không xóa được không gian làm việc", + "workspace.resetting.title": "Đặt lại không gian làm việc", + "workspace.resetting.description": "Việc này có thể mất một phút.", + "workspace.reset.failed.title": "Không thể đặt lại không gian làm việc", + "workspace.reset.success.title": "Đặt lại không gian làm việc", + "workspace.reset.success.description": "Không gian làm việc bây giờ khớp với nhánh mặc định.", + "workspace.error.stillPreparing": "Không gian làm việc vẫn đang chuẩn bị", + "workspace.status.checking": "Đang kiểm tra các thay đổi chưa được hợp nhất...", + "workspace.status.error": "Không thể xác minh trạng thái git.", + "workspace.status.clean": "Không phát hiện thấy thay đổi chưa hợp nhất nào.", + "workspace.status.dirty": "Đã phát hiện thấy các thay đổi chưa được hợp nhất trong không gian làm việc này.", + "workspace.delete.title": "Xóa không gian làm việc", + "workspace.delete.confirm": 'Xóa không gian làm việc "{{name}}"?', + "workspace.delete.button": "Xóa không gian làm việc", + "workspace.reset.title": "Đặt lại không gian làm việc", + "workspace.reset.confirm": 'Đặt lại không gian làm việc "{{name}}"?', + "workspace.reset.button": "Đặt lại không gian làm việc", + "workspace.reset.archived.none": "Không có phiên hoạt động nào sẽ được lưu trữ.", + "workspace.reset.archived.one": "1 phiên sẽ được lưu trữ.", + "workspace.reset.archived.many": "{{count}} phiên sẽ được lưu trữ.", + "workspace.reset.note": "Điều này sẽ thiết lập lại không gian làm việc để phù hợp với nhánh mặc định.", +} diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0ba0453479bb13a97139ca7f624c152bf8facbe --- /dev/null +++ b/packages/app/src/i18n/zh.ts @@ -0,0 +1,1227 @@ +import { dict as en } from "./en" + +type Keys = keyof typeof en + +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "文件", + "desktop.menu.edit": "编辑", + "desktop.menu.view": "视图", + "desktop.menu.go": "转到", + "desktop.menu.window": "窗口", + "desktop.menu.help": "帮助", + "desktop.menu.checkForUpdates": "检查更新...", + "desktop.menu.settings": "设置", + "desktop.menu.reloadWebview": "重新加载 Webview", + "desktop.menu.restart": "重启", + "desktop.menu.exportLogs": "导出日志...", + "desktop.menu.newSession": "新建会话", + "desktop.menu.openProject": "打开项目...", + "desktop.menu.newWindow": "新建窗口", + "desktop.menu.closeWindow": "关闭窗口", + "desktop.menu.undo": "撤销", + "desktop.menu.redo": "重做", + "desktop.menu.cut": "剪切", + "desktop.menu.copy": "复制", + "desktop.menu.paste": "粘贴", + "desktop.menu.delete": "删除", + "desktop.menu.selectAll": "全选", + "desktop.menu.toggleSidebar": "切换侧边栏", + "desktop.menu.toggleTerminal": "切换终端", + "desktop.menu.toggleFileTree": "切换文件树", + "desktop.menu.reload": "重新加载", + "desktop.menu.toggleDeveloperTools": "切换开发人员工具", + "desktop.menu.actualSize": "实际大小", + "desktop.menu.zoomIn": "放大", + "desktop.menu.zoomOut": "缩小", + "desktop.menu.toggleFullScreen": "切换全屏显示", + "desktop.menu.back": "后退", + "desktop.menu.forward": "前进", + "desktop.menu.previousSession": "上一个会话", + "desktop.menu.nextSession": "下一个会话", + "desktop.menu.previousProject": "上一个项目", + "desktop.menu.nextProject": "下一个项目", + "desktop.menu.minimize": "最小化", + "desktop.menu.maximize": "最大化", + "desktop.menu.documentation": "OpenCode 文档", + "desktop.menu.supportForum": "支持论坛", + "desktop.menu.shareFeedback": "提供反馈", + "desktop.menu.reportBug": "报告错误", + "desktop.menu.ariaLabel": "OpenCode 菜单", + + "desktop.updater.dialog.checkFailed.message": "检查更新失败。", + "desktop.updater.dialog.checkFailed.title": "更新错误", + "desktop.updater.dialog.upToDate.message": "你使用的是最新版本。", + "desktop.updater.dialog.upToDate.title": "无可用更新", + "desktop.updater.dialog.ready.message": "已下载更新 {{version}}。是否立即重启?", + "desktop.updater.dialog.ready.title": "更新已就绪", + "desktop.updater.dialog.restart": "重启", + "desktop.updater.dialog.later": "稍后", + + "desktop.recovery.action.relaunch": "重新启动", + "desktop.recovery.action.exportLogs": "导出日志", + "desktop.recovery.action.keepWaiting": "继续等待", + "desktop.recovery.action.quit": "退出", + "desktop.recovery.loadFailed": "OpenCode 加载失败", + "desktop.recovery.terminated": "OpenCode 窗口意外终止", + "desktop.recovery.unresponsive": "OpenCode 未响应", + "desktop.recovery.unresponsive.detail": "你可以重新启动应用、打开日志或继续等待。", + "desktop.recovery.loadFailed.detail": "窗口:{{window}}\nURL:{{url}}\n错误:{{code}} {{description}}", + "desktop.recovery.terminated.detail": "窗口:{{window}}\n原因:{{reason}}\n代码:{{code}}", + "desktop.recovery.unknown": "<未知>", + + "desktop.dialog.chooseFolder": "选择文件夹", + "desktop.dialog.chooseFile": "选择文件", + "desktop.dialog.saveFile": "保存文件", + "desktop.dialog.files": "文件", + "desktop.server.local": "本地服务器", + + "desktop.wsl.error.windowsOnly": "WSL 仅在 Windows 上可用", + "desktop.wsl.error.unavailable": "WSL 不可用", + "desktop.wsl.error.listInstalled": "无法列出已安装的 WSL 发行版", + "desktop.wsl.error.listOnline": "无法列出在线 WSL 发行版", + "desktop.wsl.error.executeDistro": "无法在发行版中执行命令", + "desktop.wsl.error.installWsl": "WSL 安装失败", + "desktop.wsl.error.installDistro": "无法安装发行版:{{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode 安装失败", + "desktop.wsl.error.alreadyAdded": "已添加 {{distro}}", + "desktop.wsl.error.opencodeMissing": "此发行版中未安装 opencode", + "desktop.wsl.error.opencodeCannotRun": "已安装 opencode,但无法运行", + "desktop.wsl.error.opencodeNotInstalled": "{{distro}} 中未安装 OpenCode", + "desktop.wsl.error.updateVersion": + "OpenCode 更新已完成,但 {{distro}} 仍报告版本 {{installed}};预期版本为 {{expected}}", + "desktop.wsl.error.noVersion": "无版本信息", + "desktop.wsl.error.serverExited": "WSL 服务器启动后退出(code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL 服务器在进入正常状态前退出(code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} 的 Sidecar 运行状况检查在 {{timeout}}ms 后超时", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} 在 {{timeout}}ms 后超时", + "desktop.wsl.error.failedPort": "无法获取端口", + + "desktop.picker.error.notSelected": "文件选择器未选择文件", + "desktop.picker.error.sizeLimit": "所选附件超过 {{limit}} MB 的限制", + + "command.category.suggested": "建议", + "command.category.view": "视图", + "command.category.project": "项目", + "command.category.provider": "提供商", + "command.category.server": "服务器", + "command.category.session": "会话", + "command.category.theme": "主题", + "command.category.language": "语言", + "command.category.file": "文件", + "command.category.context": "上下文", + "command.category.terminal": "终端", + "command.category.model": "模型", + "command.category.mcp": "MCP", + "command.category.agent": "智能体", + "command.category.permissions": "权限", + "command.category.workspace": "工作区", + "command.category.settings": "设置", + "command.logs.export": "导出日志", + + "theme.scheme.system": "系统", + "theme.scheme.light": "浅色", + "theme.scheme.dark": "深色", + + "command.sidebar.toggle": "切换侧边栏", + + "command.project.open": "打开项目", + "command.project.previous": "上一个项目", + "command.project.next": "下一个项目", + "command.project.index": "切换到项目 {{index}}", + + "command.provider.connect": "连接提供商", + + "command.server.switch": "切换服务器", + + "command.settings.open": "打开设置", + + "command.session.previous": "上一个会话", + "command.session.next": "下一个会话", + "command.session.previous.unseen": "上一个未读会话", + "command.session.next.unseen": "下一个未读会话", + "command.session.archive": "归档会话", + + "command.palette": "命令面板", + + "command.theme.cycle": "切换主题", + "command.theme.set": "使用主题:{{theme}}", + "command.theme.scheme.cycle": "切换配色方案", + "command.theme.scheme.set": "使用配色方案:{{scheme}}", + + "command.language.cycle": "切换语言", + "command.language.set": "使用语言:{{language}}", + + "command.session.new": "新建会话", + + "command.file.open": "打开文件", + + "command.tab.close": "关闭标签页", + "command.tab.reopenClosed": "重新打开已关闭的标签页", + + "command.context.addSelection": "将所选内容添加到上下文", + "command.context.addSelection.description": "添加当前文件中选中的行", + + "command.input.focus": "聚焦输入框", + + "command.terminal.toggle": "切换终端", + + "command.fileTree.toggle": "切换文件树", + + "command.review.toggle": "切换审查", + + "command.terminal.new": "新建终端", + "command.terminal.new.description": "创建新的终端标签页", + + "command.steps.toggle": "切换步骤", + "command.steps.toggle.description": "显示或隐藏当前消息的步骤", + + "command.message.previous": "上一条消息", + "command.message.previous.description": "跳转到上一条用户消息", + "command.message.next": "下一条消息", + "command.message.next.description": "跳转到下一条用户消息", + + "command.model.choose": "选择模型", + "command.model.choose.description": "选择不同的模型", + + "command.mcp.toggle": "启用或禁用 MCP 服务器", + "command.mcp.toggle.description": "启用或禁用 MCP 服务器", + + "command.agent.cycle": "切换智能体", + "command.agent.cycle.description": "切换到下一个智能体", + "command.agent.cycle.reverse": "反向切换智能体", + "command.agent.cycle.reverse.description": "切换到上一个智能体", + + "command.model.variant.cycle": "切换思考强度", + "command.model.variant.cycle.description": "切换到下一个强度等级", + + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + + "command.permissions.autoaccept.enable": "自动接受权限", + "command.permissions.autoaccept.disable": "停止自动接受权限", + + "command.workspace.toggle": "切换工作区", + "command.workspace.toggle.description": "在侧边栏启用或禁用多个工作区", + + "command.session.undo": "撤销", + "command.session.undo.description": "撤销上一条消息", + "command.session.redo": "重做", + "command.session.redo.description": "重做上一条撤销的消息", + "command.session.compact": "压缩会话", + "command.session.compact.description": "总结会话以减少上下文大小", + "command.session.fork": "从消息创建新会话", + "command.session.fork.description": "从之前的消息创建新会话", + "command.session.share": "分享会话", + "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": "未找到结果", + "palette.group.commands": "命令", + "palette.group.files": "文件", + + "dialog.provider.search.placeholder": "搜索提供商", + "dialog.provider.empty": "未找到提供商", + "dialog.provider.group.popular": "热门", + "dialog.provider.group.other": "其他", + "dialog.provider.custom.label": "自定义 OpenAI 兼容提供商", + "dialog.provider.tag.recommended": "推荐", + "dialog.provider.opencode.note": "精选模型,包括 Claude、GPT、Gemini 等", + "dialog.provider.opencode.tagline": "可靠的优化模型", + "dialog.provider.opencodeGo.tagline": "适合所有人的低成本订阅", + "dialog.provider.anthropic.note": "直接使用 Claude 模型,包括 Pro 和 Max", + "dialog.provider.copilot.note": "通过 GitHub Copilot 使用辅助编程 AI 模型", + "dialog.provider.openai.note": "适合快速处理各类 AI 任务的 GPT 模型", + "dialog.provider.google.note": "可快速生成结构化回复的 Gemini 模型", + "dialog.provider.openrouter.note": "通过一个提供商使用所有受支持的模型", + "dialog.provider.vercel.note": "通过智能路由统一使用 AI 模型", + + "dialog.model.select.title": "选择模型", + "dialog.model.search.placeholder": "搜索模型", + "dialog.model.empty": "未找到模型", + "dialog.model.manage": "管理模型", + "dialog.model.manage.description": "自定义模型选择器中显示的模型。", + "dialog.model.manage.provider.toggle": "切换所有 {{provider}} 模型", + "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型", + "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", + "dialog.model.unpaid.viewMoreProviders": "查看另外 70 多个提供商", + + "dialog.provider.viewAll": "查看更多提供商", + + "provider.connect.title": "连接 {{provider}}", + "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录", + "provider.connect.selectMethod": "选择 {{provider}} 的登录方式。", + "provider.connect.method.apiKey": "API 密钥", + "provider.connect.method.browser": "浏览器", + "provider.connect.method.headless": "无头模式", + "provider.connect.status.inProgress": "正在授权...", + "provider.connect.status.waiting": "等待授权...", + "provider.connect.status.failed": "授权失败:{{error}}", + "provider.connect.apiKey.description": + "输入你的 {{provider}} API 密钥以连接账户,并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.apiKey.label": "{{provider}} API 密钥", + "provider.connect.apiKey.placeholder": "API 密钥", + "provider.connect.apiKey.required": "API 密钥为必填项", + "provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。", + "provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。", + "provider.connect.opencodeZen.visit.prefix": "访问 ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。", + "provider.connect.oauth.code.visit.prefix": "访问 ", + "provider.connect.oauth.code.visit.link": "此链接", + "provider.connect.oauth.code.visit.suffix": " 获取授权码,以连接你的账户并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.oauth.code.label": "{{method}} 授权码", + "provider.connect.oauth.code.placeholder": "授权码", + "provider.connect.oauth.code.required": "授权码为必填项", + "provider.connect.oauth.code.invalid": "授权码无效", + "provider.connect.oauth.auto.visit.prefix": "访问 ", + "provider.connect.oauth.auto.visit.link": "此链接", + "provider.connect.oauth.auto.visit.suffix": " 并输入以下代码,以连接你的账户并在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.oauth.auto.confirmationCode": "确认码", + "provider.connect.toast.connected.title": "{{provider}} 已连接", + "provider.connect.toast.connected.description": "现在可以使用 {{provider}} 模型了。", + + "provider.custom.title": "自定义提供商", + "provider.custom.unavailable": "此服务器上无法使用自定义提供商", + "provider.custom.description.prefix": "配置与 OpenAI 兼容的提供商。请查看", + "provider.custom.description.link": "提供商配置文档", + "provider.custom.description.suffix": "。", + "provider.custom.field.providerID.label": "提供商 ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "使用小写字母、数字、连字符或下划线", + "provider.custom.field.name.label": "显示名称", + "provider.custom.field.name.placeholder": "我的 AI 提供商", + "provider.custom.field.baseURL.label": "基础 URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API 密钥", + "provider.custom.field.apiKey.placeholder": "API 密钥", + "provider.custom.field.apiKey.description": "可选。如果你通过请求头管理认证,可留空。", + "provider.custom.models.label": "模型", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "名称", + "provider.custom.models.name.placeholder": "显示名称", + "provider.custom.models.remove": "移除模型", + "provider.custom.models.add": "添加模型", + "provider.custom.headers.label": "请求头(可选)", + "provider.custom.headers.key.label": "请求头", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "值", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "移除请求头", + "provider.custom.headers.add": "添加请求头", + "provider.custom.error.providerID.required": "提供商 ID 为必填项", + "provider.custom.error.providerID.format": "请使用小写字母、数字、连字符或下划线", + "provider.custom.error.providerID.exists": "该提供商 ID 已存在", + "provider.custom.error.name.required": "显示名称为必填项", + "provider.custom.error.baseURL.required": "基础 URL 为必填项", + "provider.custom.error.baseURL.format": "必须以 http:// 或 https:// 开头", + "provider.custom.error.required": "必填", + "provider.custom.error.duplicate": "重复", + + "provider.disconnect.toast.disconnected.title": "{{provider}} 已断开连接", + "provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。", + + "model.tag.free": "免费", + "model.tag.latest": "最新", + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "文本", + "model.input.image": "图像", + "model.input.audio": "音频", + "model.input.video": "视频", + "model.input.pdf": "PDF", + "model.tooltip.allows": "支持:{{inputs}}", + "model.tooltip.context.label": "上下文", + "model.tooltip.inputs": "输入", + "model.tooltip.model": "模型", + "model.tooltip.provider": "提供商", + "model.tooltip.reasoning": "推理", + "model.tooltip.reasoning.allowed": "支持推理", + "model.tooltip.reasoning.none": "不支持推理", + "model.tooltip.context": "上下文上限 {{limit}}", + + "common.search.placeholder": "搜索", + "common.goBack": "返回", + "common.goForward": "前进", + "common.loading": "加载中", + "common.loading.ellipsis": "...", + "common.cancel": "取消", + "common.clear": "清除", + "common.connect": "连接", + "common.disconnect": "断开连接", + "common.continue": "继续", + "common.submit": "提交", + "common.save": "保存", + "common.saving": "保存中...", + "common.default": "默认", + "common.attachment": "附件", + + "prompt.placeholder.shell": "输入 shell 命令... {{example}}", + "prompt.placeholder.normal": '随便问点什么... "{{example}}"', + "prompt.placeholder.simple": "随便问点什么...", + "prompt.placeholder.summarizeComments": "总结评论…", + "prompt.placeholder.summarizeComment": "总结该评论…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "按 esc 退出", + "session.child.promptDisabled": "无法向子智能体会话发送提示词。", + "session.child.backToParent": "返回主会话。", + "prompt.example.1": "修复代码库中的一个 TODO", + "prompt.example.2": "这个项目的技术栈是什么?", + "prompt.example.3": "修复失败的测试", + "prompt.example.4": "解释认证是如何工作的", + "prompt.example.5": "查找并修复安全漏洞", + "prompt.example.6": "为用户服务添加单元测试", + "prompt.example.7": "重构这个函数,让它更易读", + "prompt.example.8": "这个错误是什么意思?", + "prompt.example.9": "帮我调试这个问题", + "prompt.example.10": "生成 API 文档", + "prompt.example.11": "优化数据库查询", + "prompt.example.12": "添加输入校验", + "prompt.example.13": "创建一个新的组件用于...", + "prompt.example.14": "我该如何部署这个项目?", + "prompt.example.15": "审查我的代码并给出最佳实践建议", + "prompt.example.16": "为这个函数添加错误处理", + "prompt.example.17": "解释这个正则表达式", + "prompt.example.18": "把它转换成 TypeScript", + "prompt.example.19": "在整个代码库中添加日志", + "prompt.example.20": "哪些依赖已经过期?", + "prompt.example.21": "帮我写一个迁移脚本", + "prompt.example.22": "为这个接口实现缓存", + "prompt.example.23": "给这个列表添加分页", + "prompt.example.24": "创建一个 CLI 命令用于...", + "prompt.example.25": "这里的环境变量是怎么工作的?", + "prompt.popover.emptyResults": "没有匹配的结果", + "prompt.popover.emptyCommands": "没有匹配的命令", + "prompt.dropzone.label": "将图片、PDF 或文本文件拖放到此处", + "prompt.dropzone.file.label": "拖放以 @提及文件", + "prompt.slash.badge.custom": "自定义", + "prompt.slash.badge.skill": "技能", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "当前", + "prompt.context.includeActiveFile": "包含当前文件", + "prompt.context.removeActiveFile": "从上下文移除活动文件", + "prompt.context.removeFile": "从上下文移除文件", + "prompt.action.attachFile": "附加文件", + "prompt.menu.addImagesAndFiles": "添加文件及更多内容", + "prompt.menu.imagesAndFiles": "图片和文件", + "prompt.menu.commands": "命令", + "prompt.menu.context": "上下文", + "prompt.menu.shellCommand": "shell 命令", + "prompt.attachment.remove": "移除附件", + "prompt.action.send": "发送", + "prompt.action.stop": "停止", + "prompt.toast.pasteUnsupported.title": "不支持的附件", + "prompt.toast.attachmentDuplicate.title": "此文件已上传", + "prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。", + "prompt.toast.modelAgentRequired.title": "请选择智能体和模型", + "prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。", + "prompt.toast.worktreeCreateFailed.title": "创建工作区失败", + "prompt.toast.sessionCreateFailed.title": "创建会话失败", + "prompt.toast.shellSendFailed.title": "发送 shell 命令失败", + "prompt.toast.commandSendFailed.title": "发送命令失败", + "prompt.toast.promptSendFailed.title": "发送提示失败", + "prompt.toast.promptSendFailed.description": "无法获取会话", + + "dialog.mcp.title": "MCP 服务器", + "dialog.mcp.description": "已启用 {{enabled}} / {{total}}", + "dialog.mcp.empty": "未配置 MCP 服务器", + + "dialog.lsp.empty": "已根据文件类型自动检测 LSP", + + "dialog.plugins.empty": "在 opencode.json 中配置的插件", + + "mcp.status.connected": "已连接", + "mcp.status.failed": "失败", + "mcp.status.needs_auth": "需要授权", + "mcp.auth.clickToAuthenticate": "点击进行授权", + "mcp.status.disabled": "已禁用", + + "dialog.fork.empty": "没有可用于创建新会话的消息", + + "dialog.directory.search.placeholder": "搜索文件夹", + "dialog.directory.empty": "未找到文件夹", + "dialog.directory.action.selectFile": "选择文件", + "dialog.directory.action.selectFolder": "选择文件夹", + "dialog.directory.root": "根目录", + "dialog.directory.parent": "上级目录", + "dialog.directory.readError": "无法读取此文件夹", + + "dialog.server.title": "服务器", + "dialog.server.description": "切换此应用连接的 OpenCode 服务器。", + "dialog.server.search.placeholder": "搜索服务器", + "dialog.server.empty": "暂无服务器", + "dialog.server.add.title": "添加服务器", + "dialog.server.add.url": "服务器 URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "无法连接到服务器", + "dialog.server.add.checking": "检查中...", + "dialog.server.add.button": "添加服务器", + "dialog.server.add.name": "服务器名称(可选)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "用户名(可选)", + "dialog.server.add.password": "密码(可选)", + "dialog.server.edit.title": "编辑服务器", + "dialog.server.default.title": "默认服务器", + "dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。", + "dialog.server.default.none": "未选择服务器", + "dialog.server.default.set": "将当前服务器设为默认", + "dialog.server.default.clear": "清除", + "dialog.server.action.remove": "移除服务器", + "dialog.server.menu.edit": "编辑", + "dialog.server.menu.default": "设为默认", + "dialog.server.menu.defaultRemove": "取消默认", + "dialog.server.menu.delete": "删除", + "dialog.server.current": "当前服务器", + "dialog.server.status.default": "默认", + "wsl.server.add": "添加 WSL 服务器", + "wsl.server.addShort": "添加 WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL 服务器", + "wsl.server.retryStart": "重试启动", + "wsl.server.updating": "正在更新...", + "wsl.onboarding.step.distro": "选择发行版", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "正在检查 WSL...", + "wsl.onboarding.restartRequired": "Windows 需要重启才能完成 WSL 安装。", + "wsl.onboarding.ready": "WSL 已就绪。", + "wsl.onboarding.required": "需要 WSL 才能继续。", + "wsl.onboarding.checkingDistros": "正在检查发行版...", + "wsl.onboarding.installingDistro": "正在安装 {{distro}}...", + "wsl.onboarding.checkingDistro": "正在检查 {{distro}}...", + "wsl.onboarding.listingDistros": "正在列出发行版...", + "wsl.onboarding.distroReady": "{{distro}} 已就绪。", + "wsl.onboarding.distroNotInstalled": "{{distro}} 尚未安装。", + "wsl.onboarding.openDistroOnce": "打开一次 {{distro}} 以完成设置。", + "wsl.onboarding.finishingDistro": "正在完成 {{distro}} 的设置。", + "wsl.onboarding.pickDistro": "选择一个发行版,或在下方安装。", + "wsl.onboarding.checkingOpencode": "正在检查 OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "正在检查 {{distro}} 中的 OpenCode...", + "wsl.onboarding.updatingOpencode": "正在更新 OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "正在更新 {{distro}} 中的 OpenCode...", + "wsl.onboarding.updateOpencodeIn": "更新 {{distro}} 中的 OpenCode。", + "wsl.onboarding.updateOpencode": "更新 OpenCode", + "wsl.onboarding.opencodeReadyIn": "{{distro}} 中的 OpenCode 已就绪。", + "wsl.onboarding.opencodeReady": "OpenCode 已就绪。", + "wsl.onboarding.installOpencodeIn": "在 {{distro}} 中安装 OpenCode。", + "wsl.onboarding.installOpencode": "安装 OpenCode", + "wsl.onboarding.chooseDistroFirst": "请先选择发行版。", + "wsl.onboarding.loadFailed": "加载 WSL 状态失败。", + "wsl.onboarding.loading": "加载中...", + "wsl.onboarding.installedDistros": "已安装的发行版", + "wsl.onboarding.checkAgain": "重新检查", + "wsl.onboarding.distroStatus.ready": "已就绪", + "wsl.onboarding.distroStatus.checking": "检查中...", + "wsl.onboarding.distroStatus.opencodeMissing": "未安装 OpenCode", + "wsl.onboarding.distroStatus.missingTools": "缺少 bash、curl", + "wsl.onboarding.distroStatus.unsupported": "不支持 · 请使用 WSL 2", + "wsl.onboarding.needAnotherDistro": "需要其他发行版?", + "wsl.onboarding.needAnotherDistroHint": "从 WSL 目录安装 Linux 发行版", + "wsl.onboarding.wslNotInstalled.title": "未安装 WSL", + "wsl.onboarding.wslNotInstalled.description": "OpenCode 需要 WSL(Windows Subsystem for Linux)才能添加 WSL 服务器", + "wsl.onboarding.wslUnavailable.title": "WSL 不可用", + "wsl.onboarding.wslUnavailable.description": "OpenCode 无法验证此设备上的 WSL。", + "wsl.onboarding.installWsl": "安装 WSL", + "wsl.onboarding.windowsRestartRequired": "重启 Windows 以完成 WSL 安装,然后重新打开 OpenCode。", + "wsl.onboarding.next": "下一步", + "wsl.onboarding.refresh": "刷新", + "wsl.onboarding.allDistrosAdded": "所有已安装的发行版均已添加。", + "wsl.onboarding.noDistros": "尚未检测到发行版。", + "wsl.onboarding.install": "安装", + "wsl.onboarding.installing": "正在安装...", + "wsl.onboarding.installDistro": "安装发行版", + "wsl.onboarding.searchDistros": "搜索发行版", + "wsl.onboarding.wsl2Required": "需要 WSL 2。", + "wsl.onboarding.toolsRequired": "此发行版需要 bash 和 curl。", + "wsl.onboarding.openTerminal": "打开终端", + "wsl.onboarding.path": "路径:{{path}}", + "wsl.onboarding.notFound": "未找到", + "wsl.onboarding.version": "版本:{{version}}", + "wsl.onboarding.unknown": "未知", + "wsl.onboarding.desktopVersion": "桌面版 {{version}}", + "wsl.onboarding.versionMismatch": "已安装版本与桌面应用版本不一致。", + "wsl.onboarding.adding": "正在添加...", + + "help.tabs.toast.ariaLabel": "标签页功能介绍。使用标签页整理工作和进行中的会话", + "help.tabs.toast.dismiss": "关闭标签页功能介绍", + "help.tabs.title": "标签页功能介绍", + "help.tabs.description": "使用标签页整理工作和进行中的会话", + "help.tabs.date": "7 月 14 日", + "help.tabs.introduction": "OpenCode Desktop 现已采用以标签页为核心的设计。", + "help.tabs.sessions": + "在标签页中开始新会话,或从任意项目打开现有会话。开始处理新任务时打开新标签页,完成后将其关闭。", + "help.tabs.organize": + "保持打开几个标签页,可更轻松地整理进行中的会话。如果打算保留某个标签页,请为其指定一个便于记忆的名称。", + "help.tabs.home": "你可以在新的主页中找到所有会话和项目。选择会话即可在标签页中将其打开。", + "help.tabs.persistence": "重新打开应用时,标签页仍会保持打开。", + "help.tabs.worktrees": + "新设计暂不支持 Git Worktrees,此功能即将推出。如果希望继续使用之前的布局,可在“设置”中切换布局。请注意,新布局将在几周后永久启用。", + + "dialog.project.edit.title": "编辑项目", + "dialog.project.edit.name": "名称", + "dialog.project.edit.icon": "图标", + "dialog.project.edit.icon.alt": "项目图标", + "dialog.project.edit.icon.hint": "点击或拖拽图片", + "dialog.project.edit.icon.recommended": "建议:128x128px", + "dialog.project.edit.color": "颜色", + "dialog.project.edit.color.select": "选择颜色:{{color}}", + "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": "系统", + "context.breakdown.user": "用户", + "context.breakdown.assistant": "助手", + "context.breakdown.tool": "工具调用", + "context.breakdown.other": "其他", + "context.systemPrompt.title": "系统提示词", + "context.rawMessages.title": "原始消息", + "context.export.session": "导出会话", + + "context.stats.session": "会话", + "context.stats.messages": "消息数", + "context.stats.provider": "提供商", + "context.stats.model": "模型", + "context.stats.limit": "上下文限制", + "context.stats.totalTokens": "总 token", + "context.stats.usage": "使用率", + "context.stats.inputTokens": "输入 token", + "context.stats.outputTokens": "输出 token", + "context.stats.reasoningTokens": "推理 token", + "context.stats.cacheTokens": "缓存 token(读/写)", + "context.stats.userMessages": "用户消息", + "context.stats.assistantMessages": "助手消息", + "context.stats.totalCost": "总成本", + "context.stats.sessionCreated": "创建时间", + "context.stats.lastActivity": "最后活动", + "context.usage.tokens": "Token", + "context.usage.usage": "使用率", + "context.usage.cost": "成本", + "context.usage.clickToView": "点击查看上下文", + "context.usage.view": "查看上下文用量", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "语言", + "toast.language.description": "已切换到{{language}}", + "toast.theme.title": "主题已切换", + "toast.scheme.title": "配色方案", + "toast.workspace.enabled.title": "工作区已启用", + "toast.workspace.enabled.description": "侧边栏现在显示多个工作树", + "toast.workspace.disabled.title": "工作区已禁用", + "toast.workspace.disabled.description": "侧边栏只显示主工作树", + "toast.permissions.autoaccept.on.title": "正在自动接受权限", + "toast.permissions.autoaccept.on.description": "权限请求将被自动批准", + "toast.permissions.autoaccept.off.title": "已停止自动接受权限", + "toast.permissions.autoaccept.off.description": "权限请求将需要批准", + "toast.model.none.title": "未选择模型", + "toast.model.none.description": "请先连接提供商以总结此会话", + "toast.file.loadFailed.title": "加载文件失败", + "toast.file.listFailed.title": "列出文件失败", + "toast.context.noLineSelection.title": "未选择行", + "toast.context.noLineSelection.description": "请先在文件标签中选择行范围。", + "toast.session.share.copyFailed.title": "无法复制链接到剪贴板", + "toast.session.share.success.title": "会话已分享", + "toast.session.share.success.description": "分享链接已复制到剪贴板", + "toast.session.share.failed.title": "分享会话失败", + "toast.session.share.failed.description": "分享会话时发生错误", + "toast.session.unshare.success.title": "已取消分享会话", + "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}}) 可安装。", + "toast.update.action.installRestart": "安装并重启", + "toast.update.action.notYet": "稍后", + + "error.page.title": "出了点问题", + "error.page.description": "加载应用程序时发生错误。", + "error.page.description.localServerStartup": "启动本地服务器时发生错误。", + "error.page.details.label": "错误详情", + "error.page.action.restart": "重启", + "error.page.action.report": "上报错误", + "error.page.action.reported": "错误已上报", + "error.page.action.exportLogs": "导出日志", + "error.page.action.checking": "检查中...", + "error.page.action.checkUpdates": "检查更新", + "error.page.action.updateTo": "更新到 {{version}}", + "error.page.report.prefix": "请将此错误报告给 OpenCode 团队", + "error.page.report.discord": "在 Discord 上", + "error.page.version": "版本:{{version}}", + "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?", + "error.serverSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", + + "directory.error.invalidUrl": "URL 中的目录无效。", + + "error.chain.unknown": "未知错误", + "error.chain.causedBy": "原因:", + "error.chain.apiError": "API 错误", + "error.chain.status": "状态:{{status}}", + "error.chain.retryable": "可重试:{{retryable}}", + "error.chain.responseBody": "响应内容:\n{{body}}", + "error.chain.didYouMean": "你是不是想输入:{{suggestions}}", + "error.chain.modelNotFound": "未找到模型:{{provider}}/{{model}}", + "error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称", + "error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意:OpenCode 暂不支持 MCP 认证。', + "error.chain.providerAuthFailed": "提供商认证失败({{provider}}):{{message}}", + "error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。', + "error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)", + "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C):{{message}}", + "error.chain.configDirectoryTypo": + '{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。', + "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}", + "error.chain.configInvalid": "配置文件 {{path}} 无效", + "error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效:{{message}}", + + "notification.permission.title": "需要权限", + "notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限", + "notification.question.title": "问题", + "notification.question.description": "{{sessionTitle}}({{projectName}})有一个问题", + "notification.action.goToSession": "前往会话", + "notification.session.responseReady.title": "回复已就绪", + "notification.session.error.title": "会话错误", + "notification.session.error.fallbackDescription": "发生错误", + + "home.recentProjects": "最近项目", + "home.empty.title": "没有最近项目", + "home.empty.description": "通过打开本地项目开始使用", + "home.title": "主页", + "home.projects": "项目", + "home.project.add": "添加项目", + "home.recentlyClosed": "最近关闭", + "home.server.collapse": "折叠服务器项目", + "home.server.expand": "展开服务器项目", + "home.sessions.search.placeholder": "搜索会话", + "home.sessions.search.placeholder.scoped": "在 {{scope}} 中搜索会话", + "home.sessions.search.sessions": "会话", + "home.sessions.search.noResults": "未找到与 {{query}} 相关的会话", + "home.sessions.empty": "这里还没有内容", + "home.sessions.empty.description": "创建一个会话以开始。", + "home.sessions.group.today": "今天", + "home.sessions.group.yesterday": "昨天", + "home.sessions.group.older": "更早", + "home.providerTip": "连接 75 个以上的提供商,使用包括 Claude、GPT、Gemini 在内的更多模型", + + "session.tab.session": "会话", + "session.tab.review": "审查", + "session.tab.context": "上下文", + "session.tab.unknown": "未知会话", + "session.panel.reviewAndFiles": "审查和文件", + "session.error.notFound": "找不到此会话", + "session.error.notFound.description": "此标签页指向的会话已不存在于此服务器上。", + "session.error.notFound.closeTab": "关闭标签页", + "session.error.serverConnection": "无法连接到此服务器", + "session.review.filesChanged": "{{count}} 个文件已更改", + "session.review.change.one": "更改", + "session.review.change.other": "更改", + "session.review.loadingChanges": "正在加载更改...", + "session.review.empty": "此会话暂无更改", + "session.review.noVcs": "未检测到 Git,无法显示更改", + "session.review.noSnapshot": "配置中已禁用快照跟踪,因此会话更改不可用", + "session.review.noChanges": "无更改", + "session.review.noUncommittedChanges": "尚无未提交的更改", + "session.review.noBranchChanges": "尚无分支更改", + "session.files.selectToOpen": "选择要打开的文件", + "session.files.all": "所有文件", + "session.files.empty": "无文件", + "session.files.binaryContent": "二进制文件(无法显示内容)", + "session.messages.renderEarlier": "显示更早的消息", + "session.messages.loadingEarlier": "正在加载更早的消息...", + "session.messages.loadEarlier": "加载更早的消息", + "session.messages.loading": "正在加载消息...", + "session.messages.jumpToLatest": "跳转到最新", + "session.context.addToContext": "将 {{selection}} 添加到上下文", + "session.todo.title": "待办事项", + "session.todo.collapse": "折叠", + "session.todo.expand": "展开", + "session.question.minimize": "收起问题", + "session.question.restore": "展开问题", + "session.question.pending.one": "{{count}} 个待处理问题", + "session.question.pending.other": "{{count}} 个待处理问题", + "session.followupDock.summary.one": "{{count}} 条排队消息", + "session.followupDock.summary.other": "{{count}} 条排队消息", + "session.followupDock.sendNow": "立即发送", + "session.followupDock.edit": "编辑", + "session.followupDock.collapse": "折叠排队消息", + "session.followupDock.expand": "展开排队消息", + "session.revertDock.summary.one": "{{count}} 条已回滚消息", + "session.revertDock.summary.other": "{{count}} 条已回滚消息", + "session.revertDock.collapse": "折叠已回滚消息", + "session.revertDock.expand": "展开已回滚消息", + "session.revertDock.restore": "恢复消息", + "session.new.title": "构建任何内容", + "session.new.project.new": "新建项目", + "session.new.project.search": "搜索项目", + "session.new.project.add": "添加项目", + "session.new.worktree.main": "主分支", + "session.new.worktree.mainWithBranch": "主分支({{branch}})", + "session.new.worktree.create": "创建新的 worktree", + "session.new.workspace.runIn": "运行会话的位置", + "session.new.workspace.triggerLocal": "本地", + "session.new.workspace.local": "本地仓库", + "session.new.workspace.existing": "工作区…", + "session.new.git.none": "无 Git", + "session.new.lastModified": "最后修改", + "session.header.search.placeholder": "搜索 {{project}}", + "session.header.searchFiles": "搜索文件", + "session.header.openIn": "打开方式", + "session.header.open.action": "打开 {{app}}", + "session.header.open.ariaLabel": "在 {{app}} 中打开", + "session.header.open.menu": "打开选项", + "session.header.open.copyPath": "复制路径", + + "status.popover.trigger": "状态", + "status.popover.ariaLabel": "服务器配置", + "status.popover.tab.servers": "服务器", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "插件", + "status.popover.action.manageServers": "管理服务器", + + "session.share.popover.title": "发布到网页", + "session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。", + "session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。", + "session.share.action.share": "分享", + "session.share.action.publish": "发布", + "session.share.action.publishing": "正在发布...", + "session.share.action.unpublish": "取消发布", + "session.share.action.unpublishing": "正在取消发布...", + "session.share.action.view": "查看", + "session.share.copy.copied": "已复制", + "session.share.copy.copyLink": "复制链接", + + "lsp.tooltip.none": "没有 LSP 服务器", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "正在加载提示...", + + "terminal.loading": "正在加载终端...", + "terminal.title": "终端", + "terminal.title.numbered": "终端 {{number}}", + "terminal.close": "关闭终端", + "terminal.connectionLost.title": "连接已丢失", + "terminal.connectionLost.description": "终端连接已中断。这可能发生在服务器重启时。", + "terminal.connectTicket.csrfError": "PTY 连接票证因来源或 CSRF 检查被拒绝。请检查服务器 CORS 配置。", + "terminal.connectTicket.statusError": "PTY 连接票证失败,状态为 {{status}}", + + "titlebar.update": "更新", + "titlebar.updateVersion": "更新 {{version}}", + + "common.closeTab": "关闭标签页", + "common.dismiss": "忽略", + "common.requestFailed": "请求失败", + "common.moreOptions": "更多选项", + "common.learnMore": "了解更多", + "common.rename": "重命名", + "common.export": "导出", + "common.reset": "重置", + "common.archive": "归档", + "common.delete": "删除", + "common.close": "关闭", + "common.edit": "编辑", + "common.loadMore": "加载更多", + "common.key.esc": "ESC", + + "sidebar.menu.toggle": "切换菜单", + "sidebar.nav.projectsAndSessions": "项目和会话", + "sidebar.settings": "设置", + "sidebar.help": "帮助", + "sidebar.workspaces.enable": "启用工作区", + "sidebar.workspaces.disable": "禁用工作区", + "sidebar.gettingStarted.title": "入门", + "sidebar.gettingStarted.line1": "OpenCode 提供免费模型,你可以立即开始使用。", + "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。", + "sidebar.project.recentSessions": "最近会话", + "sidebar.project.viewAllSessions": "查看全部会话", + "sidebar.project.clearNotifications": "清除通知", + "sidebar.empty.title": "没有打开的项目", + "sidebar.empty.description": "打开一个项目以开始使用", + + "app.name.desktop": "OpenCode Desktop", + + "settings.section.desktop": "桌面", + "settings.section.server": "服务器", + + "settings.tab.general": "通用", + "settings.tab.shortcuts": "快捷键", + + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL 集成", + "settings.desktop.wsl.description": "在 Windows 的 WSL 环境中运行 OpenCode 服务器。", + + "settings.general.section.appearance": "外观", + "settings.general.section.advanced": "高级", + "settings.general.section.notifications": "系统通知", + "settings.general.section.updates": "更新", + "settings.general.section.sounds": "音效", + "settings.general.section.feed": "动态", + "settings.general.section.display": "显示", + "settings.general.row.language.title": "语言", + "settings.general.row.language.description": "更改 OpenCode 的显示语言", + "settings.general.row.shell.title": "终端 Shell", + "settings.general.row.shell.description": "选择终端使用的 shell。兼容的 shell 也会用于智能体工具调用。", + "settings.general.row.shell.autoDefault": "自动(默认)", + "settings.general.row.shell.terminalOnly": "仅终端", + "settings.general.row.appearance.title": "外观", + "settings.general.row.appearance.description": "自定义 OpenCode 在你的设备上的外观", + "settings.general.row.colorScheme.title": "配色方案", + "settings.general.row.colorScheme.description": "选择 OpenCode 跟随系统、浅色或深色主题", + "settings.general.row.theme.title": "主题", + "settings.general.row.theme.description": "自定义 OpenCode 的主题。", + "settings.general.row.font.title": "代码字体", + "settings.general.row.font.description": "自定义代码块使用的字体", + "settings.general.row.terminalFont.title": "终端字体", + "settings.general.row.terminalFont.description": "自定义终端使用的字体", + "settings.general.row.uiFont.title": "界面字体", + "settings.general.row.uiFont.description": "自定义整个界面使用的字体", + "settings.general.row.followup.title": "后续消息行为", + "settings.general.row.followup.description": "选择后续提示是立即引导当前会话还是排队等待", + "settings.general.row.followup.option.queue": "排队", + "settings.general.row.followup.option.steer": "引导", + "settings.general.row.showFileTree.title": "文件树", + "settings.general.row.showFileTree.description": "在会话中显示文件树面板", + "settings.general.row.showNavigation.title": "导航控件", + "settings.general.row.showNavigation.description": "在桌面版标题栏中显示前进和后退按钮", + "settings.general.row.showSearch.title": "命令面板", + "settings.general.row.showSearch.description": "在标题栏中显示搜索和命令面板按钮", + "settings.general.row.showTerminal.title": "终端", + "settings.general.row.showTerminal.description": "在桌面版标题栏中显示终端按钮", + "settings.general.row.showStatus.title": "服务器状态", + "settings.general.row.showStatus.description": "在标题栏中显示服务器状态按钮", + "settings.general.row.mobileTitlebarBottom.title": "底部导航", + "settings.general.row.mobileTitlebarBottom.description": "在移动设备上将标题栏和会话标签页置于屏幕底部", + "settings.general.row.showCustomAgents.title": "显示智能体", + "settings.general.row.showCustomAgents.description": "在输入框中切换智能体。隐藏时默认使用 Build 智能体。", + "settings.general.row.reasoningSummaries.title": "显示推理摘要", + "settings.general.row.reasoningSummaries.description": "在时间线中显示模型推理摘要", + "settings.general.row.shellToolPartsExpanded.title": "展开 Shell 工具调用", + "settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 Shell 工具调用详情", + "settings.general.row.editToolPartsExpanded.title": "展开编辑工具调用", + "settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具调用详情", + "settings.general.row.newInterface.title": "新布局", + "settings.general.row.newInterface.badge": "新", + "settings.general.row.newInterface.description": "使用新的标签页和主页布局。在有限时间内可在不同布局之间切换。", + "settings.general.row.newInterfaceNotice.title": "你现在使用的是新布局", + "settings.general.row.newInterfaceNotice.description": "之前的布局不再可用", + "settings.general.row.newInterfaceNotice.dismiss": "忽略", + "settings.general.row.pinchZoom.title": "双指缩放", + "settings.general.row.pinchZoom.description": "允许使用触控板双指捏合和 Ctrl+滚轮手势缩放", + "settings.general.row.wayland.title": "使用原生 Wayland", + "settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。", + "settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。", + "settings.general.row.releaseNotes.title": "发行说明", + "settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗", + + "settings.updates.row.startup.title": "启动时检查更新", + "settings.updates.row.startup.description": "在 OpenCode 启动时自动检查更新", + "settings.updates.row.check.title": "检查更新", + "settings.updates.row.check.description": "手动检查更新并在有更新时安装", + "settings.updates.action.checkNow": "立即检查", + "settings.updates.action.checking": "正在检查...", + "settings.updates.action.downloading": "正在下载...", + "settings.updates.action.installing": "正在安装...", + "settings.updates.toast.latest.title": "已是最新版本", + "settings.updates.toast.latest.description": "你正在使用最新版本的 OpenCode。", + + "sound.option.none": "无", + "sound.option.alert01": "警报 01", + "sound.option.alert02": "警报 02", + "sound.option.alert03": "警报 03", + "sound.option.alert04": "警报 04", + "sound.option.alert05": "警报 05", + "sound.option.alert06": "警报 06", + "sound.option.alert07": "警报 07", + "sound.option.alert08": "警报 08", + "sound.option.alert09": "警报 09", + "sound.option.alert10": "警报 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + + "settings.general.notifications.agent.title": "智能体", + "settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知", + "settings.general.notifications.permissions.title": "权限", + "settings.general.notifications.permissions.description": "当需要权限时显示系统通知", + "settings.general.notifications.errors.title": "错误", + "settings.general.notifications.errors.description": "发生错误时显示系统通知", + "settings.general.sounds.agent.title": "智能体", + "settings.general.sounds.agent.description": "当智能体完成或需要注意时播放声音", + "settings.general.sounds.permissions.title": "权限", + "settings.general.sounds.permissions.description": "当需要权限时播放声音", + "settings.general.sounds.errors.title": "错误", + "settings.general.sounds.errors.description": "发生错误时播放声音", + + "settings.shortcuts.title": "键盘快捷键", + "settings.shortcuts.reset.button": "重置为默认值", + "settings.shortcuts.reset.toast.title": "快捷键已重置", + "settings.shortcuts.reset.toast.description": "键盘快捷键已重置为默认设置。", + "settings.shortcuts.conflict.title": "快捷键已被占用", + "settings.shortcuts.conflict.description": "{{keybind}} 已分配给 {{titles}}。", + "settings.shortcuts.unassigned": "未设置", + "settings.shortcuts.pressKeys": "按下按键", + "settings.shortcuts.search.placeholder": "搜索快捷键", + "settings.shortcuts.search.empty": "未找到快捷键", + "settings.shortcuts.group.general": "通用", + "settings.shortcuts.group.session": "会话", + "settings.shortcuts.group.navigation": "导航", + "settings.shortcuts.group.modelAndAgent": "模型与智能体", + "settings.shortcuts.group.terminal": "终端", + "settings.shortcuts.group.prompt": "提示", + + "settings.providers.title": "提供商", + "settings.providers.description": "可在此处配置提供商设置。", + "settings.providers.section.connected": "已连接的提供商", + "settings.providers.connected.empty": "没有已连接的提供商", + "settings.providers.section.popular": "热门提供商", + "settings.providers.tag.environment": "环境", + "settings.providers.tag.config": "配置", + "settings.providers.tag.custom": "自定义", + "settings.providers.tag.other": "其他", + + "settings.models.title": "模型", + "settings.models.description": "可在此处配置模型设置。", + + "settings.agents.title": "智能体", + "settings.agents.description": "可在此处配置智能体设置。", + + "settings.commands.title": "命令", + "settings.commands.description": "可在此处配置命令设置。", + + "settings.mcp.title": "MCP", + "settings.mcp.description": "可在此处配置 MCP 设置。", + + "settings.permissions.title": "权限", + "settings.permissions.description": "控制服务器默认可以使用哪些工具。", + "settings.permissions.section.tools": "工具", + "settings.permissions.toast.updateFailed.title": "更新权限失败", + "settings.permissions.action.allow": "允许", + "settings.permissions.action.ask": "询问", + "settings.permissions.action.deny": "拒绝", + "settings.permissions.tool.read.title": "读取", + "settings.permissions.tool.read.description": "读取文件(匹配文件路径)", + "settings.permissions.tool.edit.title": "编辑", + "settings.permissions.tool.edit.description": "修改文件,包括编辑、写入和应用补丁", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "使用 glob 模式匹配文件", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "使用正则表达式搜索文件内容", + "settings.permissions.tool.list.title": "列出文件", + "settings.permissions.tool.list.description": "列出目录中的文件", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "运行 shell 命令", + "settings.permissions.tool.task.title": "任务", + "settings.permissions.tool.task.description": "启动子智能体", + "settings.permissions.tool.skill.title": "技能", + "settings.permissions.tool.skill.description": "按名称加载技能", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "运行语言服务器查询", + "settings.permissions.tool.todowrite.title": "更新待办", + "settings.permissions.tool.todowrite.description": "更新待办列表", + "settings.permissions.tool.webfetch.title": "获取网页", + "settings.permissions.tool.webfetch.description": "从 URL 获取内容", + "settings.permissions.tool.websearch.title": "网页搜索", + "settings.permissions.tool.websearch.description": "搜索网页", + "settings.permissions.tool.external_directory.title": "外部目录", + "settings.permissions.tool.external_directory.description": "访问项目目录之外的文件", + "settings.permissions.tool.doom_loop.title": "死循环", + "settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用", + + "session.delete.failed.title": "删除会话失败", + "session.delete.title": "删除会话", + "session.delete.confirm": '删除会话 "{{name}}"?', + "session.delete.button": "删除会话", + + "workspace.new": "新建工作区", + "workspace.type.local": "本地", + "workspace.type.sandbox": "沙盒", + "workspace.create.failed.title": "创建工作区失败", + "workspace.delete.failed.title": "删除工作区失败", + "workspace.resetting.title": "正在重置工作区", + "workspace.resetting.description": "这可能需要一点时间。", + "workspace.reset.failed.title": "重置工作区失败", + "workspace.reset.success.title": "工作区已重置", + "workspace.reset.success.description": "工作区现已与默认分支一致。", + "workspace.error.stillPreparing": "工作区仍在准备中", + "workspace.status.checking": "正在检查未合并的更改...", + "workspace.status.error": "无法验证 Git 状态。", + "workspace.status.clean": "未检测到未合并的更改。", + "workspace.status.dirty": "检测到未合并的更改。", + "workspace.delete.title": "删除工作区", + "workspace.delete.confirm": '删除工作区 "{{name}}"?', + "workspace.delete.button": "删除工作区", + "workspace.reset.title": "重置工作区", + "workspace.reset.confirm": '重置工作区 "{{name}}"?', + "workspace.reset.button": "重置工作区", + "workspace.reset.archived.none": "不会归档任何活跃会话。", + "workspace.reset.archived.one": "将归档 1 个会话。", + "workspace.reset.archived.many": "将归档 {{count}} 个会话。", + "workspace.reset.note": "这将把工作区重置为与默认分支一致。", + "common.open": "打开", + "dialog.releaseNotes.action.getStarted": "开始", + "dialog.releaseNotes.action.next": "下一步", + "dialog.releaseNotes.action.hideFuture": "不再显示", + "dialog.releaseNotes.media.alt": "版本说明预览", + "toast.project.reloadFailed.title": "无法重新加载 {{project}}", + "error.server.invalidConfiguration": "配置无效", + "common.moreCountSuffix": " (还有 {{count}} 个)", + "common.time.justNow": "刚刚", + "common.time.minutesAgo.short": "{{count}}分钟前", + "common.time.hoursAgo.short": "{{count}}小时前", + "common.time.daysAgo.short": "{{count}}天前", + "settings.providers.connected.environmentDescription": "已通过环境变量连接", + "settings.providers.custom.description": "通过基础 URL 添加与 OpenAI 兼容的提供商。", + + "app.server.unreachable": "无法连接到 {{server}}", + "app.server.retrying": "正在自动重试...", + "app.server.otherServers": "其他服务器", + "dialog.server.add.usernamePlaceholder": "用户名", + "dialog.server.add.passwordPlaceholder": "密码", + "server.row.noUsername": "无用户名", + "session.review.noVcs.createGit.title": "创建 Git 仓库", + "session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改", + "session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...", + "session.review.noVcs.createGit.action": "创建 Git 仓库", + "session.todo.progress": "已完成 {{done}} 个任务(共 {{total}} 个)", + "session.question.progress": "{{current}}/{{total}} 个问题", + "session.header.open.finder": "访达", + "session.header.open.fileExplorer": "文件资源管理器", + "session.header.reveal.finder": "在访达中显示", + "session.header.reveal.fileExplorer": "在文件资源管理器中显示", + "session.header.reveal.containingFolder": "打开所在文件夹", + "session.header.open.fileManager": "文件管理器", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "终端", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "开发性能诊断", + "debugBar.na": "不适用", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": "最后一次完成的涉及会话页面的路由转换,从路由器启动到稳定后的第一次绘制。", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "过去 5 秒内的滚动帧率。", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "过去 5 秒内最差的帧时间。", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "过去 5 秒内超过 32ms 的帧。", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "过去 5 秒内的阻塞时间和长任务计数。最大任务:{{max}}。", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "过去 5 秒内观察到的最差输入延迟。", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "过去 5 秒内的近似交互持续时间。这类似于 INP,而非官方的 Web Vitals INP。", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "当前应用生命周期的累积布局偏移。", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "使用的 JS 堆与堆限制。仅限 Chromium。", + "debugBar.mem.tip": "使用的 JS 堆与堆限制。{{used}} / {{limit}}。", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "强制所有交互元素显示焦点样式", + "debugBar.focus.on": "开", + "debugBar.focus.off": "关", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "在不更改所选语言的情况下,强制设置整个应用的布局方向", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "空格", + "common.key.backspace": "退格", + "common.key.enter": "回车", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "未知", + "error.page.circular": "[循环]", + "error.serverSDK.noServerAvailable": "无可用服务器", + "error.serverSDK.serverNotAvailable": "服务器不可用", + "error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败", + "error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败", + "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败", + "error.childStore.storeCreateFailed": "创建存储失败", + "terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}", +} satisfies Partial> diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts new file mode 100644 index 0000000000000000000000000000000000000000..6cc3d555289d39248c1a14513ad435eac38863ec --- /dev/null +++ b/packages/app/src/i18n/zht.ts @@ -0,0 +1,1223 @@ +import { dict as en } from "./en" + +type Keys = keyof typeof en + +export const dict = { + "desktop.menu.app": "OpenCode", + "desktop.menu.file": "檔案", + "desktop.menu.edit": "編輯", + "desktop.menu.view": "檢視", + "desktop.menu.go": "前往", + "desktop.menu.window": "視窗", + "desktop.menu.help": "說明", + "desktop.menu.checkForUpdates": "檢查更新...", + "desktop.menu.settings": "設定", + "desktop.menu.reloadWebview": "重新載入 Webview", + "desktop.menu.restart": "重新啟動", + "desktop.menu.exportLogs": "匯出記錄檔...", + "desktop.menu.newSession": "新增工作階段", + "desktop.menu.openProject": "開啟專案...", + "desktop.menu.newWindow": "新增視窗", + "desktop.menu.closeWindow": "關閉視窗", + "desktop.menu.undo": "復原", + "desktop.menu.redo": "重做", + "desktop.menu.cut": "剪下", + "desktop.menu.copy": "複製", + "desktop.menu.paste": "貼上", + "desktop.menu.delete": "刪除", + "desktop.menu.selectAll": "全選", + "desktop.menu.toggleSidebar": "切換側邊欄", + "desktop.menu.toggleTerminal": "切換終端機", + "desktop.menu.toggleFileTree": "切換檔案樹", + "desktop.menu.reload": "重新載入", + "desktop.menu.toggleDeveloperTools": "切換開發人員工具", + "desktop.menu.actualSize": "實際大小", + "desktop.menu.zoomIn": "放大", + "desktop.menu.zoomOut": "縮小", + "desktop.menu.toggleFullScreen": "切換全螢幕", + "desktop.menu.back": "上一頁", + "desktop.menu.forward": "下一頁", + "desktop.menu.previousSession": "上一個工作階段", + "desktop.menu.nextSession": "下一個工作階段", + "desktop.menu.previousProject": "上一個專案", + "desktop.menu.nextProject": "下一個專案", + "desktop.menu.minimize": "最小化", + "desktop.menu.maximize": "最大化", + "desktop.menu.documentation": "OpenCode 文件", + "desktop.menu.supportForum": "支援論壇", + "desktop.menu.shareFeedback": "提供意見回饋", + "desktop.menu.reportBug": "回報錯誤", + "desktop.menu.ariaLabel": "OpenCode 選單", + + "desktop.updater.dialog.checkFailed.message": "檢查更新失敗。", + "desktop.updater.dialog.checkFailed.title": "更新錯誤", + "desktop.updater.dialog.upToDate.message": "目前已是最新版本。", + "desktop.updater.dialog.upToDate.title": "沒有更新", + "desktop.updater.dialog.ready.message": "已下載更新 {{version}}。要立即重新啟動嗎?", + "desktop.updater.dialog.ready.title": "更新已就緒", + "desktop.updater.dialog.restart": "重新啟動", + "desktop.updater.dialog.later": "稍後", + + "desktop.recovery.action.relaunch": "重新啟動應用程式", + "desktop.recovery.action.exportLogs": "匯出記錄檔", + "desktop.recovery.action.keepWaiting": "繼續等待", + "desktop.recovery.action.quit": "結束", + "desktop.recovery.loadFailed": "OpenCode 載入失敗", + "desktop.recovery.terminated": "OpenCode 視窗意外終止", + "desktop.recovery.unresponsive": "OpenCode 沒有回應", + "desktop.recovery.unresponsive.detail": "你可以重新啟動應用程式、開啟記錄檔,或繼續等待。", + "desktop.recovery.loadFailed.detail": "視窗:{{window}}\nURL:{{url}}\n錯誤:{{code}} {{description}}", + "desktop.recovery.terminated.detail": "視窗:{{window}}\n原因:{{reason}}\n代碼:{{code}}", + "desktop.recovery.unknown": "<未知>", + + "desktop.dialog.chooseFolder": "選擇資料夾", + "desktop.dialog.chooseFile": "選擇檔案", + "desktop.dialog.saveFile": "儲存檔案", + "desktop.dialog.files": "檔案", + "desktop.server.local": "本機伺服器", + + "desktop.wsl.error.windowsOnly": "WSL 僅適用於 Windows", + "desktop.wsl.error.unavailable": "WSL 無法使用", + "desktop.wsl.error.listInstalled": "無法列出已安裝的 WSL 發行版", + "desktop.wsl.error.listOnline": "無法列出線上 WSL 發行版", + "desktop.wsl.error.executeDistro": "無法在發行版中執行命令", + "desktop.wsl.error.installWsl": "WSL 安裝失敗", + "desktop.wsl.error.installDistro": "無法安裝發行版:{{distro}}", + "desktop.wsl.error.installOpencode": "OpenCode 安裝失敗", + "desktop.wsl.error.alreadyAdded": "已新增 {{distro}}", + "desktop.wsl.error.opencodeMissing": "此發行版中未安裝 opencode", + "desktop.wsl.error.opencodeCannotRun": "已安裝 opencode,但無法執行", + "desktop.wsl.error.opencodeNotInstalled": "{{distro}} 中未安裝 OpenCode", + "desktop.wsl.error.updateVersion": + "OpenCode 更新完成,但 {{distro}} 回報的版本仍為 {{installed}};預期版本為 {{expected}}", + "desktop.wsl.error.noVersion": "無版本資訊", + "desktop.wsl.error.serverExited": "WSL 伺服器在啟動後結束 (code={{code}} signal={{signal}})", + "desktop.wsl.error.serverExitedBeforeHealthy": + "WSL 伺服器在健全狀態確認前結束 (code={{code}} signal={{signal}}){{output}}", + "desktop.wsl.error.healthTimeout": "{{distro}} 的 Sidecar 健全狀態檢查在 {{timeout}}ms 後逾時", + "desktop.wsl.error.commandTimeout": "{{command}} {{args}} 在 {{timeout}}ms 後逾時", + "desktop.wsl.error.failedPort": "無法取得連接埠", + + "desktop.picker.error.notSelected": "檔案選擇器未選取檔案", + "desktop.picker.error.sizeLimit": "所選附件超過 {{limit}} MB 的限制", + + "command.category.suggested": "建議", + "command.category.view": "檢視", + "command.category.project": "專案", + "command.category.provider": "提供者", + "command.category.server": "伺服器", + "command.category.session": "工作階段", + "command.category.theme": "主題", + "command.category.language": "語言", + "command.category.file": "檔案", + "command.category.context": "上下文", + "command.category.terminal": "終端機", + "command.category.model": "模型", + "command.category.mcp": "MCP", + "command.category.agent": "代理程式", + "command.category.permissions": "權限", + "command.category.workspace": "工作區", + + "command.category.settings": "設定", + "command.logs.export": "匯出記錄檔", + "theme.scheme.system": "系統", + "theme.scheme.light": "淺色", + "theme.scheme.dark": "深色", + + "command.sidebar.toggle": "切換側邊欄", + "command.project.open": "開啟專案", + "command.project.previous": "上一個專案", + "command.project.next": "下一個專案", + "command.project.index": "切換至專案 {{index}}", + "command.provider.connect": "連線提供者", + "command.server.switch": "切換伺服器", + "command.settings.open": "開啟設定", + "command.session.previous": "上一個工作階段", + "command.session.next": "下一個工作階段", + "command.session.previous.unseen": "上一個未讀工作階段", + "command.session.next.unseen": "下一個未讀工作階段", + "command.session.archive": "封存工作階段", + + "command.palette": "命令面板", + + "command.theme.cycle": "依序切換主題", + "command.theme.set": "使用主題: {{theme}}", + "command.theme.scheme.cycle": "依序切換配色方案", + "command.theme.scheme.set": "使用配色方案: {{scheme}}", + + "command.language.cycle": "依序切換語言", + "command.language.set": "使用語言: {{language}}", + + "command.session.new": "新增工作階段", + "command.file.open": "開啟檔案", + "command.tab.close": "關閉分頁", + "command.tab.reopenClosed": "重新開啟已關閉的分頁", + "command.context.addSelection": "將選取內容加入上下文", + "command.context.addSelection.description": "加入目前檔案中選取的行", + "command.input.focus": "聚焦輸入框", + "command.terminal.toggle": "切換終端機", + "command.fileTree.toggle": "切換檔案樹", + "command.review.toggle": "切換檢閱", + "command.terminal.new": "新增終端機", + "command.terminal.new.description": "建立新的終端機分頁", + "command.steps.toggle": "切換步驟", + "command.steps.toggle.description": "顯示或隱藏目前訊息的步驟", + "command.message.previous": "上一則訊息", + "command.message.previous.description": "跳到上一則使用者訊息", + "command.message.next": "下一則訊息", + "command.message.next.description": "跳到下一則使用者訊息", + "command.model.choose": "選擇模型", + "command.model.choose.description": "選擇不同的模型", + "command.mcp.toggle": "切換 MCP", + "command.mcp.toggle.description": "切換 MCP", + "command.agent.cycle": "切換下一個代理程式", + "command.agent.cycle.description": "切換到下一個代理程式", + "command.agent.cycle.reverse": "切換上一個代理程式", + "command.agent.cycle.reverse.description": "切換到上一個代理程式", + "command.model.variant.cycle": "切換思考強度", + "command.model.variant.cycle.description": "切換到下一個強度等級", + "command.prompt.mode.shell": "Shell", + "command.prompt.mode.normal": "Prompt", + "command.permissions.autoaccept.enable": "自動接受權限", + "command.permissions.autoaccept.disable": "停止自動接受權限", + "command.workspace.toggle": "切換工作區", + "command.workspace.toggle.description": "在側邊欄啟用或停用多個工作區", + "command.session.undo": "復原", + "command.session.undo.description": "復原上一則訊息", + "command.session.redo": "重做", + "command.session.redo.description": "重做上一則復原的訊息", + "command.session.compact": "精簡工作階段", + "command.session.compact.description": "摘要工作階段以減少上下文大小", + "command.session.fork": "從訊息分支", + "command.session.fork.description": "從先前的訊息建立新工作階段", + "command.session.share": "分享工作階段", + "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": "找不到結果", + "palette.group.commands": "命令", + "palette.group.files": "檔案", + + "dialog.provider.search.placeholder": "搜尋提供者", + "dialog.provider.empty": "找不到提供者", + "dialog.provider.group.popular": "熱門", + "dialog.provider.group.other": "其他", + "dialog.provider.custom.label": "自訂 OpenAI 相容提供者", + "dialog.provider.tag.recommended": "推薦", + "dialog.provider.opencode.note": "精選模型,包含 Claude、GPT、Gemini 等等", + "dialog.provider.opencode.tagline": "經過最佳化的可靠模型", + "dialog.provider.opencodeGo.tagline": "適合所有人的低成本訂閱", + "dialog.provider.anthropic.note": "直接存取 Claude 模型,包括 Pro 和 Max", + "dialog.provider.openai.note": "適合快速且功能強大的通用 AI 工作之 GPT 模型", + "dialog.provider.copilot.note": "透過 GitHub Copilot 使用程式設計輔助 AI 模型", + "dialog.provider.google.note": "Gemini 模型,提供快速且結構化的回應", + "dialog.provider.openrouter.note": "從單一提供者存取所有支援的模型", + "dialog.provider.vercel.note": "透過智慧路由統一存取 AI 模型", + + "dialog.model.select.title": "選擇模型", + "dialog.model.search.placeholder": "搜尋模型", + "dialog.model.empty": "找不到模型", + "dialog.model.manage": "管理模型", + "dialog.model.manage.description": "自訂模型選擇器中顯示的模型。", + "dialog.model.manage.provider.toggle": "切換所有 {{provider}} 模型", + + "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型", + "dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型", + "dialog.model.unpaid.viewMoreProviders": "查看另外 70 多個提供者", + + "dialog.provider.viewAll": "查看更多提供者", + + "provider.connect.title": "連線 {{provider}}", + "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登入", + "provider.connect.selectMethod": "選擇 {{provider}} 的登入方式。", + "provider.connect.method.apiKey": "API 金鑰", + "provider.connect.method.browser": "瀏覽器", + "provider.connect.method.headless": "無頭模式", + "provider.connect.status.inProgress": "正在授權...", + "provider.connect.status.waiting": "等待授權...", + "provider.connect.status.failed": "授權失敗: {{error}}", + "provider.connect.apiKey.description": + "輸入你的 {{provider}} API 金鑰以連線帳戶,並在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.apiKey.label": "{{provider}} API 金鑰", + "provider.connect.apiKey.placeholder": "API 金鑰", + "provider.connect.apiKey.required": "API 金鑰為必填", + "provider.connect.opencodeZen.line1": "OpenCode Zen 為你提供一組精選的可靠最佳化模型,用於程式碼代理程式。", + "provider.connect.opencodeZen.line2": "只需一個 API 金鑰,你就能使用 Claude、GPT、Gemini、GLM 等模型。", + "provider.connect.opencodeZen.visit.prefix": "造訪 ", + "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", + "provider.connect.opencodeZen.visit.suffix": " 取得你的 API 金鑰。", + "provider.connect.oauth.code.visit.prefix": "造訪 ", + "provider.connect.oauth.code.visit.link": "此連結", + "provider.connect.oauth.code.visit.suffix": " 取得授權碼,以連線你的帳戶並在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.oauth.code.label": "{{method}} 授權碼", + "provider.connect.oauth.code.placeholder": "授權碼", + "provider.connect.oauth.code.required": "授權碼為必填", + "provider.connect.oauth.code.invalid": "授權碼無效", + "provider.connect.oauth.auto.visit.prefix": "造訪 ", + "provider.connect.oauth.auto.visit.link": "此連結", + "provider.connect.oauth.auto.visit.suffix": + " 並輸入以下程式碼,以連線你的帳戶並在 OpenCode 中使用 {{provider}} 模型。", + "provider.connect.oauth.auto.confirmationCode": "確認碼", + "provider.connect.toast.connected.title": "{{provider}} 已連線", + "provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。", + + "provider.custom.title": "自訂提供者", + "provider.custom.unavailable": "此伺服器無法使用自訂提供者", + "provider.custom.description.prefix": "設定與 OpenAI 相容的提供者。請參閱", + "provider.custom.description.link": "提供者設定文件", + "provider.custom.description.suffix": "。", + "provider.custom.field.providerID.label": "提供者 ID", + "provider.custom.field.providerID.placeholder": "myprovider", + "provider.custom.field.providerID.description": "使用小寫字母、數字、連字號或底線", + "provider.custom.field.name.label": "顯示名稱", + "provider.custom.field.name.placeholder": "我的 AI 提供者", + "provider.custom.field.baseURL.label": "基底 URL", + "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", + "provider.custom.field.apiKey.label": "API 金鑰", + "provider.custom.field.apiKey.placeholder": "API 金鑰", + "provider.custom.field.apiKey.description": "選填。若您透過標頭管理驗證,可留空。", + "provider.custom.models.label": "模型", + "provider.custom.models.id.label": "ID", + "provider.custom.models.id.placeholder": "model-id", + "provider.custom.models.name.label": "名稱", + "provider.custom.models.name.placeholder": "顯示名稱", + "provider.custom.models.remove": "移除模型", + "provider.custom.models.add": "新增模型", + "provider.custom.headers.label": "標頭(選填)", + "provider.custom.headers.key.label": "標頭", + "provider.custom.headers.key.placeholder": "Header-Name", + "provider.custom.headers.value.label": "值", + "provider.custom.headers.value.placeholder": "value", + "provider.custom.headers.remove": "移除標頭", + "provider.custom.headers.add": "新增標頭", + "provider.custom.error.providerID.required": "提供者 ID 為必填", + "provider.custom.error.providerID.format": "請使用小寫字母、數字、連字號或底線", + "provider.custom.error.providerID.exists": "該提供者 ID 已存在", + "provider.custom.error.name.required": "顯示名稱為必填", + "provider.custom.error.baseURL.required": "基底 URL 為必填", + "provider.custom.error.baseURL.format": "必須以 http:// 或 https:// 開頭", + "provider.custom.error.required": "必填", + "provider.custom.error.duplicate": "重複", + + "provider.disconnect.toast.disconnected.title": "{{provider}} 已中斷連線", + "provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。", + "model.tag.free": "免費", + "model.tag.latest": "最新", + + "model.provider.anthropic": "Anthropic", + "model.provider.openai": "OpenAI", + "model.provider.google": "Google", + "model.provider.xai": "xAI", + "model.provider.meta": "Meta", + "model.input.text": "文字", + "model.input.image": "圖片", + "model.input.audio": "音訊", + "model.input.video": "影片", + "model.input.pdf": "PDF", + "model.tooltip.allows": "支援: {{inputs}}", + "model.tooltip.context.label": "上下文", + "model.tooltip.inputs": "輸入", + "model.tooltip.model": "模型", + "model.tooltip.provider": "提供者", + "model.tooltip.reasoning": "推理", + "model.tooltip.reasoning.allowed": "支援推理", + "model.tooltip.reasoning.none": "不支援推理", + "model.tooltip.context": "上下文上限 {{limit}}", + "common.search.placeholder": "搜尋", + "common.goBack": "返回", + "common.goForward": "前進", + "common.loading": "載入中", + "common.loading.ellipsis": "...", + "common.cancel": "取消", + "common.clear": "清除", + "common.connect": "連線", + "common.disconnect": "中斷連線", + "common.continue": "繼續", + "common.submit": "提交", + "common.save": "儲存", + "common.saving": "儲存中...", + "common.default": "預設", + "common.attachment": "附件", + + "prompt.placeholder.shell": "輸入 shell 命令... {{example}}", + "prompt.placeholder.normal": "想問什麼都可以…「{{example}}」", + "prompt.placeholder.simple": "想問什麼都可以…", + "prompt.placeholder.summarizeComments": "摘要留言…", + "prompt.placeholder.summarizeComment": "摘要這則留言…", + "prompt.mode.shell": "Shell", + "prompt.mode.normal": "Prompt", + "prompt.mode.shell.exit": "按 esc 退出", + "session.child.promptDisabled": "無法在子代理程式工作階段中輸入提示詞。", + "session.child.backToParent": "返回主要工作階段。", + + "prompt.example.1": "修復程式碼庫中的一個 TODO", + "prompt.example.2": "這個專案的技術堆疊是什麼?", + "prompt.example.3": "修復失敗的測試", + "prompt.example.4": "解釋驗證是如何運作的", + "prompt.example.5": "尋找並修復安全漏洞", + "prompt.example.6": "為使用者服務新增單元測試", + "prompt.example.7": "重構這個函式,讓它更易讀", + "prompt.example.8": "這個錯誤是什麼意思?", + "prompt.example.9": "幫我偵錯這個問題", + "prompt.example.10": "產生 API 文件", + "prompt.example.11": "最佳化資料庫查詢", + "prompt.example.12": "新增輸入驗證", + "prompt.example.13": "建立一個新的元件用於...", + "prompt.example.14": "我該如何部署這個專案?", + "prompt.example.15": "檢閱我的程式碼並提供最佳實務建議", + "prompt.example.16": "為這個函式新增錯誤處理", + "prompt.example.17": "解釋這個正規表示式", + "prompt.example.18": "把它轉換成 TypeScript", + "prompt.example.19": "在整個程式碼庫中加入記錄功能", + "prompt.example.20": "哪些相依性已經過期?", + "prompt.example.21": "幫我寫一個遷移腳本", + "prompt.example.22": "為這個端點實作快取", + "prompt.example.23": "為這份清單加入分頁功能", + "prompt.example.24": "建立一個 CLI 命令用於...", + "prompt.example.25": "這裡的環境變數是怎麼運作的?", + + "prompt.popover.emptyResults": "沒有符合的結果", + "prompt.popover.emptyCommands": "沒有符合的命令", + "prompt.dropzone.label": "將圖片、PDF 或文字檔案拖放到此處", + "prompt.dropzone.file.label": "拖放以 @提及檔案", + "prompt.slash.badge.custom": "自訂", + "prompt.slash.badge.skill": "技能", + "prompt.slash.badge.mcp": "mcp", + "prompt.context.active": "作用中", + "prompt.context.includeActiveFile": "包含作用中檔案", + "prompt.context.removeActiveFile": "從上下文移除目前檔案", + "prompt.context.removeFile": "從上下文移除檔案", + "prompt.action.attachFile": "附加檔案", + "prompt.menu.addImagesAndFiles": "新增檔案及更多內容", + "prompt.menu.imagesAndFiles": "圖片和檔案", + "prompt.menu.commands": "命令", + "prompt.menu.context": "上下文", + "prompt.menu.shellCommand": "shell 命令", + "prompt.attachment.remove": "移除附件", + "prompt.action.send": "傳送", + "prompt.action.stop": "停止", + + "prompt.toast.pasteUnsupported.title": "不支援的附件", + "prompt.toast.attachmentDuplicate.title": "此檔案已上傳", + "prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。", + "prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型", + "prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。", + "prompt.toast.worktreeCreateFailed.title": "建立工作樹失敗", + "prompt.toast.sessionCreateFailed.title": "建立工作階段失敗", + "prompt.toast.shellSendFailed.title": "傳送 shell 命令失敗", + "prompt.toast.commandSendFailed.title": "傳送命令失敗", + "prompt.toast.promptSendFailed.title": "傳送提示失敗", + "prompt.toast.promptSendFailed.description": "無法取得工作階段", + + "dialog.mcp.title": "MCP", + "dialog.mcp.description": "已啟用 {{enabled}} / {{total}}", + "dialog.mcp.empty": "未設定 MCP", + + "dialog.lsp.empty": "會根據檔案類型自動偵測 LSP", + "dialog.plugins.empty": "在 opencode.json 中設定的外掛程式", + + "mcp.status.connected": "已連線", + "mcp.status.failed": "失敗", + "mcp.status.needs_auth": "需要授權", + "mcp.auth.clickToAuthenticate": "按一下以進行授權", + "mcp.status.disabled": "已停用", + + "dialog.fork.empty": "沒有可用於分支的訊息", + + "dialog.directory.search.placeholder": "搜尋資料夾", + "dialog.directory.empty": "找不到資料夾", + "dialog.directory.action.selectFile": "選擇檔案", + "dialog.directory.action.selectFolder": "選擇資料夾", + "dialog.directory.root": "根目錄", + "dialog.directory.parent": "上層目錄", + "dialog.directory.readError": "無法讀取此資料夾", + + "dialog.server.title": "伺服器", + "dialog.server.description": "切換此應用程式連線的 OpenCode 伺服器。", + "dialog.server.search.placeholder": "搜尋伺服器", + "dialog.server.empty": "尚無伺服器", + "dialog.server.add.title": "新增伺服器", + "dialog.server.add.url": "伺服器 URL", + "dialog.server.add.placeholder": "http://localhost:4096", + "dialog.server.add.error": "無法連線到伺服器", + "dialog.server.add.checking": "檢查中...", + "dialog.server.add.button": "新增伺服器", + "dialog.server.add.name": "伺服器名稱(選填)", + "dialog.server.add.namePlaceholder": "Localhost", + "dialog.server.add.username": "使用者名稱(選填)", + "dialog.server.add.password": "密碼(選填)", + "dialog.server.edit.title": "編輯伺服器", + "dialog.server.default.title": "預設伺服器", + "dialog.server.default.description": "應用程式啟動時連線此伺服器,而不是啟動本機伺服器。需要重新啟動。", + "dialog.server.default.none": "未選擇伺服器", + "dialog.server.default.set": "將目前伺服器設為預設", + "dialog.server.default.clear": "清除", + "dialog.server.action.remove": "移除伺服器", + + "dialog.server.menu.edit": "編輯", + "dialog.server.menu.default": "設為預設", + "dialog.server.menu.defaultRemove": "取消預設", + "dialog.server.menu.delete": "刪除", + "dialog.server.current": "目前伺服器", + "dialog.server.status.default": "預設", + "wsl.server.add": "新增 WSL 伺服器", + "wsl.server.addShort": "新增 WSL", + "wsl.server.label": "WSL", + "wsl.server.menu.label": "WSL 伺服器", + "wsl.server.retryStart": "重試啟動", + "wsl.server.updating": "更新中...", + "wsl.onboarding.step.distro": "選擇發行版", + "wsl.onboarding.step.opencode": "OpenCode", + "wsl.onboarding.checkingRuntime": "正在檢查 WSL...", + "wsl.onboarding.restartRequired": "Windows 需要重新啟動,才能完成 WSL 安裝。", + "wsl.onboarding.ready": "WSL 已就緒。", + "wsl.onboarding.required": "需要 WSL 才能繼續。", + "wsl.onboarding.checkingDistros": "正在檢查發行版...", + "wsl.onboarding.installingDistro": "正在安裝 {{distro}}...", + "wsl.onboarding.checkingDistro": "正在檢查 {{distro}}...", + "wsl.onboarding.listingDistros": "正在列出發行版...", + "wsl.onboarding.distroReady": "{{distro}} 已就緒。", + "wsl.onboarding.distroNotInstalled": "尚未安裝 {{distro}}。", + "wsl.onboarding.openDistroOnce": "開啟 {{distro}} 一次以完成設定。", + "wsl.onboarding.finishingDistro": "正在完成 {{distro}} 的設定。", + "wsl.onboarding.pickDistro": "選擇發行版,或在下方安裝。", + "wsl.onboarding.checkingOpencode": "正在檢查 OpenCode...", + "wsl.onboarding.checkingOpencodeIn": "正在檢查 {{distro}} 中的 OpenCode...", + "wsl.onboarding.updatingOpencode": "正在更新 OpenCode...", + "wsl.onboarding.updatingOpencodeIn": "正在更新 {{distro}} 中的 OpenCode...", + "wsl.onboarding.updateOpencodeIn": "更新 {{distro}} 中的 OpenCode。", + "wsl.onboarding.updateOpencode": "更新 OpenCode", + "wsl.onboarding.opencodeReadyIn": "{{distro}} 中的 OpenCode 已就緒。", + "wsl.onboarding.opencodeReady": "OpenCode 已就緒。", + "wsl.onboarding.installOpencodeIn": "在 {{distro}} 中安裝 OpenCode。", + "wsl.onboarding.installOpencode": "安裝 OpenCode", + "wsl.onboarding.chooseDistroFirst": "請先選擇發行版。", + "wsl.onboarding.loadFailed": "無法載入 WSL 狀態。", + "wsl.onboarding.loading": "載入中...", + "wsl.onboarding.installedDistros": "已安裝的發行版", + "wsl.onboarding.checkAgain": "再次檢查", + "wsl.onboarding.distroStatus.ready": "就緒", + "wsl.onboarding.distroStatus.checking": "檢查中...", + "wsl.onboarding.distroStatus.opencodeMissing": "未安裝 OpenCode", + "wsl.onboarding.distroStatus.missingTools": "缺少 bash、curl", + "wsl.onboarding.distroStatus.unsupported": "不支援 · 請使用 WSL 2", + "wsl.onboarding.needAnotherDistro": "需要其他發行版?", + "wsl.onboarding.needAnotherDistroHint": "從 WSL 目錄安裝 Linux 發行版", + "wsl.onboarding.wslNotInstalled.title": "未安裝 WSL", + "wsl.onboarding.wslNotInstalled.description": "新增 WSL 伺服器前,必須先安裝 WSL(Windows Subsystem for Linux)", + "wsl.onboarding.wslUnavailable.title": "WSL 無法使用", + "wsl.onboarding.wslUnavailable.description": "OpenCode 無法確認此電腦上的 WSL。", + "wsl.onboarding.installWsl": "安裝 WSL", + "wsl.onboarding.windowsRestartRequired": "重新啟動 Windows 以完成 WSL 安裝,然後重新開啟 OpenCode。", + "wsl.onboarding.next": "下一步", + "wsl.onboarding.refresh": "重新整理", + "wsl.onboarding.allDistrosAdded": "所有已安裝的發行版都已新增。", + "wsl.onboarding.noDistros": "尚未偵測到發行版。", + "wsl.onboarding.install": "安裝", + "wsl.onboarding.installing": "安裝中...", + "wsl.onboarding.installDistro": "安裝發行版", + "wsl.onboarding.searchDistros": "搜尋發行版", + "wsl.onboarding.wsl2Required": "需要 WSL 2。", + "wsl.onboarding.toolsRequired": "此發行版需要 bash 和 curl。", + "wsl.onboarding.openTerminal": "開啟終端機", + "wsl.onboarding.path": "路徑: {{path}}", + "wsl.onboarding.notFound": "找不到", + "wsl.onboarding.version": "版本: {{version}}", + "wsl.onboarding.unknown": "未知", + "wsl.onboarding.desktopVersion": "桌面版 {{version}}", + "wsl.onboarding.versionMismatch": "已安裝版本與桌面應用程式版本不符。", + "wsl.onboarding.adding": "新增中...", + + "help.tabs.toast.ariaLabel": "分頁功能介紹。使用分頁整理工作和進行中的工作階段", + "help.tabs.toast.dismiss": "關閉分頁功能資訊", + "help.tabs.title": "分頁功能介紹", + "help.tabs.description": "使用分頁整理工作和進行中的工作階段", + "help.tabs.date": "7 月 14 日", + "help.tabs.introduction": "OpenCode Desktop 現在以分頁為核心。", + "help.tabs.sessions": + "在分頁中開始新的工作階段,或從任一專案開啟現有的工作階段。開始新工作時請開啟新分頁,完成後再將其關閉。", + "help.tabs.organize": + "同時開啟幾個分頁,可讓進行中的工作階段更容易整理。如果打算保留分頁,請將其重新命名為容易記住的名稱。", + "help.tabs.home": "你可以在新的首頁找到所有工作階段和專案。選取工作階段即可在分頁中開啟。", + "help.tabs.persistence": "重新開啟應用程式時,分頁仍會保持開啟。", + "help.tabs.worktrees": + "新版設計尚未支援 Git Worktrees,此功能即將推出。如果你偏好繼續使用先前的版面,可以在設定中切換版面。請留意,新版面將在幾週後成為永久版面。", + + "dialog.project.edit.title": "編輯專案", + "dialog.project.edit.name": "名稱", + "dialog.project.edit.icon": "圖示", + "dialog.project.edit.icon.alt": "專案圖示", + "dialog.project.edit.icon.hint": "按一下或拖曳圖片", + "dialog.project.edit.icon.recommended": "建議:128x128px", + "dialog.project.edit.color": "顏色", + "dialog.project.edit.color.select": "選擇{{color}}顏色", + + "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": "系統", + "context.breakdown.user": "使用者", + "context.breakdown.assistant": "助理", + "context.breakdown.tool": "工具呼叫", + "context.breakdown.other": "其他", + + "context.systemPrompt.title": "系統提示詞", + "context.rawMessages.title": "原始訊息", + + "context.export.session": "匯出工作階段", + + "context.stats.session": "工作階段", + "context.stats.messages": "訊息數", + "context.stats.provider": "提供者", + "context.stats.model": "模型", + "context.stats.limit": "上下文限制", + "context.stats.totalTokens": "總 token", + "context.stats.usage": "使用量", + "context.stats.inputTokens": "輸入 token", + "context.stats.outputTokens": "輸出 token", + "context.stats.reasoningTokens": "推理 token", + "context.stats.cacheTokens": "快取 token(讀/寫)", + "context.stats.userMessages": "使用者訊息", + "context.stats.assistantMessages": "助理訊息", + "context.stats.totalCost": "總成本", + "context.stats.sessionCreated": "建立時間", + "context.stats.lastActivity": "最後活動", + + "context.usage.tokens": "Token", + "context.usage.usage": "使用量", + "context.usage.cost": "成本", + "context.usage.clickToView": "按一下以檢視上下文", + "context.usage.view": "檢視上下文用量", + + "language.en": "English", + "language.zh": "简体中文", + "language.zht": "繁體中文", + "language.ko": "한국어", + "language.de": "Deutsch", + "language.es": "Español", + "language.fr": "Français", + "language.da": "Dansk", + "language.ja": "日本語", + "language.pl": "Polski", + "language.ru": "Русский", + "language.ar": "العربية", + "language.no": "Norsk", + "language.br": "Português (Brasil)", + "language.bs": "Bosanski", + "language.uk": "Українська", + "language.th": "ไทย", + "language.tr": "Türkçe", + + "toast.language.title": "語言", + "toast.language.description": "已切換到 {{language}}", + + "toast.theme.title": "主題已切換", + "toast.scheme.title": "配色方案", + + "toast.workspace.enabled.title": "工作區已啟用", + "toast.workspace.enabled.description": "側邊欄現在顯示多個工作樹", + "toast.workspace.disabled.title": "工作區已停用", + "toast.workspace.disabled.description": "側邊欄只顯示主工作樹", + + "toast.permissions.autoaccept.on.title": "正在自動接受權限", + "toast.permissions.autoaccept.on.description": "權限要求將自動獲准", + "toast.permissions.autoaccept.off.title": "已停止自動接受權限", + "toast.permissions.autoaccept.off.description": "權限要求將需要核准", + + "toast.model.none.title": "未選擇模型", + "toast.model.none.description": "請先連線提供者以摘要此工作階段", + + "toast.file.loadFailed.title": "載入檔案失敗", + + "toast.file.listFailed.title": "列出檔案失敗", + "toast.context.noLineSelection.title": "未選取行", + "toast.context.noLineSelection.description": "請先在檔案分頁中選取行範圍。", + "toast.session.share.copyFailed.title": "無法複製連結到剪貼簿", + "toast.session.share.success.title": "工作階段已分享", + "toast.session.share.success.description": "分享連結已複製到剪貼簿", + "toast.session.share.failed.title": "分享工作階段失敗", + "toast.session.share.failed.description": "分享工作階段時發生錯誤", + + "toast.session.unshare.success.title": "已取消分享工作階段", + "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}}) 可安裝。", + "toast.update.action.installRestart": "安裝並重新啟動", + "toast.update.action.notYet": "稍後", + + "error.page.title": "出了點問題", + "error.page.description": "載入應用程式時發生錯誤。", + "error.page.description.localServerStartup": "啟動本機伺服器時發生錯誤。", + "error.page.details.label": "錯誤詳情", + "error.page.action.restart": "重新啟動", + "error.page.action.report": "回報錯誤", + "error.page.action.reported": "已回報錯誤", + "error.page.action.exportLogs": "匯出記錄檔", + "error.page.action.checking": "檢查中...", + "error.page.action.checkUpdates": "檢查更新", + "error.page.action.updateTo": "更新到 {{version}}", + "error.page.report.prefix": "請將此錯誤回報給 OpenCode 團隊", + "error.page.report.discord": "在 Discord 上", + "error.page.version": "版本: {{version}}", + + "error.dev.rootNotFound": "找不到根元素。你是不是忘了把它新增到 index.html?或者 id 屬性拼錯了?", + + "error.serverSync.connectFailed": "無法連線到伺服器。是否有伺服器正在 `{{url}}` 執行?", + "directory.error.invalidUrl": "URL 中的目錄無效。", + + "error.chain.unknown": "未知錯誤", + "error.chain.causedBy": "原因:", + "error.chain.apiError": "API 錯誤", + "error.chain.status": "狀態: {{status}}", + "error.chain.retryable": "可重試: {{retryable}}", + "error.chain.responseBody": "回應內容:\n{{body}}", + "error.chain.didYouMean": "你是不是想輸入: {{suggestions}}", + "error.chain.modelNotFound": "找不到模型: {{provider}}/{{model}}", + "error.chain.checkConfig": "請檢查你的設定 (opencode.json) 中的 provider/model 名稱", + "error.chain.mcpFailed": "MCP 伺服器「{{name}}」啟動失敗。注意:OpenCode 尚不支援 MCP 驗證。", + "error.chain.providerAuthFailed": "提供者驗證失敗({{provider}}):{{message}}", + "error.chain.providerInitFailed": '無法初始化提供者 "{{provider}}"。請檢查憑證和設定。', + "error.chain.configJsonInvalid": "設定檔 {{path}} 不是有效的 JSON(C)", + "error.chain.configJsonInvalidWithMessage": "設定檔 {{path}} 不是有效的 JSON(C): {{message}}", + "error.chain.configDirectoryTypo": + '{{path}} 中的目錄 "{{dir}}" 無效。請將目錄重新命名為 "{{suggestion}}" 或移除它。這是一個常見拼寫錯誤。', + "error.chain.configFrontmatterError": "無法解析 {{path}} 中的 frontmatter:\n{{message}}", + "error.chain.configInvalid": "設定檔 {{path}} 無效", + "error.chain.configInvalidWithMessage": "設定檔 {{path}} 無效: {{message}}", + + "notification.permission.title": "需要權限", + "notification.permission.description": "{{sessionTitle}}({{projectName}})需要權限", + "notification.question.title": "問題", + "notification.question.description": "{{sessionTitle}}({{projectName}})有一個問題", + "notification.action.goToSession": "前往工作階段", + + "notification.session.responseReady.title": "回覆已就緒", + "notification.session.error.title": "工作階段錯誤", + "notification.session.error.fallbackDescription": "發生錯誤", + + "home.recentProjects": "最近專案", + "home.empty.title": "沒有最近專案", + "home.empty.description": "開啟本機專案以開始使用", + "home.title": "首頁", + "home.projects": "專案", + "home.project.add": "新增專案", + "home.recentlyClosed": "最近關閉", + "home.server.collapse": "收合伺服器專案", + "home.server.expand": "展開伺服器專案", + "home.sessions.search.placeholder": "搜尋工作階段", + "home.sessions.search.placeholder.scoped": "在 {{scope}} 中搜尋工作階段", + "home.sessions.search.sessions": "工作階段", + "home.sessions.search.noResults": "找不到符合 {{query}} 的工作階段", + "home.sessions.empty": "這裡還沒有內容", + "home.sessions.empty.description": "建立工作階段以開始使用", + "home.sessions.group.today": "今天", + "home.sessions.group.yesterday": "昨天", + "home.sessions.group.older": "更早", + "home.providerTip": "連線 75 個以上的提供者,使用 Claude、GPT、Gemini 等其他模型", + + "session.tab.session": "工作階段", + "session.tab.review": "檢閱", + "session.tab.context": "上下文", + "session.tab.unknown": "未知的工作階段", + "session.panel.reviewAndFiles": "檢閱與檔案", + "session.error.notFound": "找不到此工作階段", + "session.error.notFound.description": "此分頁指向的工作階段已不存在於此伺服器。", + "session.error.notFound.closeTab": "關閉分頁", + "session.error.serverConnection": "無法連線至此伺服器", + "session.review.filesChanged": "{{count}} 個檔案變更", + "session.review.change.one": "變更", + "session.review.change.other": "變更", + "session.review.loadingChanges": "正在載入變更...", + "session.review.empty": "此工作階段尚無變更", + "session.review.noChanges": "沒有變更", + "session.review.noUncommittedChanges": "尚無未提交的變更", + "session.review.noBranchChanges": "尚無分支變更", + "session.review.noVcs": "未偵測到 Git 版本控制系統,無法顯示變更", + "session.review.noSnapshot": "設定中已停用快照追蹤,因此無法使用工作階段變更", + "session.files.selectToOpen": "選取要開啟的檔案", + "session.files.all": "所有檔案", + "session.files.empty": "沒有檔案", + "session.files.binaryContent": "二進位檔案(無法顯示內容)", + "session.messages.renderEarlier": "顯示更早的訊息", + "session.messages.loadingEarlier": "正在載入更早的訊息...", + "session.messages.loadEarlier": "載入更早的訊息", + "session.messages.loading": "正在載入訊息...", + + "session.messages.jumpToLatest": "跳到最新", + "session.context.addToContext": "將 {{selection}} 新增到上下文", + "session.todo.title": "待辦事項", + "session.todo.collapse": "收合", + "session.todo.expand": "展開", + "session.question.minimize": "最小化問題", + "session.question.restore": "還原問題", + "session.question.pending.one": "{{count}} 個待處理問題", + "session.question.pending.other": "{{count}} 個待處理問題", + "session.followupDock.summary.one": "{{count}} 則佇列訊息", + "session.followupDock.summary.other": "{{count}} 則佇列訊息", + "session.followupDock.sendNow": "立即傳送", + "session.followupDock.edit": "編輯", + "session.followupDock.collapse": "收合佇列訊息", + "session.followupDock.expand": "展開佇列訊息", + "session.revertDock.summary.one": "{{count}} 則已還原的訊息", + "session.revertDock.summary.other": "{{count}} 則已還原的訊息", + "session.revertDock.collapse": "收合已還原的訊息", + "session.revertDock.expand": "展開已還原的訊息", + "session.revertDock.restore": "還原訊息", + + "session.new.title": "實現任何想法", + "session.new.project.new": "新專案", + "session.new.project.search": "搜尋專案", + "session.new.project.add": "新增專案", + "session.new.worktree.main": "主分支", + "session.new.worktree.mainWithBranch": "主分支 ({{branch}})", + "session.new.worktree.create": "建立新的 worktree", + "session.new.workspace.runIn": "工作階段執行位置", + "session.new.workspace.triggerLocal": "本機", + "session.new.workspace.local": "本機儲存庫", + "session.new.workspace.existing": "工作區…", + "session.new.git.none": "無 Git", + "session.new.lastModified": "最後修改", + + "session.header.search.placeholder": "搜尋 {{project}}", + "session.header.searchFiles": "搜尋檔案", + "session.header.openIn": "開啟於", + "session.header.open.action": "開啟 {{app}}", + "session.header.open.ariaLabel": "在 {{app}} 中開啟", + "session.header.open.menu": "開啟選項", + "session.header.open.copyPath": "複製路徑", + + "status.popover.trigger": "狀態", + "status.popover.ariaLabel": "伺服器設定", + "status.popover.tab.servers": "伺服器", + "status.popover.tab.mcp": "MCP", + "status.popover.tab.lsp": "LSP", + "status.popover.tab.plugins": "外掛程式", + "status.popover.action.manageServers": "管理伺服器", + + "session.share.popover.title": "發佈到網頁", + "session.share.popover.description.shared": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。", + "session.share.popover.description.unshared": "在網頁上公開分享此工作階段。任何擁有連結的人都可以存取。", + "session.share.action.share": "分享", + "session.share.action.publish": "發佈", + "session.share.action.publishing": "正在發佈...", + "session.share.action.unpublish": "取消發佈", + "session.share.action.unpublishing": "正在取消發佈...", + "session.share.action.view": "檢視", + "session.share.copy.copied": "已複製", + "session.share.copy.copyLink": "複製連結", + + "lsp.tooltip.none": "沒有 LSP 伺服器", + "lsp.label.connected": "{{count}} LSP", + + "prompt.loading": "正在載入提示...", + "terminal.loading": "正在載入終端機...", + "terminal.title": "終端機", + "terminal.title.numbered": "終端機 {{number}}", + "terminal.close": "關閉終端機", + + "terminal.connectionLost.title": "連線中斷", + "terminal.connectionLost.description": "終端機連線已中斷。這可能會在伺服器重新啟動時發生。", + "terminal.connectTicket.csrfError": "PTY 連線票證因來源或 CSRF 檢查而遭拒。請檢查伺服器 CORS 設定。", + "terminal.connectTicket.statusError": "PTY 連線票證失敗,狀態為 {{status}}", + + "titlebar.update": "更新", + "titlebar.updateVersion": "更新 {{version}}", + "common.closeTab": "關閉分頁", + "common.dismiss": "忽略", + "common.requestFailed": "要求失敗", + "common.moreOptions": "更多選項", + "common.learnMore": "深入了解", + "common.rename": "重新命名", + "common.export": "匯出", + "common.reset": "重設", + "common.archive": "封存", + "common.delete": "刪除", + "common.close": "關閉", + "common.edit": "編輯", + "common.loadMore": "載入更多", + + "common.key.esc": "ESC", + "sidebar.menu.toggle": "切換選單", + "sidebar.nav.projectsAndSessions": "專案與工作階段", + "sidebar.settings": "設定", + "sidebar.help": "說明", + "sidebar.workspaces.enable": "啟用工作區", + "sidebar.workspaces.disable": "停用工作區", + "sidebar.gettingStarted.title": "開始使用", + "sidebar.gettingStarted.line1": "OpenCode 提供免費模型,你可以立即開始使用。", + "sidebar.gettingStarted.line2": "連線任意提供者即可使用更多模型,如 Claude、GPT、Gemini 等。", + "sidebar.project.recentSessions": "最近工作階段", + "sidebar.project.viewAllSessions": "查看全部工作階段", + "sidebar.project.clearNotifications": "清除通知", + "sidebar.empty.title": "未開啟任何專案", + "sidebar.empty.description": "開啟專案以開始使用", + + "app.name.desktop": "OpenCode Desktop", + "settings.section.desktop": "桌面", + "settings.section.server": "伺服器", + "settings.tab.general": "一般", + "settings.tab.shortcuts": "快速鍵", + "settings.desktop.section.wsl": "WSL", + "settings.desktop.wsl.title": "WSL 整合", + "settings.desktop.wsl.description": "在 Windows 上的 WSL 中執行 OpenCode 伺服器。", + + "settings.general.section.appearance": "外觀", + "settings.general.section.advanced": "進階", + "settings.general.section.notifications": "系統通知", + "settings.general.section.updates": "更新", + "settings.general.section.sounds": "音效", + "settings.general.section.feed": "資訊流", + "settings.general.section.display": "顯示", + + "settings.general.row.language.title": "語言", + "settings.general.row.language.description": "變更 OpenCode 的顯示語言", + "settings.general.row.shell.title": "終端機 Shell", + "settings.general.row.shell.description": "選擇終端機使用的 Shell。相容的 Shell 也會用於代理程式工具呼叫。", + "settings.general.row.shell.autoDefault": "自動(預設)", + "settings.general.row.shell.terminalOnly": "僅限終端機", + "settings.general.row.appearance.title": "外觀", + "settings.general.row.appearance.description": "自訂 OpenCode 在你的裝置上的外觀", + "settings.general.row.colorScheme.title": "配色方案", + "settings.general.row.colorScheme.description": "選擇 OpenCode 要跟隨系統、淺色或深色主題", + "settings.general.row.theme.title": "主題", + "settings.general.row.theme.description": "自訂 OpenCode 的主題。", + "settings.general.row.font.title": "程式碼字型", + "settings.general.row.font.description": "自訂程式碼區塊使用的字型", + "settings.general.row.terminalFont.title": "終端機字型", + "settings.general.row.terminalFont.description": "自訂終端機使用的字型", + "settings.general.row.uiFont.title": "介面字型", + "settings.general.row.uiFont.description": "自訂整個介面使用的字型", + "settings.general.row.followup.title": "後續追問行為", + "settings.general.row.followup.description": "選擇後續追問提示是立即引導還是進入佇列等待", + "settings.general.row.followup.option.queue": "佇列", + "settings.general.row.followup.option.steer": "引導", + "settings.general.row.showFileTree.title": "檔案樹", + "settings.general.row.showFileTree.description": "在工作階段中顯示檔案樹面板", + "settings.general.row.showNavigation.title": "導覽控制項", + "settings.general.row.showNavigation.description": "在桌面標題列中顯示上一頁和下一頁按鈕", + "settings.general.row.showSearch.title": "命令面板", + "settings.general.row.showSearch.description": "在標題列中顯示搜尋和命令面板按鈕", + "settings.general.row.showTerminal.title": "終端機", + "settings.general.row.showTerminal.description": "在桌面標題列中顯示終端機按鈕", + "settings.general.row.showStatus.title": "伺服器狀態", + "settings.general.row.showStatus.description": "在標題列中顯示伺服器狀態按鈕", + "settings.general.row.mobileTitlebarBottom.title": "底部導覽", + "settings.general.row.mobileTitlebarBottom.description": "在行動裝置上將標題列和工作階段分頁置於畫面底部", + "settings.general.row.showCustomAgents.title": "自訂代理程式", + "settings.general.row.showCustomAgents.description": "在輸入區顯示代理程式選擇器", + "settings.general.row.reasoningSummaries.title": "顯示推理摘要", + "settings.general.row.reasoningSummaries.description": "在時間軸中顯示模型推理摘要", + + "settings.general.row.shellToolPartsExpanded.title": "展開 shell 工具區塊", + "settings.general.row.shellToolPartsExpanded.description": "在時間軸中預設展開 shell 工具區塊", + "settings.general.row.editToolPartsExpanded.title": "展開 edit 工具區塊", + "settings.general.row.editToolPartsExpanded.description": "在時間軸中預設展開 edit、write 和 patch 工具區塊", + "settings.general.row.newInterface.title": "新版面", + "settings.general.row.newInterface.badge": "新", + "settings.general.row.newInterface.description": "使用新的分頁和首頁版面。在限定時間內可切換版面。", + "settings.general.row.newInterfaceNotice.title": "你現在使用的是新版面", + "settings.general.row.newInterfaceNotice.description": "先前的版面已無法使用", + "settings.general.row.newInterfaceNotice.dismiss": "忽略", + "settings.general.row.pinchZoom.title": "雙指縮放", + "settings.general.row.pinchZoom.description": "允許使用觸控板雙指開合和 Ctrl-捲動手勢縮放", + "settings.general.row.wayland.title": "使用原生 Wayland", + "settings.general.row.wayland.description": "在 Wayland 上停用 X11 後備模式。需要重新啟動。", + "settings.general.row.wayland.tooltip": "在混合更新率螢幕的 Linux 系統上,原生 Wayland 可能更穩定。", + + "settings.general.row.releaseNotes.title": "發行說明", + "settings.general.row.releaseNotes.description": "更新後顯示「新功能」彈出視窗", + + "settings.updates.row.startup.title": "啟動時檢查更新", + "settings.updates.row.startup.description": "在 OpenCode 啟動時自動檢查更新", + "settings.updates.row.check.title": "檢查更新", + "settings.updates.row.check.description": "手動檢查更新並在有更新時安裝", + "settings.updates.action.checkNow": "立即檢查", + "settings.updates.action.checking": "檢查中...", + "settings.updates.action.downloading": "下載中...", + "settings.updates.action.installing": "安裝中...", + "settings.updates.toast.latest.title": "已是最新版本", + "settings.updates.toast.latest.description": "你正在使用最新版本的 OpenCode。", + + "sound.option.none": "無", + "sound.option.alert01": "警報 01", + "sound.option.alert02": "警報 02", + "sound.option.alert03": "警報 03", + "sound.option.alert04": "警報 04", + "sound.option.alert05": "警報 05", + "sound.option.alert06": "警報 06", + "sound.option.alert07": "警報 07", + "sound.option.alert08": "警報 08", + "sound.option.alert09": "警報 09", + "sound.option.alert10": "警報 10", + "sound.option.bipbop01": "Bip-bop 01", + "sound.option.bipbop02": "Bip-bop 02", + "sound.option.bipbop03": "Bip-bop 03", + "sound.option.bipbop04": "Bip-bop 04", + "sound.option.bipbop05": "Bip-bop 05", + "sound.option.bipbop06": "Bip-bop 06", + "sound.option.bipbop07": "Bip-bop 07", + "sound.option.bipbop08": "Bip-bop 08", + "sound.option.bipbop09": "Bip-bop 09", + "sound.option.bipbop10": "Bip-bop 10", + "sound.option.staplebops01": "Staplebops 01", + "sound.option.staplebops02": "Staplebops 02", + "sound.option.staplebops03": "Staplebops 03", + "sound.option.staplebops04": "Staplebops 04", + "sound.option.staplebops05": "Staplebops 05", + "sound.option.staplebops06": "Staplebops 06", + "sound.option.staplebops07": "Staplebops 07", + "sound.option.nope01": "Nope 01", + "sound.option.nope02": "Nope 02", + "sound.option.nope03": "Nope 03", + "sound.option.nope04": "Nope 04", + "sound.option.nope05": "Nope 05", + "sound.option.nope06": "Nope 06", + "sound.option.nope07": "Nope 07", + "sound.option.nope08": "Nope 08", + "sound.option.nope09": "Nope 09", + "sound.option.nope10": "Nope 10", + "sound.option.nope11": "Nope 11", + "sound.option.nope12": "Nope 12", + "sound.option.yup01": "Yup 01", + "sound.option.yup02": "Yup 02", + "sound.option.yup03": "Yup 03", + "sound.option.yup04": "Yup 04", + "sound.option.yup05": "Yup 05", + "sound.option.yup06": "Yup 06", + "settings.general.notifications.agent.title": "代理程式", + "settings.general.notifications.agent.description": "當代理程式完成或需要注意時顯示系統通知", + "settings.general.notifications.permissions.title": "權限", + "settings.general.notifications.permissions.description": "當需要權限時顯示系統通知", + "settings.general.notifications.errors.title": "錯誤", + "settings.general.notifications.errors.description": "發生錯誤時顯示系統通知", + + "settings.general.sounds.agent.title": "代理程式", + "settings.general.sounds.agent.description": "當代理程式完成或需要注意時播放聲音", + "settings.general.sounds.permissions.title": "權限", + "settings.general.sounds.permissions.description": "當需要權限時播放聲音", + "settings.general.sounds.errors.title": "錯誤", + "settings.general.sounds.errors.description": "發生錯誤時播放聲音", + + "settings.shortcuts.title": "鍵盤快速鍵", + "settings.shortcuts.reset.button": "重設為預設值", + "settings.shortcuts.reset.toast.title": "快速鍵已重設", + "settings.shortcuts.reset.toast.description": "鍵盤快速鍵已重設為預設設定。", + "settings.shortcuts.conflict.title": "快速鍵已被占用", + "settings.shortcuts.conflict.description": "{{keybind}} 已分配給 {{titles}}。", + "settings.shortcuts.unassigned": "未設定", + "settings.shortcuts.pressKeys": "按下按鍵", + "settings.shortcuts.search.placeholder": "搜尋快速鍵", + "settings.shortcuts.search.empty": "找不到快速鍵", + + "settings.shortcuts.group.general": "一般", + "settings.shortcuts.group.session": "工作階段", + "settings.shortcuts.group.navigation": "導覽", + "settings.shortcuts.group.modelAndAgent": "模型與代理程式", + "settings.shortcuts.group.terminal": "終端機", + "settings.shortcuts.group.prompt": "提示", + + "settings.providers.title": "提供者", + "settings.providers.description": "提供者設定將在此處可設定。", + "settings.providers.section.connected": "已連線的提供者", + "settings.providers.connected.empty": "沒有已連線的提供者", + "settings.providers.section.popular": "熱門提供者", + "settings.providers.tag.environment": "環境", + "settings.providers.tag.config": "設定", + "settings.providers.tag.custom": "自訂", + "settings.providers.tag.other": "其他", + "settings.models.title": "模型", + "settings.models.description": "模型設定將在此處可設定。", + "settings.agents.title": "代理程式", + "settings.agents.description": "代理程式設定將在此處可設定。", + "settings.commands.title": "命令", + "settings.commands.description": "命令設定將在此處可設定。", + "settings.mcp.title": "MCP", + "settings.mcp.description": "MCP 設定將在此處可設定。", + + "settings.permissions.title": "權限", + "settings.permissions.description": "控制伺服器預設可以使用哪些工具。", + "settings.permissions.section.tools": "工具", + "settings.permissions.toast.updateFailed.title": "更新權限失敗", + + "settings.permissions.action.allow": "允許", + "settings.permissions.action.ask": "詢問", + "settings.permissions.action.deny": "拒絕", + + "settings.permissions.tool.read.title": "讀取", + "settings.permissions.tool.read.description": "讀取檔案(符合檔案路徑)", + "settings.permissions.tool.edit.title": "編輯", + "settings.permissions.tool.edit.description": "修改檔案,包括編輯、寫入、修補和多重編輯", + "settings.permissions.tool.glob.title": "Glob", + "settings.permissions.tool.glob.description": "使用 glob 模式符合檔案", + "settings.permissions.tool.grep.title": "Grep", + "settings.permissions.tool.grep.description": "使用正規表示式搜尋檔案內容", + "settings.permissions.tool.list.title": "清單", + "settings.permissions.tool.list.description": "列出目錄中的檔案", + "settings.permissions.tool.bash.title": "Bash", + "settings.permissions.tool.bash.description": "執行 shell 命令", + "settings.permissions.tool.task.title": "Task", + "settings.permissions.tool.task.description": "啟動子代理程式", + "settings.permissions.tool.skill.title": "Skill", + "settings.permissions.tool.skill.description": "按名稱載入技能", + "settings.permissions.tool.lsp.title": "LSP", + "settings.permissions.tool.lsp.description": "執行語言伺服器查詢", + "settings.permissions.tool.todowrite.title": "更新待辦", + "settings.permissions.tool.todowrite.description": "更新待辦清單", + "settings.permissions.tool.webfetch.title": "Web Fetch", + "settings.permissions.tool.webfetch.description": "從 URL 取得內容", + "settings.permissions.tool.websearch.title": "Web Search", + "settings.permissions.tool.websearch.description": "搜尋網頁", + "settings.permissions.tool.external_directory.title": "外部目錄", + "settings.permissions.tool.external_directory.description": "存取專案目錄之外的檔案", + "settings.permissions.tool.doom_loop.title": "Doom Loop", + "settings.permissions.tool.doom_loop.description": "偵測具有相同輸入的重複工具呼叫", + + "session.delete.failed.title": "刪除工作階段失敗", + "session.delete.title": "刪除工作階段", + "session.delete.confirm": "要刪除工作階段「{{name}}」嗎?", + "session.delete.button": "刪除工作階段", + + "workspace.new": "新增工作區", + "workspace.type.local": "本機", + "workspace.type.sandbox": "沙盒", + "workspace.create.failed.title": "建立工作區失敗", + "workspace.delete.failed.title": "刪除工作區失敗", + "workspace.resetting.title": "正在重設工作區", + "workspace.resetting.description": "這可能需要一點時間。", + "workspace.reset.failed.title": "重設工作區失敗", + "workspace.reset.success.title": "工作區已重設", + "workspace.reset.success.description": "工作區現在與預設分支一致。", + "workspace.error.stillPreparing": "工作區仍在準備中", + "workspace.status.checking": "正在檢查未合併的變更...", + "workspace.status.error": "無法驗證 Git 狀態。", + "workspace.status.clean": "未偵測到未合併的變更。", + "workspace.status.dirty": "偵測到未合併的變更。", + "workspace.delete.title": "刪除工作區", + "workspace.delete.confirm": "要刪除工作區「{{name}}」嗎?", + "workspace.delete.button": "刪除工作區", + "workspace.reset.title": "重設工作區", + "workspace.reset.confirm": "要重設工作區「{{name}}」嗎?", + "workspace.reset.button": "重設工作區", + "workspace.reset.archived.none": "不會封存任何作用中工作階段。", + "workspace.reset.archived.one": "將封存 1 個工作階段。", + "workspace.reset.archived.many": "將封存 {{count}} 個工作階段。", + "workspace.reset.note": "這將把工作區重設為與預設分支一致。", + "common.open": "開啟", + "dialog.releaseNotes.action.getStarted": "開始", + "dialog.releaseNotes.action.next": "下一步", + "dialog.releaseNotes.action.hideFuture": "不再顯示", + "dialog.releaseNotes.media.alt": "版本預覽", + "toast.project.reloadFailed.title": "無法重新載入 {{project}}", + "error.server.invalidConfiguration": "無效的設定", + "common.moreCountSuffix": " (還有 {{count}} 個)", + "common.time.justNow": "剛剛", + "common.time.minutesAgo.short": "{{count}}分鐘前", + "common.time.hoursAgo.short": "{{count}}小時前", + "common.time.daysAgo.short": "{{count}}天前", + "settings.providers.connected.environmentDescription": "已從環境變數連線", + "settings.providers.custom.description": "透過基底 URL 新增與 OpenAI 相容的提供者。", + + "app.server.unreachable": "無法連線至 {{server}}", + "app.server.retrying": "正在自動重試...", + "app.server.otherServers": "其他伺服器", + "dialog.server.add.usernamePlaceholder": "使用者名稱", + "dialog.server.add.passwordPlaceholder": "密碼", + "server.row.noUsername": "無使用者名稱", + "session.review.noVcs.createGit.title": "建立 Git 儲存庫", + "session.review.noVcs.createGit.description": "追蹤、檢閱及復原此專案中的變更", + "session.review.noVcs.createGit.actionLoading": "正在建立 Git 儲存庫...", + "session.review.noVcs.createGit.action": "建立 Git 儲存庫", + "session.todo.progress": "已完成 {{done}} 個待辦事項(共 {{total}} 個)", + "session.question.progress": "{{current}}/{{total}} 個問題", + "session.header.open.finder": "Finder", + "session.header.open.fileExplorer": "檔案總管", + "session.header.reveal.finder": "在 Finder 中顯示", + "session.header.reveal.fileExplorer": "在檔案總管中顯示", + "session.header.reveal.containingFolder": "開啟所在的檔案夾", + "session.header.open.fileManager": "檔案管理員", + "session.header.open.app.vscode": "VS Code", + "session.header.open.app.cursor": "Cursor", + "session.header.open.app.zed": "Zed", + "session.header.open.app.textmate": "TextMate", + "session.header.open.app.antigravity": "Antigravity", + "session.header.open.app.terminal": "終端機", + "session.header.open.app.iterm2": "iTerm2", + "session.header.open.app.ghostty": "Ghostty", + "session.header.open.app.warp": "Warp", + "session.header.open.app.xcode": "Xcode", + "session.header.open.app.androidStudio": "Android Studio", + "session.header.open.app.powershell": "PowerShell", + "session.header.open.app.sublimeText": "Sublime Text", + "debugBar.ariaLabel": "開發效能診斷", + "debugBar.na": "不適用", + "debugBar.nav.label": "NAV", + "debugBar.nav.tip": "最後一次完成的涉及工作階段頁面的路由轉換,從路由器啟動到穩定後的第一次繪製。", + "debugBar.fps.label": "FPS", + "debugBar.fps.tip": "過去 5 秒內的移動影格率。", + "debugBar.frame.label": "FRAME", + "debugBar.frame.tip": "過去 5 秒內最差的影格時間。", + "debugBar.jank.label": "JANK", + "debugBar.jank.tip": "過去 5 秒內超過 32ms 的影格。", + "debugBar.long.label": "LONG", + "debugBar.long.tip": "過去 5 秒內的阻塞時間和長任務計數。最大任務:{{max}}。", + "debugBar.delay.label": "DELAY", + "debugBar.delay.tip": "過去 5 秒內觀察到的最差輸入延遲。", + "debugBar.inp.label": "INP", + "debugBar.inp.tip": "過去 5 秒內的近似互動持續時間。這類似於 INP,而非官方的 Web Vitals INP。", + "debugBar.cls.label": "CLS", + "debugBar.cls.tip": "目前應用程式生命週期的累積版面配置位移。", + "debugBar.mem.label": "MEM", + "debugBar.mem.tipUnavailable": "使用的 JS 堆積與堆積限制。僅限 Chromium。", + "debugBar.mem.tip": "使用的 JS 堆積與堆積限制。{{used}} / {{limit}}。", + "debugBar.focus.label": "FOCUS", + "debugBar.focus.tip": "強制所有互動元素顯示焦點樣式", + "debugBar.focus.on": "開啟", + "debugBar.focus.off": "關閉", + "debugBar.direction.label": "DIR", + "debugBar.direction.tip": "在不變更所選語言的情況下,強制設定整個應用程式的版面配置方向", + "debugBar.direction.ltr": "LTR", + "debugBar.direction.rtl": "RTL", + "common.key.ctrl": "Ctrl", + "common.key.alt": "Alt", + "common.key.shift": "Shift", + "common.key.meta": "Meta", + "common.key.space": "空白鍵", + "common.key.backspace": "退格鍵", + "common.key.enter": "Enter", + "common.key.tab": "Tab", + "common.key.delete": "Delete", + "common.key.home": "Home", + "common.key.end": "End", + "common.key.pageUp": "Page Up", + "common.key.pageDown": "Page Down", + "common.key.insert": "Insert", + "common.unknown": "未知", + "error.page.circular": "[循環]", + "error.serverSDK.noServerAvailable": "無可用的伺服器", + "error.serverSDK.serverNotAvailable": "伺服器無法使用", + "error.childStore.persistedCacheCreateFailed": "建立持續性快取失敗", + "error.childStore.persistedProjectMetadataCreateFailed": "建立持續性專案中繼資料失敗", + "error.childStore.persistedProjectIconCreateFailed": "建立持續性專案圖示失敗", + "error.childStore.storeCreateFailed": "建立儲存區失敗", + "terminal.connectionLost.abnormalClose": "WebSocket 異常關閉:{{code}}", +} satisfies Partial> diff --git a/packages/app/src/index.css b/packages/app/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..78a9e103a7f5eb72b974c9de1ee46147c3959ba1 --- /dev/null +++ b/packages/app/src/index.css @@ -0,0 +1,330 @@ +@import "@opencode-ai/ui/styles/tailwind"; +@import "@opencode-ai/session-ui/styles"; +@import "@opencode-ai/ui/v2/styles/tailwind.css"; +@import "tw-animate-css"; + +@font-face { + font-family: "JetBrainsMono Nerd Font Mono"; + src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Inter"; + src: url("/assets/Inter.ttf") format("truetype"); + font-weight: 100 900; + font-style: normal; +} + +@media (display-mode: standalone) { + /* WebKit excludes safe-area insets from dvh in installed apps. */ + #root { + height: 100vh; + } +} + +@layer components { + [data-component="getting-started"] { + container-type: inline-size; + container-name: getting-started; + } + + [data-component="dropdown-menu-content"].desktop-app-menu, + [data-component="dropdown-menu-sub-content"].desktop-app-menu { + min-width: 160px; + padding: 2px; + } + + [data-component="dropdown-menu-content"].desktop-app-menu { + width: 160px; + } + + [data-component="dropdown-menu-sub-content"].desktop-app-menu { + width: max-content; + min-width: 240px; + max-width: min(320px, calc(100vw - 24px)); + } + + [data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-group-label"] { + display: flex; + align-items: center; + height: 28px; + padding: 0 12px; + font-size: var(--font-size-x-small); + font-weight: var(--font-weight-medium); + line-height: 1; + color: var(--text-weak); + } + + [data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-item"], + [data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-sub-trigger"], + [data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-item"], + [data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-sub-trigger"] { + min-height: 28px; + padding: 0 12px; + gap: 8px; + font-weight: var(--font-weight-regular); + line-height: 1; + } + + [data-component="dropdown-menu-content"].desktop-app-menu [data-slot="dropdown-menu-item-label"], + [data-component="dropdown-menu-sub-content"].desktop-app-menu [data-slot="dropdown-menu-item-label"] { + white-space: nowrap; + } + + [data-slot="desktop-app-menu-keybind"] { + margin-left: auto; + color: var(--text-weak); + font-size: var(--font-size-x-small); + font-weight: var(--font-weight-regular); + white-space: nowrap; + } + + [data-slot="desktop-app-menu-chevron"] { + display: flex; + margin-left: auto; + color: var(--icon-base); + } + + [data-component="getting-started-actions"] { + display: flex; + flex-direction: column; + gap: 0.75rem; /* gap-3 */ + } + + [data-component="getting-started-actions"] > [data-component="button"] { + width: 100%; + } + + @container getting-started (min-width: 17rem) { + [data-component="getting-started-actions"] { + flex-direction: row; + align-items: center; + } + + [data-component="getting-started-actions"] > [data-component="button"] { + width: auto; + } + } + + .home-session-group-header::before { + content: ""; + position: absolute; + top: -12px; + left: 0; + width: 100%; + height: 12px; + background: var(--v2-background-bg-base); + } + + .home-session-group-header::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 16px; + pointer-events: none; + background: linear-gradient( + 180deg, + var(--v2-background-bg-base) 0%, + color-mix(in srgb, var(--v2-background-bg-base) 92.0456%, transparent) 7.93%, + color-mix(in srgb, var(--v2-background-bg-base) 84.9947%, transparent) 14.14%, + color-mix(in srgb, var(--v2-background-bg-base) 78.6813%, transparent) 19%, + color-mix(in srgb, var(--v2-background-bg-base) 72.9394%, transparent) 22.85%, + color-mix(in srgb, var(--v2-background-bg-base) 67.6028%, transparent) 26.05%, + color-mix(in srgb, var(--v2-background-bg-base) 62.5055%, transparent) 28.95%, + color-mix(in srgb, var(--v2-background-bg-base) 57.4815%, transparent) 31.91%, + color-mix(in srgb, var(--v2-background-bg-base) 52.3647%, transparent) 35.27%, + color-mix(in srgb, var(--v2-background-bg-base) 46.989%, transparent) 39.4%, + color-mix(in srgb, var(--v2-background-bg-base) 41.1884%, transparent) 44.65%, + color-mix(in srgb, var(--v2-background-bg-base) 34.7969%, transparent) 51.36%, + color-mix(in srgb, var(--v2-background-bg-base) 27.6484%, transparent) 59.9%, + color-mix(in srgb, var(--v2-background-bg-base) 19.5767%, transparent) 70.62%, + color-mix(in srgb, var(--v2-background-bg-base) 10.416%, transparent) 83.87%, + transparent 100% + ); + } + + [data-slot="titlebar-update-loader"] { + display: block; + flex-shrink: 0; + margin: 1px; + width: 12px; + height: 12px; + border-radius: 9999px; + border: 2px solid color-mix(in srgb, var(--v2-icon-icon-accent) 30%, transparent); + border-top-color: var(--v2-icon-icon-accent); + animation: titlebar-update-loader-spin 0.67s linear infinite; + transform-origin: 50% 50%; + } + + @media (prefers-reduced-motion: reduce) { + [data-slot="titlebar-update-loader"] { + animation: none; + } + } + + @keyframes titlebar-update-loader-spin { + to { + transform: rotate(360deg); + } + } + + @keyframes home-projects-fade-top { + from { + visibility: hidden; + } + to { + visibility: visible; + } + } + + @keyframes home-projects-fade-bottom { + from { + visibility: visible; + } + to { + visibility: hidden; + } + } + + [data-slot="home-projects-scroll"] { + timeline-scope: --home-projects-scroll; + } + + [data-slot="model-selector-scroll"] { + timeline-scope: --model-selector-scroll; + } + + [data-slot="manage-models-scroll"] { + timeline-scope: --manage-models-scroll; + } + + [data-slot="home-projects-scroll"]::before, + [data-slot="home-projects-scroll"]::after, + [data-slot="model-selector-scroll"]::before, + [data-slot="model-selector-scroll"]::after, + [data-slot="manage-models-scroll"]::before { + content: ""; + position: absolute; + left: 0; + right: 0; + z-index: 10; + height: 16px; + pointer-events: none; + visibility: hidden; + } + + [data-slot="home-projects-scroll"]::before { + top: 0; + background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent); + } + + [data-slot="model-selector-scroll"]::before { + top: 0; + height: 12px; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--v2-background-bg-layer-01) 80%, transparent) 0%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 78.3336%, transparent) 4.7%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 76.4824%, transparent) 8.9%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 74.368%, transparent) 12.8%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 71.9123%, transparent) 16.56%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 69.037%, transparent) 20.37%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 65.664%, transparent) 24.4%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 61.715%, transparent) 28.83%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 57.1117%, transparent) 33.84%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 51.776%, transparent) 39.6%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 45.6296%, transparent) 46.3%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 38.5944%, transparent) 54.1%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 30.592%, transparent) 63.2%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 21.5443%, transparent) 73.76%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 11.373%, transparent) 85.97%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 0%, transparent) 100% + ); + } + + [data-slot="manage-models-scroll"]::before { + top: 0; + background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent); + } + + [data-slot="home-projects-scroll"]::after { + bottom: 0; + background: linear-gradient(to top, var(--v2-background-bg-base), transparent); + } + + [data-slot="model-selector-scroll"]::after { + bottom: 0; + height: 12px; + background: linear-gradient( + 0deg, + color-mix(in srgb, var(--v2-background-bg-layer-01) 80%, transparent) 0%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 78.3336%, transparent) 4.7%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 76.4824%, transparent) 8.9%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 74.368%, transparent) 12.8%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 71.9123%, transparent) 16.56%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 69.037%, transparent) 20.37%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 65.664%, transparent) 24.4%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 61.715%, transparent) 28.83%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 57.1117%, transparent) 33.84%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 51.776%, transparent) 39.6%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 45.6296%, transparent) 46.3%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 38.5944%, transparent) 54.1%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 30.592%, transparent) 63.2%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 21.5443%, transparent) 73.76%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 11.373%, transparent) 85.97%, + color-mix(in srgb, var(--v2-background-bg-layer-01) 0%, transparent) 100% + ); + } + + @supports (animation-timeline: --home-projects-scroll) and (timeline-scope: --home-projects-scroll) { + [data-slot="home-projects-scroll"] .scroll-view__viewport { + scroll-timeline: --home-projects-scroll y; + } + + [data-slot="home-projects-scroll"]::before { + animation: home-projects-fade-top linear both; + animation-timeline: --home-projects-scroll; + animation-range: 0 0.1px; + } + + [data-slot="home-projects-scroll"]::after { + animation: home-projects-fade-bottom linear both; + animation-timeline: --home-projects-scroll; + animation-range: calc(100% - 1.1px) calc(100% - 1px); + } + } + + @supports (animation-timeline: --model-selector-scroll) and (timeline-scope: --model-selector-scroll) { + [data-slot="model-selector-scroll"] .scroll-view__viewport { + scroll-timeline: --model-selector-scroll y; + } + + [data-slot="model-selector-scroll"]::before { + animation: home-projects-fade-top linear both; + animation-timeline: --model-selector-scroll; + animation-range: 0 0.1px; + } + + [data-slot="model-selector-scroll"]::after { + animation: home-projects-fade-bottom linear both; + animation-timeline: --model-selector-scroll; + animation-range: calc(100% - 1.1px) calc(100% - 1px); + } + } + + @supports (animation-timeline: --manage-models-scroll) and (timeline-scope: --manage-models-scroll) { + [data-slot="manage-models-scroll"] .settings-v2-panel { + scroll-timeline: --manage-models-scroll y; + } + + [data-slot="manage-models-scroll"]::before { + animation: home-projects-fade-top linear both; + animation-timeline: --manage-models-scroll; + animation-range: 0 0.1px; + } + } +} diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0e0ed0286f8e1f7ca1dfbfe4d759812cf08199ee --- /dev/null +++ b/packages/app/src/index.ts @@ -0,0 +1,30 @@ +export { AppBaseProviders, AppInterface } from "./app" +export { useLayout } from "./context/layout" +export { useServerSDK } from "./context/server-sdk" +export { useServerSync } from "./context/server-sync" +export { useServer } from "./context/server" +export { useSettings } from "./context/settings" +export { useTabs } from "./context/tabs" +export { useProviders } from "./hooks/use-providers" +export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker" +export { useCommand } from "./context/command" +export { loadLocaleDict, normalizeLocale, type Locale, useLanguage } from "./context/language" +export { useWslServers } from "./wsl/context" +export { type DisplayBackend, type FatalRendererErrorLog, type Platform, PlatformProvider } from "./context/platform" +export { type UpdaterPlatform, type UpdaterState } from "./updater" +export { + type WslDistroProbe, + type WslInstalledDistro, + type WslJob, + type WslOnlineDistro, + type WslOpencodeCheck, + type WslRuntimeCheck, + type WslServerConfig, + type WslServerItem, + type WslServerRuntime, + type WslServersEvent, + type WslServersPlatform, + type WslServersState, +} from "./wsl/types" +export { ServerConnection } from "./context/server" +export { createDraftStore, type DraftStore } from "./utils/draft-store" diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a0b35763965507feabacd9f6ec01f5d4afd0af7f --- /dev/null +++ b/packages/app/src/pages/directory-layout.tsx @@ -0,0 +1,123 @@ +import { DataProvider } from "@opencode-ai/session-ui/context" +import { showToast } from "@/utils/toast" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { useLocation, useNavigate, useParams } from "@solidjs/router" +import { type Accessor, createEffect, createMemo, createResource, onCleanup, type ParentProps, Show } from "solid-js" +import { useLanguage } from "@/context/language" +import { LocalProvider } from "@/context/local" +import { SDKProvider } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { decode64 } from "@/utils/base64" +import { Schema } from "effect" +import type { ServerConnection } from "@/context/server" +import { sessionHref } from "@/utils/session-route" +import { useServerSync } from "@/context/server-sync" + +export function DirectoryDataProvider( + props: ParentProps<{ + directory: string | Accessor + draftID?: string + server?: Accessor + }>, +) { + const location = useLocation() + const navigate = useNavigate() + const params = useParams() + const sync = useSync() + const serverSync = useServerSync() + const directory = () => (typeof props.directory === "function" ? props.directory() : props.directory) + const slug = createMemo(() => base64Encode(directory())) + const href = (sessionID: string) => { + const server = props.server?.() + if (server) return sessionHref(server, sessionID) + return `/${slug()}/session/${sessionID}` + } + + createEffect(() => { + // A draft lives at /new-session?draftId=… and has no directory segment to normalize. + if (props.draftID || props.server?.()) return + const next = sync().data.path.directory + if (!next || next === directory()) return + const path = location.pathname.slice(slug().length + 1) + navigate(`/${base64Encode(next)}${path}${location.search}${location.hash}`, { replace: true }) + }) + + createResource( + () => params.id, + (id) => + sync() + .session.sync(id) + .catch(() => {}), + ) + + createEffect(() => { + const sessionID = params.id + if (!sessionID) return + serverSync().session.pin(sessionID) + onCleanup(() => serverSync().session.unpin(sessionID)) + }) + + return ( + + {(directory) => ( + navigate(href(sessionID))} + onSessionHref={href} + > + {props.children} + + )} + + ) +} + +export const ProjectDirString = Schema.String.pipe(Schema.brand("ProjectDirString")) +export type ProjectDirString = Schema.Schema.Type + +export function decodeDirectory(dir: string): ProjectDirString | undefined { + const decoded = decode64(dir) + if (!decoded) return + return ProjectDirString.make(decoded) +} + +export default function Layout(props: ParentProps) { + const params = useParams() + const language = useLanguage() + const navigate = useNavigate() + let invalid = "" + + const resolved = createMemo(() => { + if (!params.dir) return "" + return decodeDirectory(params.dir) ?? "" + }) + + createEffect(() => { + const dir = params.dir + if (!dir) return + if (resolved()) { + invalid = "" + return + } + if (invalid === dir) return + invalid = dir + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: language.t("directory.error.invalidUrl"), + }) + navigate("/", { replace: true }) + }) + + return ( + + {(resolved) => ( + + {props.children} + + )} + + ) +} diff --git a/packages/app/src/pages/error-description.test.ts b/packages/app/src/pages/error-description.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..222cd7a1ef76a5d7fa0bb460f1dae50d0ba3282b --- /dev/null +++ b/packages/app/src/pages/error-description.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "bun:test" +import { errorDescriptionKey } from "./error-description" + +describe("error description", () => { + test("describes local server startup errors", () => { + expect(errorDescriptionKey(Object.assign(new Error("migration failed"), { localServerStartup: true }))).toBe( + "error.page.description.localServerStartup", + ) + }) + + test("uses the generic description for other errors", () => { + expect(errorDescriptionKey(new Error("unknown"))).toBe("error.page.description") + expect(errorDescriptionKey(Object.assign(new Error("unknown"), { localServerStartup: false }))).toBe( + "error.page.description", + ) + }) +}) diff --git a/packages/app/src/pages/error-description.ts b/packages/app/src/pages/error-description.ts new file mode 100644 index 0000000000000000000000000000000000000000..86accb6e1df7844c5911fb8e172b0c0baeb7eade --- /dev/null +++ b/packages/app/src/pages/error-description.ts @@ -0,0 +1,11 @@ +export function errorDescriptionKey(error: unknown) { + if ( + typeof error === "object" && + error !== null && + "localServerStartup" in error && + error.localServerStartup === true + ) { + return "error.page.description.localServerStartup" as const + } + return "error.page.description" as const +} diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2fc60954129d373c5baa2219ab530955f24bdab7 --- /dev/null +++ b/packages/app/src/pages/error.tsx @@ -0,0 +1,372 @@ +import { TextField } from "@opencode-ai/ui/text-field" +import * as Sentry from "@sentry/solid" +import { Logo } from "@opencode-ai/ui/logo" +import { Button } from "@opencode-ai/ui/button" +import { Component, createSignal, onMount, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { usePlatform } from "@/context/platform" +import { useLanguage } from "@/context/language" +import { Icon } from "@opencode-ai/ui/icon" +import { errorDescriptionKey } from "./error-description" + +export type InitError = { + name: string + data: Record +} + +type Translator = ReturnType["t"] +const CHAIN_SEPARATOR = "\n" + "─".repeat(40) + "\n" + +function isIssue(value: unknown): value is { message: string; path: string[] } { + if (!value || typeof value !== "object") return false + if (!("message" in value) || !("path" in value)) return false + const message = (value as { message: unknown }).message + const path = (value as { path: unknown }).path + if (typeof message !== "string") return false + if (!Array.isArray(path)) return false + return path.every((part) => typeof part === "string") +} + +function isInitError(error: unknown): error is InitError { + return ( + typeof error === "object" && + error !== null && + "name" in error && + "data" in error && + typeof (error as InitError).data === "object" + ) +} + +function safeJson(value: unknown, circular: string): string { + const seen = new WeakSet() + const json = JSON.stringify( + value, + (_key, val) => { + if (typeof val === "bigint") return val.toString() + if (typeof val === "object" && val) { + if (seen.has(val)) return circular + seen.add(val) + } + return val + }, + 2, + ) + return json ?? String(value) +} + +function formatInitError(error: InitError, t: Translator): string { + const data = error.data + const json = (value: unknown) => safeJson(value, t("error.page.circular")) + switch (error.name) { + case "MCPFailed": { + const name = typeof data.name === "string" ? data.name : "" + return t("error.chain.mcpFailed", { name }) + } + case "ProviderAuthError": { + const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown") + const message = typeof data.message === "string" ? data.message : json(data.message) + return t("error.chain.providerAuthFailed", { provider: providerID, message }) + } + case "APIError": { + const message = typeof data.message === "string" ? data.message : t("error.chain.apiError") + const lines: string[] = [message] + + if (typeof data.statusCode === "number") { + lines.push(t("error.chain.status", { status: data.statusCode })) + } + + if (typeof data.isRetryable === "boolean") { + lines.push(t("error.chain.retryable", { retryable: data.isRetryable })) + } + + if (typeof data.responseBody === "string" && data.responseBody) { + lines.push(t("error.chain.responseBody", { body: data.responseBody })) + } + + return lines.join("\n") + } + case "ProviderModelNotFoundError": { + const { providerID, modelID, suggestions } = data as { + providerID: string + modelID: string + suggestions?: string[] + } + + const suggestionsLine = + Array.isArray(suggestions) && suggestions.length + ? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })] + : [] + + return [ + t("error.chain.modelNotFound", { provider: providerID, model: modelID }), + ...suggestionsLine, + t("error.chain.checkConfig"), + ].join("\n") + } + case "ProviderInitError": { + const providerID = typeof data.providerID === "string" ? data.providerID : t("common.unknown") + return t("error.chain.providerInitFailed", { provider: providerID }) + } + case "ConfigJsonError": { + const path = typeof data.path === "string" ? data.path : json(data.path) + const message = typeof data.message === "string" ? data.message : "" + if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message }) + return t("error.chain.configJsonInvalid", { path }) + } + case "ConfigDirectoryTypoError": { + const path = typeof data.path === "string" ? data.path : json(data.path) + const dir = typeof data.dir === "string" ? data.dir : json(data.dir) + const suggestion = typeof data.suggestion === "string" ? data.suggestion : json(data.suggestion) + return t("error.chain.configDirectoryTypo", { dir, path, suggestion }) + } + case "ConfigFrontmatterError": { + const path = typeof data.path === "string" ? data.path : json(data.path) + const message = typeof data.message === "string" ? data.message : json(data.message) + return t("error.chain.configFrontmatterError", { path, message }) + } + case "ConfigInvalidError": { + const issues = Array.isArray(data.issues) + ? data.issues.filter(isIssue).map((issue) => "↳ " + issue.message + " " + issue.path.join(".")) + : [] + const message = typeof data.message === "string" ? data.message : "" + const path = typeof data.path === "string" ? data.path : json(data.path) + + const line = message + ? t("error.chain.configInvalidWithMessage", { path, message }) + : t("error.chain.configInvalid", { path }) + + return [line, ...issues].join("\n") + } + case "UnknownError": + return typeof data.message === "string" ? data.message : json(data) + default: + if (typeof data.message === "string") return data.message + return json(data) + } +} + +function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string { + const json = (value: unknown) => safeJson(value, t("error.page.circular")) + if (!error) return t("error.chain.unknown") + + if (isInitError(error)) { + const message = formatInitError(error, t) + if (depth > 0 && parentMessage === message) return "" + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" + return indent + `${error.name}\n${message}` + } + + if (error instanceof Error) { + const isDuplicate = depth > 0 && parentMessage === error.message + const parts: string[] = [] + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" + + const header = `${error.name}${error.message ? `: ${error.message}` : ""}` + const stack = error.stack?.trim() + + if (stack) { + const startsWithHeader = stack.startsWith(header) + + if (isDuplicate && startsWithHeader) { + const trace = stack.split("\n").slice(1).join("\n").trim() + if (trace) { + parts.push(indent + trace) + } + } + + if (isDuplicate && !startsWithHeader) { + parts.push(indent + stack) + } + + if (!isDuplicate && startsWithHeader) { + parts.push(indent + stack) + } + + if (!isDuplicate && !startsWithHeader) { + parts.push(indent + `${header}\n${stack}`) + } + } + + if (!stack && !isDuplicate) { + parts.push(indent + header) + } + + if (error.cause) { + const causeResult = formatErrorChain(error.cause, t, depth + 1, error.message) + if (causeResult) { + parts.push(causeResult) + } + } + + return parts.join("\n\n") + } + + if (typeof error === "string") { + if (depth > 0 && parentMessage === error) return "" + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" + return indent + error + } + + const indent = depth > 0 ? `\n${CHAIN_SEPARATOR}${t("error.chain.causedBy")}\n` : "" + return indent + json(error) +} + +function formatError(error: unknown, t: Translator): string { + return formatErrorChain(error, t, 0) +} + +interface ErrorPageProps { + error: unknown +} + +export const ErrorPage: Component = (props) => { + const platform = usePlatform() + const language = useLanguage() + const formattedError = () => formatError(props.error, language.t) + let recordedFatalError: Promise | undefined + const [store, setStore] = createStore({ + actionError: undefined as string | undefined, + }) + + function ensureFatalErrorRecorded() { + recordedFatalError ??= + platform.recordFatalRendererError?.({ + error: formattedError(), + url: location.href, + version: platform.version, + platform: platform.platform, + os: platform.os, + }) ?? Promise.resolve() + return recordedFatalError + } + + onMount(() => { + void ensureFatalErrorRecorded().catch(() => undefined) + }) + + async function checkForUpdates() { + const state = await platform.updater?.check() + setStore("actionError", state?.status === "error" ? state.message : undefined) + } + + async function installUpdate() { + await platform.updater + ?.install() + .then(() => setStore("actionError", undefined)) + .catch((err) => { + setStore("actionError", formatError(err, language.t)) + }) + } + + const updateVersion = () => { + const state = platform.updater?.state() + if (state?.status !== "ready") return + return state.version + } + + async function exportDebugLogs() { + const exportLogs = platform.exportDebugLogs + if (!exportLogs) return + await ensureFatalErrorRecorded() + .then(() => exportLogs()) + .then(() => setStore("actionError", undefined)) + .catch((err) => { + setStore("actionError", formatError(err, language.t)) + }) + } + + return ( +
+
+ +
+

{language.t("error.page.title")}

+

{language.t(errorDescriptionKey(props.error))}

+
+ +
+ + + + + + {(_) => { + const [reported, setReported] = createSignal(false) + return ( + + ) + }} + + + + {platform.updater?.state().status === "checking" + ? language.t("error.page.action.checking") + : language.t("error.page.action.checkUpdates")} + + } + > + {(version) => ( + + )} + + +
+ + {(message) =>

{message()}

} +
+
+
+ {language.t("error.page.report.prefix")} + +
+ + {(version) => ( +

{language.t("error.page.version", { version: version() })}

+ )} +
+
+
+
+ ) +} diff --git a/packages/app/src/pages/home-session-archive.test.ts b/packages/app/src/pages/home-session-archive.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d04e808f46c0e4e5063c6ae0955c1ab4e39de33 --- /dev/null +++ b/packages/app/src/pages/home-session-archive.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from "bun:test" +import { SESSION_TABS_REMOVED_EVENT, readSessionTabsRemovedDetail } from "@/components/titlebar-session-events" +import { archiveHomeSession } from "./home-session-archive" +import type { ServerConnection } from "@/context/server" + +const remote = "remote" as ServerConnection.Key + +test("archiving a Home session removes its open titlebar tab", async () => { + let detail: ReturnType + let removed = false + window.addEventListener( + SESSION_TABS_REMOVED_EVENT, + (event) => { + detail = readSessionTabsRemovedDetail(event) + }, + { once: true }, + ) + + await archiveHomeSession({ + server: remote, + session: { id: "ses_1", directory: "/workspace" }, + archive: async () => undefined, + remove: () => { + removed = true + }, + }) + + expect(removed).toBe(true) + expect(detail).toEqual({ server: remote, directory: "/workspace", sessionIDs: ["ses_1"] }) +}) + +test("reports archive failures without removing the session", async () => { + const failure = new Error("offline") + let error: unknown + let removed = false + + await archiveHomeSession({ + server: remote, + session: { id: "ses_1", directory: "/workspace" }, + archive: async () => Promise.reject(failure), + remove: () => { + removed = true + }, + onError: (value) => { + error = value + }, + }) + + expect(error).toBe(failure) + expect(removed).toBe(false) +}) diff --git a/packages/app/src/pages/home-session-archive.ts b/packages/app/src/pages/home-session-archive.ts new file mode 100644 index 0000000000000000000000000000000000000000..bafca66e72df40eb0e0ad5dc4c4d8b2876b8007c --- /dev/null +++ b/packages/app/src/pages/home-session-archive.ts @@ -0,0 +1,27 @@ +import { notifySessionTabsRemoved } from "@/components/titlebar-session-events" +import type { ServerConnection } from "@/context/server" + +type HomeSession = { + id: string + directory: string +} + +export async function archiveHomeSession(input: { + server: ServerConnection.Key + session: HomeSession + archive: (sessionID: string) => Promise + remove: () => void + onError?: (error: unknown) => void +}) { + await input + .archive(input.session.id) + .then(() => { + input.remove() + notifySessionTabsRemoved({ + server: input.server, + directory: input.session.directory, + sessionIDs: [input.session.id], + }) + }) + .catch((error) => input.onError?.(error)) +} diff --git a/packages/app/src/pages/home-session-open.test.ts b/packages/app/src/pages/home-session-open.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..74b9426654a849643fb67feeaba60182a1ab3beb --- /dev/null +++ b/packages/app/src/pages/home-session-open.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test" +import { shouldOpenSessionInBackground } from "./home-session-open" + +describe("shouldOpenSessionInBackground", () => { + test("opens middle clicks in the background", () => { + expect( + shouldOpenSessionInBackground({ button: 1, mac: true, meta: false, ctrl: false, shift: false, alt: false }), + ).toBe(true) + expect( + shouldOpenSessionInBackground({ button: 2, mac: true, meta: false, ctrl: false, shift: false, alt: false }), + ).toBe(false) + }) + + test("requires only the platform primary modifier", () => { + expect( + shouldOpenSessionInBackground({ button: 0, mac: true, meta: true, ctrl: false, shift: false, alt: false }), + ).toBe(true) + expect( + shouldOpenSessionInBackground({ button: 0, mac: false, meta: false, ctrl: true, shift: false, alt: false }), + ).toBe(true) + expect( + shouldOpenSessionInBackground({ button: 0, mac: true, meta: true, ctrl: false, shift: true, alt: false }), + ).toBe(false) + expect( + shouldOpenSessionInBackground({ button: 0, mac: false, meta: false, ctrl: true, shift: false, alt: true }), + ).toBe(false) + expect( + shouldOpenSessionInBackground({ button: 0, mac: false, meta: true, ctrl: false, shift: false, alt: false }), + ).toBe(false) + }) +}) diff --git a/packages/app/src/pages/home-session-open.ts b/packages/app/src/pages/home-session-open.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f117935b77e9971e2b14a626943e11266b80ef8 --- /dev/null +++ b/packages/app/src/pages/home-session-open.ts @@ -0,0 +1,14 @@ +export function shouldOpenSessionInBackground(input: { + button: number + mac: boolean + meta: boolean + ctrl: boolean + shift: boolean + alt: boolean +}) { + if (input.button === 1) return true + if (input.button !== 0) return false + if (input.shift || input.alt) return false + if (input.mac) return input.meta && !input.ctrl + return input.ctrl && !input.meta +} diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4fadb8a68f24360d0111ced24259dc3716634a5e --- /dev/null +++ b/packages/app/src/pages/home.tsx @@ -0,0 +1,50 @@ +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { createHomeController } from "./home/home-controller" +import { createHomeProjectsController } from "./home/home-projects-controller" +import { HomeUtilityNav } from "./home/home-projects-view" +import { HomeProjects } from "./home/home-projects" +import { createHomeScrollController } from "./home/home-scroll-controller" +import { createHomeSessionSearchController } from "./home/home-session-search-controller" +import { createHomeSessionsController } from "./home/home-sessions-controller" +import { HomeSessions } from "./home/home-sessions" + +export function NewHome() { + const home = createHomeController() + const projects = createHomeProjectsController(home) + const sessions = createHomeSessionsController(home) + const search = createHomeSessionSearchController(home, sessions) + const scroll = createHomeScrollController(sessions.data.groups) + return ( +
+ scroll.viewport.update(event.currentTarget.scrollTop)} + onWheel={scroll.viewport.containOuterWheel} + > +
+ + + +
+
+
+ ) +} diff --git a/packages/app/src/pages/home/home-controller.ts b/packages/app/src/pages/home/home-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b67533629af07e1c6edafa90eeae11d250f28fa --- /dev/null +++ b/packages/app/src/pages/home/home-controller.ts @@ -0,0 +1,121 @@ +import { useGlobal } from "@/context/global" +import { type HomeProjectSelection, useLayout } from "@/context/layout" +import { ServerConnection, useServer } from "@/context/server" +import { useServerSync } from "@/context/server-sync" +import { useTabs } from "@/context/tabs" +import { toggleHomeProjectSelection } from "@/pages/layout/helpers" +import { createEffect, createMemo } from "solid-js" + +export function createHomeController() { + const sync = useServerSync() + const layout = useLayout() + const server = useServer() + const global = useGlobal() + const tabs = useTabs() + const selection = layout.home.selection + const focusedServer = createMemo( + () => global.servers.list().find((conn) => ServerConnection.key(conn) === selection().server) ?? server.current, + ) + const focusedServerCtx = createMemo(() => { + const conn = focusedServer() + if (!conn) return undefined + return global.ensureServerCtx(conn) + }) + const focusedSync = () => focusedServerCtx()?.sync ?? sync() + const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list()) + const recentlyClosed = createMemo( + () => focusedServerCtx()?.projects.recentlyClosed() ?? layout.projects.recentlyClosed(), + ) + const homedir = createMemo(() => focusedSync().data.path.home ?? "") + const selectedProject = createMemo(() => projects().find((project) => project.worktree === selection().directory)) + const newSessionProject = createMemo( + () => + selectedProject() ?? + projects().find((project) => project.worktree === focusedServerCtx()?.projects.last()) ?? + projects()[0], + ) + + createEffect(() => { + const list = global.servers.list() + if (list.some((conn) => ServerConnection.key(conn) === selection().server)) return + const conn = list.find((conn) => ServerConnection.key(conn) === server.key) ?? list[0] + if (conn) setSelection({ server: ServerConnection.key(conn) }) + }) + + function setSelection(next: HomeProjectSelection) { + layout.home.setSelection(next) + } + + function openProjectNewSession(conn: ServerConnection.Any, directory: string) { + const ctx = global.ensureServerCtx(conn) + ctx.projects.open(directory) + ctx.projects.touch(directory) + void tabs.newDraft({ server: ServerConnection.key(conn), directory }) + } + + return { + selection: { + value: selection, + set: setSelection, + focusServer: (conn: ServerConnection.Any) => setSelection({ server: ServerConnection.key(conn) }), + }, + server: { + list: global.servers.list, + health: (conn: ServerConnection.Any) => global.servers.health[ServerConnection.key(conn)], + context: (conn: ServerConnection.Any) => global.ensureServerCtx(conn), + focused: focusedServer, + focusedContext: focusedServerCtx, + focusedSync, + }, + project: { + list: projects, + recentlyClosed, + homedir, + selected: selectedProject, + newSession: newSessionProject, + forServer: (conn: ServerConnection.Any) => global.ensureServerCtx(conn).projects.list(), + select: (conn: ServerConnection.Any, directory: string) => { + const key = ServerConnection.key(conn) + if (global.servers.health[key]?.healthy === false) return + if ( + !global + .ensureServerCtx(conn) + .projects.list() + .some((project) => project.worktree === directory) + ) + return + setSelection(toggleHomeProjectSelection(selection(), key, directory)) + }, + add: (conn: ServerConnection.Any, directories: string[]) => { + const directory = directories[0] + if (!directory) return + const ctx = global.ensureServerCtx(conn) + directories.forEach((item) => { + if (ctx.projects.list().some((project) => project.worktree === item)) return + const location = { directory: item } + void ctx.sdk.api.file + .list({ path: ".", location }) + .then(async (files) => { + if (files.data.length > 0) return ctx.sdk.api.project.current({ location }) + const result = await ctx.sdk.client.project.initGit({ directory: item }) + return result.data ?? ctx.sdk.api.project.current({ location }) + }) + .then((project) => ctx.sync.child(item, { bootstrap: false })[1]("project", project.id)) + .catch(() => undefined) + ctx.projects.open(item) + }) + ctx.projects.touch(directory) + setSelection({ server: ServerConnection.key(conn), directory }) + }, + openNewSession: () => { + const conn = focusedServer() + const project = newSessionProject() + if (!conn || !project) return + openProjectNewSession(conn, project.worktree) + }, + openProjectNewSession, + }, + } +} + +export type HomeController = ReturnType diff --git a/packages/app/src/pages/home/home-projects-controller.tsx b/packages/app/src/pages/home/home-projects-controller.tsx new file mode 100644 index 0000000000000000000000000000000000000000..18c58bd2901c092cfbdebde8f153f64ade1d911a --- /dev/null +++ b/packages/app/src/pages/home/home-projects-controller.tsx @@ -0,0 +1,128 @@ +import { useDirectoryPicker } from "@/components/directory-picker" +import { useServerManagementController } from "@/components/dialog-select-server" +import { useSettingsCommand } from "@/components/settings-dialog" +import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2" +import { type LocalProject } from "@/context/layout" +import { useLanguage } from "@/context/language" +import { useNotification } from "@/context/notification" +import { usePlatform } from "@/context/platform" +import { ServerConnection } from "@/context/server" +import { closeHomeProject, errorMessage, homeProjectDirectories } from "@/pages/layout/helpers" +import { Persist, persisted } from "@/utils/persist" +import { showToast } from "@/utils/toast" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { createResource } from "solid-js" +import { createStore } from "solid-js/store" +import type { HomeController } from "./home-controller" + +export function createHomeProjectsController(home: HomeController) { + const platform = usePlatform() + const pickDirectory = useDirectoryPicker() + const dialog = useDialog() + const language = useLanguage() + const notification = useNotification() + const openSettings = useSettingsCommand() + const serverManagement = useServerManagementController({ navigateOnAdd: false }) + const [_state, setState, _, ready] = persisted( + Persist.global("home.servers", ["home.servers.v1"]), + createStore({ collapsed: {} as Record }), + ) + const [state] = createResource( + () => ready.promise ?? Promise.resolve(), + (promise) => promise.then(() => _state), + { initialValue: _state }, + ) + function directories(project: LocalProject) { + return [project.worktree, ...(project.sandboxes ?? [])] + } + + function canRevealProject(conn: ServerConnection.Any) { + return platform.platform === "desktop" && !!platform.openPath && ServerConnection.local(conn) + } + + return { + copy: { + language, + }, + selection: { + value: home.selection.value, + }, + server: { + list: home.server.list, + health: home.server.health, + projects: home.project.forServer, + collapsed: (conn: ServerConnection.Any) => state().collapsed[ServerConnection.key(conn)] ?? false, + toggleCollapsed: (conn: ServerConnection.Any) => { + const key = ServerConnection.key(conn) + setState("collapsed", key, !state().collapsed[key]) + }, + canDefault: serverManagement.canDefault, + defaultKey: serverManagement.defaultKey, + setDefault: (conn: ServerConnection.Any | undefined) => + serverManagement.setDefault(conn ? ServerConnection.key(conn) : null), + remove: (conn: ServerConnection.Any) => serverManagement.handleRemove(ServerConnection.key(conn)), + edit: (conn: ServerConnection.Http) => dialog.show(() => ), + focus: home.selection.focusServer, + }, + project: { + list: home.project.list, + recentlyClosed: home.project.recentlyClosed, + homedir: home.project.homedir, + select: home.project.select, + add: home.project.add, + openNewSession: home.project.openProjectNewSession, + edit: (conn: ServerConnection.Any, project: LocalProject) => { + void import("@/components/dialog-edit-project-v2").then(({ DialogEditProjectV2 }) => { + void dialog.show(() => ) + }) + }, + unseenCount: (conn: ServerConnection.Any, project: LocalProject) => { + const state = notification.ensureServerState(ServerConnection.key(conn)) + return directories(project).reduce((total, directory) => total + state.project.unseenCount(directory), 0) + }, + clearNotifications: (conn: ServerConnection.Any, project: LocalProject) => { + const state = notification.ensureServerState(ServerConnection.key(conn)) + directories(project) + .filter((directory) => state.project.unseenCount(directory) > 0) + .forEach((directory) => state.project.markViewed(directory)) + }, + choose: (conn: ServerConnection.Any) => { + if (home.server.health(conn)?.healthy === false) return + pickDirectory({ + server: conn, + title: language.t("command.project.open"), + multiple: true, + onSelect: (result) => home.project.add(conn, homeProjectDirectories(result)), + }) + }, + close: (conn: ServerConnection.Any, directory: string) => { + const next = closeHomeProject( + home.selection.value(), + ServerConnection.key(conn), + home.server.context(conn).projects, + directory, + ) + if (next) home.selection.set(next) + }, + move: (conn: ServerConnection.Any, worktree: string, index: number) => { + home.server.context(conn).projects.move(worktree, index) + }, + canReveal: canRevealProject, + reveal: (conn: ServerConnection.Any, project: LocalProject) => { + if (!platform.openPath || !canRevealProject(conn)) return + platform.openPath(project.worktree).catch((cause: unknown) => + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(cause, language.t("common.requestFailed")), + }), + ) + }, + }, + utility: { + settings: openSettings, + help: () => platform.openExternal("https://opencode.ai/desktop-feedback"), + }, + } +} + +export type HomeProjectsController = ReturnType diff --git a/packages/app/src/pages/home/home-projects-view.tsx b/packages/app/src/pages/home/home-projects-view.tsx new file mode 100644 index 0000000000000000000000000000000000000000..eebb0d574882e73036547623b272caea6b1b9441 --- /dev/null +++ b/packages/app/src/pages/home/home-projects-view.tsx @@ -0,0 +1,619 @@ +import { type Accessor, createMemo, For, type JSX, onCleanup, Show, splitProps } from "solid-js" +import { createStore } from "solid-js/store" +import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" +import { isSortable, useSortable } from "@dnd-kit/solid/sortable" +import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" +import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers" +import { RestrictToElement } from "@dnd-kit/dom/modifiers" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/context/layout" +import { ServerConnection } from "@/context/server" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" +import { ServerRowMenuView, serverMenuLabels } from "@/components/server/server-row-menu" +import { ServerHealthIndicator } from "@/components/server/server-row" +import { type ServerHealth } from "@/utils/server-health" +import { fileManagerApp } from "@/utils/file-manager" + +const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap" + +const serverContextMenuID = (server: ServerConnection.Any) => `server:${ServerConnection.key(server)}` +const projectContextMenuID = (server: ServerConnection.Any, directory: string) => + `project:${ServerConnection.key(server)}:${directory}` + +export type HomeProjectsViewProps = { + language: ReturnType + servers: Accessor + projects: Accessor + recentlyClosed: Accessor + selection: Accessor + homedir: Accessor + serverHealth: (server: ServerConnection.Any) => ServerHealth | undefined + projectsForServer: (server: ServerConnection.Any) => LocalProject[] + collapsed: (server: ServerConnection.Any) => boolean + canDefaultServer: Accessor + defaultServerKey: Accessor + canRevealProject: (server: ServerConnection.Any) => boolean + unseenCount: (server: ServerConnection.Any, project: LocalProject) => number + onWheel: (event: WheelEvent) => void + onChooseProject: (server: ServerConnection.Any) => void + onFocusServer: (server: ServerConnection.Any) => void + onToggleCollapsed: (server: ServerConnection.Any) => void + onEditServer: (server: ServerConnection.Http) => void + onSetDefaultServer: (server: ServerConnection.Any | undefined) => void + onRemoveServer: (server: ServerConnection.Any) => void + onMoveProject: (server: ServerConnection.Any, worktree: string, index: number) => void + onSelectProject: (server: ServerConnection.Any, directory: string) => void + onAddProjects: (server: ServerConnection.Any, directories: string[]) => void + onOpenProjectNewSession: (server: ServerConnection.Any, directory: string) => void + onEditProject: (server: ServerConnection.Any, project: LocalProject) => void + onRevealProject: (server: ServerConnection.Any, project: LocalProject) => void + onClearNotifications: (server: ServerConnection.Any, project: LocalProject) => void + onCloseProject: (server: ServerConnection.Any, directory: string) => void + onOpenSettings: () => void + onOpenHelp: () => void +} + +export function HomeProjectsView(props: HomeProjectsViewProps) { + const [contextMenu, setContextMenu] = createStore({ open: undefined as string | undefined }) + const contextMenuProps = { + contextMenuOpen: (id: string) => contextMenu.open === id, + onSetContextMenuOpen: (id: string, open: boolean) => setContextMenu("open", open ? id : undefined), + } + return ( + + ) +} + +export function HomeUtilityNav(props: { + class?: string + onOpenSettings: () => void + onOpenHelp: () => void + language: ReturnType +}) { + return ( +
+ + + {props.language.t("sidebar.settings")} + + + + {props.language.t("sidebar.help")} + +
+ ) +} + +function HomeServerRow(props: { + language: HomeProjectsViewProps["language"] + projectsForServer: HomeProjectsViewProps["projectsForServer"] + contextMenuOpen: HomeProjectsContextMenuProps["contextMenuOpen"] + canDefaultServer: HomeProjectsViewProps["canDefaultServer"] + defaultServerKey: HomeProjectsViewProps["defaultServerKey"] + onFocusServer: HomeProjectsViewProps["onFocusServer"] + onToggleCollapsed: HomeProjectsViewProps["onToggleCollapsed"] + onEditServer: HomeProjectsViewProps["onEditServer"] + onSetDefaultServer: HomeProjectsViewProps["onSetDefaultServer"] + onRemoveServer: HomeProjectsViewProps["onRemoveServer"] + onSetContextMenuOpen: HomeProjectsContextMenuProps["onSetContextMenuOpen"] + onChooseProject: HomeProjectsViewProps["onChooseProject"] + server: ServerConnection.Any + selected: boolean + collapsed: boolean + health: ServerHealth | undefined +}) { + const healthy = () => !!props.health?.healthy + const canToggle = () => healthy() && props.projectsForServer(props.server).length > 0 + const contextMenuID = () => serverContextMenuID(props.server) + onCleanup(() => { + const id = contextMenuID() + if (props.contextMenuOpen(id)) props.onSetContextMenuOpen(id, false) + }) + return ( +
+ props.onFocusServer(props.server)} + > + { + event.preventDefault() + event.stopPropagation() + if (!canToggle()) return + props.onToggleCollapsed(props.server) + }} + onPointerDown={(event) => event.preventDefault()} + > + + +
+ +
+ + {props.server.displayName ?? new URL(props.server.http.url).host} + + {(label) => ( + + {label()} + + )} + + +
+
+ props.onSetDefaultServer(props.server)} + onRemoveDefault={() => props.onSetDefaultServer(undefined)} + onRemove={() => props.onRemoveServer(props.server)} + open={props.contextMenuOpen(contextMenuID())} + onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)} + /> + + } + aria-label={props.language.t("home.project.add")} + disabled={props.health?.healthy === false} + onClick={() => props.onChooseProject(props.server)} + /> + +
+
+ ) +} + +type HomeProjectsContextMenuProps = { + contextMenuOpen: (id: string) => boolean + onSetContextMenuOpen: (id: string, open: boolean) => void +} + +type HomeProjectListProps = HomeProjectsViewProps & + HomeProjectsContextMenuProps & { + server: ServerConnection.Any + items: LocalProject[] + } + +function HomeProjectList(props: HomeProjectListProps) { + let listRef!: HTMLDivElement + + return ( + [ + ...defaults.filter((sensor) => sensor !== PointerSensor), + PointerSensor.configure({ + activationConstraints: (event) => + event.pointerType === "touch" + ? [new PointerActivationConstraints.Delay({ value: 250, tolerance: 5 })] + : [new PointerActivationConstraints.Distance({ value: 4 })], + preventActivation: (event) => event.target instanceof Element && !!event.target.closest("[data-action]"), + }), + ]} + modifiers={[RestrictToVerticalAxis, RestrictToElement.configure({ element: () => listRef })]} + plugins={(defaults) => [ + ...defaults.filter((plugin) => plugin !== AutoScroller && plugin !== Feedback), + AutoScroller.configure({ acceleration: 8, threshold: { x: 0, y: 0.05 } }), + Feedback.configure({ dropAnimation: null }), + ]} + onDragEnd={(event) => { + const source = event.operation.source + if (event.canceled || !isSortable(source)) return + if (source.initialIndex !== source.index) props.onMoveProject(props.server, source.id.toString(), source.index) + if (props.selection().server !== ServerConnection.key(props.server)) + props.onSelectProject(props.server, source.id.toString()) + }} + > +
+ {/* Keyed on worktree strings: the enriched project objects are + recreated on every store or sync update, so iterating them directly + remounts all rows — killing any in-flight drag activation (the + row's sortable unregisters on unmount) and discarding animations. + String keys keep row elements alive and move them on reorder. */} + project.worktree)}> + {(worktree, index) => } + +
+
+ ) +} + +function HomeProjectSlot( + props: HomeProjectListProps & { + worktree: string + index: () => number + }, +) { + const initial = props.items.find((item) => item.worktree === props.worktree) + if (!initial) return + const project = createMemo( + (previous) => props.items.find((item) => item.worktree === props.worktree) ?? previous, + initial, + ) + + return ( + + ) +} + +function HomeProjectEmpty( + props: HomeProjectsViewProps & { + server: ServerConnection.Any + items: LocalProject[] + }, +) { + const unreachable = () => props.serverHealth(props.server)?.healthy === false + return ( +
+ props.onChooseProject(props.server)} + > + + {props.language.t("home.project.add")} + + 0}> +
+
{props.language.t("home.recentlyClosed")}
+
+ + {(project) => } + +
+
+ ) +} + +function HomeRecentlyClosedRow( + props: HomeProjectsViewProps & { + project: LocalProject + server: ServerConnection.Any + }, +) { + const unreachable = () => props.serverHealth(props.server)?.healthy === false + const path = () => { + const home = props.homedir() + const worktree = props.project.worktree + if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}` + return worktree + } + return ( + + props.onAddProjects(props.server, [props.project.worktree])} + > + + {displayName(props.project)} + + + ) +} + +function HomeProjectRow( + props: HomeProjectsViewProps & + HomeProjectsContextMenuProps & { + project: LocalProject + server: ServerConnection.Any + index: () => number + serverSelected: boolean + selected: boolean + unseen: number + }, +) { + const platform = usePlatform() + const serverUnreachable = () => props.serverHealth(props.server)?.healthy === false + const sortable = useSortable({ + get id() { + return props.project.worktree + }, + get index() { + return props.index() + }, + }) + let pointerDownSelected: boolean | undefined + const contextMenuID = () => projectContextMenuID(props.server, props.project.worktree) + onCleanup(() => { + const id = contextMenuID() + if (props.contextMenuOpen(id)) props.onSetContextMenuOpen(id, false) + }) + return ( +
{ + event.preventDefault() + props.onSetContextMenuOpen(contextMenuID(), true) + }} + > + { + // Same-server mouse selection happens on pointerdown (like tabs), + // but only ever selects; selectProject toggles, and deselecting here + // would fire on every drag before the threshold is met. Cross-server + // selection waits for click so reordering a remote server's projects + // does not focus that server and load its session index. Touch is + // excluded so flick-scrolling the list cannot select rows. + pointerDownSelected = undefined + if (event.button !== 0 || event.pointerType === "touch") return + if (!props.serverSelected) return + pointerDownSelected = props.selected + if (!props.selected) props.onSelectProject(props.server, props.project.worktree) + }} + onClick={(event) => { + // The drag sensor calls preventDefault on post-drag clicks; never + // toggle selection as part of a reorder. + if (event.defaultPrevented) return + // Keyboard activation and touch taps keep the original toggle. + if (event.detail === 0 || pointerDownSelected === undefined) { + props.onSelectProject(props.server, props.project.worktree) + return + } + // Mouse: pointerdown already selected unselected rows; a plain click + // on an already-selected row toggles it off. + if (pointerDownSelected) props.onSelectProject(props.server, props.project.worktree) + pointerDownSelected = undefined + }} + > + + {displayName(props.project)} + +
+ props.onSetContextMenuOpen(contextMenuID(), open)} + > + } + aria-label={props.language.t("common.moreOptions")} + /> + + + props.onOpenProjectNewSession(props.server, props.project.worktree)}> + {props.language.t("command.session.new")} + + props.onEditProject(props.server, props.project)}> + {props.language.t("dialog.project.edit.title")} + + + props.onRevealProject(props.server, props.project)}> + {props.language.t( + fileManagerApp(platform.platform === "desktop" ? (platform.os ?? "unknown") : "unknown") + .actionLabel, + )} + + + props.onClearNotifications(props.server, props.project)} + > + {props.language.t("sidebar.project.clearNotifications")} + + + props.onCloseProject(props.server, props.project.worktree)}> + {props.language.t("common.close")} + + + + + } + aria-label={props.language.t("command.session.new")} + onClick={() => props.onOpenProjectNewSession(props.server, props.project.worktree)} + /> +
+
+ ) +} + +function HomeProjectNavButton(props: JSX.ButtonHTMLAttributes) { + const [local, rest] = splitProps(props, ["class", "classList", "children"]) + return ( + + ) +} + +function HomeProjectAvatar(props: { project: LocalProject; outline?: boolean }) { + const name = createMemo(() => displayName(props.project)) + return ( + + ) +} diff --git a/packages/app/src/pages/home/home-projects.tsx b/packages/app/src/pages/home/home-projects.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ff2abf7c16dcce6108e60a7ad38bb2e3bf8530c5 --- /dev/null +++ b/packages/app/src/pages/home/home-projects.tsx @@ -0,0 +1,40 @@ +import type { HomeProjectsController } from "./home-projects-controller" +import { HomeProjectsView } from "./home-projects-view" +import type { HomeScrollController } from "./home-scroll-controller" + +export function HomeProjects(props: { projects: HomeProjectsController; scroll: HomeScrollController }) { + return ( + + ) +} diff --git a/packages/app/src/pages/home/home-scroll-controller.ts b/packages/app/src/pages/home/home-scroll-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..96b0cf46d3eacbe68d548ee801e5dffa966b3743 --- /dev/null +++ b/packages/app/src/pages/home/home-scroll-controller.ts @@ -0,0 +1,145 @@ +import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js" +import { createStore } from "solid-js/store" +import type { HomeSessionGroup } from "./home-sessions-controller" + +const HOME_SESSION_HEADER_STICKY_TOP = 12 +const HOME_SESSION_HEADER_TEXT_HEIGHT = 16 +const HOME_SESSION_HEADER_FADE_DISTANCE = 16 + +export function createHomeScrollController(groups: Accessor) { + const [thumbTrack, setThumbTrack] = createSignal() + const [hoverTarget, setHoverTarget] = createSignal() + const [state, setState] = createStore({ + titleOpacity: {} as Partial>, + }) + const headerRefs = new Map() + const headerOffsets = new Map() + let viewport: HTMLDivElement | undefined + let content: HTMLDivElement | undefined + let positionFrame: number | undefined + let resizeObserver: ResizeObserver | undefined + let stickyTop = HOME_SESSION_HEADER_STICKY_TOP + + createEffect(() => { + const items = groups() + const ids = new Set(items.map((group) => group.id)) + headerRefs.forEach((_, id) => { + if (!ids.has(id)) headerRefs.delete(id) + }) + headerOffsets.forEach((_, id) => { + if (!ids.has(id)) headerOffsets.delete(id) + }) + if (items.length === 0) { + content = undefined + bindResizeObserver() + } + queuePositionUpdate() + }) + + onCleanup(() => { + if (positionFrame !== undefined) cancelAnimationFrame(positionFrame) + resizeObserver?.disconnect() + }) + + function queuePositionUpdate() { + if (typeof requestAnimationFrame === "undefined") { + updatePositionCache() + return + } + if (positionFrame !== undefined) return + positionFrame = requestAnimationFrame(() => { + positionFrame = undefined + updatePositionCache() + }) + } + + function updatePositionCache() { + if (!viewport) return + const header = groups() + .map((group) => headerRefs.get(group.id)) + .find((element) => element !== undefined) + if (header && typeof getComputedStyle === "function") { + const top = Number.parseFloat(getComputedStyle(header).top) + if (Number.isFinite(top)) stickyTop = top + } + groups().forEach((group) => { + const element = headerRefs.get(group.id) + if (element) headerOffsets.set(group.id, element.offsetTop) + }) + update(viewport.scrollTop) + } + + function update(scrollTop: number) { + const items = groups() + items.forEach((group, index) => { + const nextOffset = items + .slice(index + 1) + .map((item) => headerOffsets.get(item.id)) + .find((offset) => offset !== undefined) + const fadeEnd = stickyTop + HOME_SESSION_HEADER_TEXT_HEIGHT + const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop + const opacity = + nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE)) + setState("titleOpacity", group.id, Math.round(opacity * 1000) / 1000) + }) + } + + function bindResizeObserver() { + resizeObserver?.disconnect() + if (typeof ResizeObserver === "undefined") return + resizeObserver = new ResizeObserver(queuePositionUpdate) + if (viewport) resizeObserver.observe(viewport) + if (content) resizeObserver.observe(content) + } + + function containWheel(event: WheelEvent) { + if (!viewport) return + if (event.defaultPrevented || event.ctrlKey || !event.deltaY) return + if (!(event.target instanceof Element)) return + const scrollable = event.target.closest("[data-scrollable]") + if ( + scrollable !== viewport && + scrollable && + (event.deltaY < 0 + ? scrollable.scrollTop > 0 + : scrollable.scrollTop < scrollable.scrollHeight - scrollable.clientHeight) + ) + return + event.preventDefault() + } + + return { + viewport: { + thumbTrack, + hoverTarget, + setThumbTrack, + setHoverTarget, + setViewport: (element: HTMLDivElement) => { + viewport = element + bindResizeObserver() + queuePositionUpdate() + }, + update, + containWheel, + containOuterWheel: (event: WheelEvent) => { + if (!viewport) return + if (event.target instanceof Node && viewport.contains(event.target)) return + containWheel(event) + }, + }, + header: { + setContent: (element: HTMLDivElement) => { + content = element + bindResizeObserver() + queuePositionUpdate() + }, + setHeader: (id: HomeSessionGroup["id"], element: HTMLDivElement) => { + headerRefs.set(id, element) + queuePositionUpdate() + }, + titleOpacity: (id: HomeSessionGroup["id"]) => state.titleOpacity[id] ?? 1, + }, + } +} + +export type HomeScrollController = ReturnType diff --git a/packages/app/src/pages/home/home-session-search-controller.ts b/packages/app/src/pages/home/home-session-search-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e55c4b7469d29055180db7dbf0b37993fc96c88 --- /dev/null +++ b/packages/app/src/pages/home/home-session-search-controller.ts @@ -0,0 +1,114 @@ +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" +import { serverName } from "@/context/server" +import { displayName } from "@/pages/layout/helpers" +import { makeEventListener } from "@solid-primitives/event-listener" +import { createMemo, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" +import type { HomeController } from "./home-controller" +import { homeSessionSearchKey, type HomeSessionRecord, type HomeSessionsController } from "./home-sessions-controller" + +type HomeSessionSearchSource = Pick + +export function createHomeSessionSearchController(home: HomeController, sessions: HomeSessionSearchSource) { + const command = useCommand() + const language = useLanguage() + const [state, setState] = createStore({ value: "", focused: false, highlighted: "" }) + let root: HTMLDivElement | undefined + let input: HTMLInputElement | undefined + let list: HTMLDivElement | undefined + const query = createMemo(() => state.value.trim()) + const results = createMemo(() => { + const value = query().toLowerCase() + if (!value) return [] + return sessions.data + .searchRecords() + .filter((record) => `${record.session.title} ${record.projectName}`.toLowerCase().includes(value)) + }) + const active = createMemo(() => { + const records = results() + if (records.some((record) => homeSessionSearchKey(record) === state.highlighted)) return state.highlighted + return records[0] ? homeSessionSearchKey(records[0]) : "" + }) + const open = createMemo(() => state.focused && query().length > 0) + const placeholder = createMemo(() => { + const project = home.project.selected() + if (project) return language.t("home.sessions.search.placeholder.scoped", { scope: displayName(project) }) + if (home.server.list().length > 1) { + const conn = home.server.focused() + if (conn) return language.t("home.sessions.search.placeholder.scoped", { scope: serverName(conn) }) + } + return language.t("home.sessions.search.placeholder") + }) + + onCleanup( + makeEventListener(document, "pointerdown", (event) => { + if (!open()) return + const target = event.target + if (!(target instanceof Node) || root?.contains(target)) return + close() + }), + ) + + command.register("home.search", () => [ + { + id: "home.sessions.search.focus", + title: placeholder(), + keybind: "mod+f", + hidden: true, + onSelect: focus, + }, + ]) + + function focus() { + input?.focus() + setState("focused", true) + } + + function close() { + setState({ value: "", focused: false }) + } + + function select(record: HomeSessionRecord, options?: { background?: boolean }) { + sessions.session.open(record.session, options) + if (!options?.background) close() + } + + return { + query: { + value: () => state.value, + placeholder, + open, + focus, + input: (value: string) => setState({ value, highlighted: "" }), + close, + }, + result: { + loading: sessions.data.loading, + list: results, + active, + noResultsLabel: () => language.t("home.sessions.search.noResults", { query: query() }), + highlight: (record: HomeSessionRecord) => setState("highlighted", homeSessionSearchKey(record)), + move: (delta: number) => { + const records = results() + if (records.length === 0) return + const index = records.findIndex((record) => homeSessionSearchKey(record) === active()) + const next = ((index === -1 ? 0 : index) + delta + records.length) % records.length + setState("highlighted", homeSessionSearchKey(records[next])) + list?.querySelector(`[data-key="${state.highlighted}"]`)?.scrollIntoView({ block: "nearest" }) + }, + select, + selectActive: () => { + const record = results().find((item) => homeSessionSearchKey(item) === active()) + if (record) select(record) + }, + }, + element: { + setRoot: (element: HTMLDivElement) => (root = element), + setInput: (element: HTMLInputElement) => (input = element), + setList: (element: HTMLDivElement) => (list = element), + }, + } +} + +export type HomeSessionSearchController = ReturnType diff --git a/packages/app/src/pages/home/home-sessions-controller.tsx b/packages/app/src/pages/home/home-sessions-controller.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f306f208cc80268ebecdf69f48ad0ee5a6ec9f80 --- /dev/null +++ b/packages/app/src/pages/home/home-sessions-controller.tsx @@ -0,0 +1,320 @@ +import type { Session } from "@opencode-ai/sdk/v2/client" +import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useQuery } from "@tanstack/solid-query" +import { DateTime } from "luxon" +import { type Accessor, createEffect, createMemo, createRoot, type JSX, startTransition } from "solid-js" +import { produce } from "solid-js/store" +import { useCommand } from "@/context/command" +import { + loadHomeSessionIndex, + retainHomeSessions, + type HomeSessionEvents, +} from "@/context/global-sync/home-session-index" +import type { LocalProject } from "@/context/layout" +import { useLanguage } from "@/context/language" +import { ServerConnection } from "@/context/server" +import { sessionHasOpenTab, useTabs } from "@/context/tabs" +import { compareSessionTime, displayName, errorMessage, projectForSession } from "@/pages/layout/helpers" +import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state" +import { pathKey } from "@/utils/path-key" +import { showToast } from "@/utils/toast" +import { Binary } from "@opencode-ai/core/util/binary" +import { archiveHomeSession } from "../home-session-archive" +import type { HomeController } from "./home-controller" + +const HOME_SESSION_LIMIT = 64 +export type HomeSessionRecord = { + session: Session + project: LocalProject + projectName: string +} + +export type HomeSessionGroup = { + id: "today" | "yesterday" | "older" + title: string + sessions: HomeSessionRecord[] +} + +export type OpenSessionOptions = { background?: boolean } + +export function createHomeSessionsController(home: HomeController) { + const tabs = useTabs() + const command = useCommand() + const dialog = useDialog() + const language = useLanguage() + const projectDirectories = createMemo(() => { + const project = home.project.selected() + if (!project) return home.project.list().flatMap(directories) + return directories(project) + }) + const projectByID = createMemo( + () => new Map(home.project.list().flatMap((project) => (project.id ? [[project.id, project] as const] : []))), + ) + const homeSessions = () => home.server.focusedSync().homeSessions + const sessionEventLoad = useQuery(() => ({ + queryKey: homeSessions().eventsKey, + queryFn: async (): Promise => ({ sequence: 0, entries: [] }), + initialData: { sequence: 0, entries: [] } satisfies HomeSessionEvents, + enabled: false, + })) + const sessionLoad = useQuery(() => ({ + queryKey: homeSessions().indexKey, + enabled: !!home.server.focusedContext(), + queryFn: async ({ signal }) => { + const ctx = home.server.focusedContext() + if (!ctx) return { sessions: [], eventSequence: 0 } + const cache = homeSessions() + const eventSequence = cache.eventSequence() + const index = await loadHomeSessionIndex( + (input, options) => ctx.sdk.client.v2.session.list(input, options), + eventSequence, + signal, + ) + cache.complete(eventSequence) + return index + }, + retry: false, + staleTime: 30_000, + refetchOnMount: true, + refetchOnReconnect: true, + })) + const indexedSessions = createMemo(() => + retainHomeSessions( + homeSessions().sessions(sessionLoad.data, sessionEventLoad.data), + HOME_SESSION_LIMIT, + Date.now(), + ), + ) + const allRecords = createMemo(() => + buildHomeSessionRecords({ + sessions: indexedSessions, + projectDirectories, + projects: home.project.list, + projectByID, + }), + ) + const records = createMemo(() => allRecords().slice(0, HOME_SESSION_LIMIT)) + const groups = createMemo(() => groupSessions(records(), language)) + const prefetched = new Set() + + createEffect(() => { + const ctx = home.server.focusedContext() + const conn = home.server.focused() + if (!ctx || !conn) return + records() + .slice(0, 2) + .forEach((record) => { + const key = `${ServerConnection.key(conn)}\0${record.session.id}` + if (prefetched.has(key)) return + prefetched.add(key) + createRoot((dispose) => { + try { + void ctx.sync.session + .sync(record.session.id) + .then(() => + Promise.all( + (ctx.sync.session.data.message[record.session.id] ?? []).flatMap((message) => + (ctx.sync.session.data.part[message.id] ?? []).flatMap((part) => { + if (part.type !== "text" || !part.text) return [] + return preloadMarkdown(part.text, part.id) + }), + ), + ), + ) + .catch(() => {}) + .finally(dispose) + } catch { + dispose() + } + }) + }) + }) + + command.register("home.palette", () => [ + { + id: "command.palette", + title: language.t("command.palette"), + hidden: true, + onSelect: async () => { + const conn = home.server.focused() + if (!conn) return + const ctx = home.server.focusedContext() + if (!ctx) return + const { DialogHomeCommandPaletteV2 } = await import("@/components/dialog-command-palette-v2") + void dialog.show(() => ( + { + if (!entry.sessionID || !entry.directory || !entry.server) return + const sessionID = entry.sessionID + const server = entry.server + const directory = entry.project?.worktree ?? entry.directory + ctx.projects.open(directory) + ctx.projects.touch(directory) + void startTransition(() => { + const tab = tabs.addSessionTab({ server, sessionId: sessionID }) + tabs.select(tab) + }) + }} + /> + )) + }, + }, + ]) + + return { + copy: { + language, + }, + data: { + records, + groups, + loading: () => sessionLoad.isLoading, + searchRecords: allRecords, + }, + session: { + showProjectName: () => !home.project.selected(), + server: () => home.selection.value().server, + canCreate: () => !!home.project.newSession(), + create: home.project.openNewSession, + open: (session: Session, options?: OpenSessionOptions) => { + const directoryKey = pathKey(session.directory) + const project = + home.project + .list() + .find( + (item) => + pathKey(item.worktree) === directoryKey || + item.sandboxes?.some((sandbox) => pathKey(sandbox) === directoryKey), + ) ?? projectForSession(session, home.project.list(), projectByID()) + const conn = home.server.focused() + if (!conn) return + const directory = project?.worktree ?? session.directory + const ctx = home.server.focusedContext() + if (!ctx) return + ctx.projects.open(directory) + if (options?.background) { + tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id }) + return + } + ctx.projects.touch(directory) + void startTransition(() => { + const tab = tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id }) + tabs.select(tab) + }) + }, + archive: async (session: Session) => { + const conn = home.server.focused() + const ctx = home.server.focusedContext() + if (!conn || !ctx) return + const [, setStore] = ctx.sync.child(session.directory) + if ((await ctx.sdk.protocol) !== "v1") return + await archiveHomeSession({ + server: ServerConnection.key(conn), + session, + archive: (sessionID) => + ctx.sdk.client.session.update({ + sessionID, + directory: session.directory, + time: { archived: Date.now() }, + }), + remove: () => { + setStore( + produce((draft) => { + const match = Binary.search(draft.session, session.id, (item) => item.id) + if (match.found) draft.session.splice(match.index, 1) + }), + ) + homeSessions().remove(session.id) + }, + onError: (cause) => + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(cause, language.t("common.requestFailed")), + }), + }) + }, + }, + tab: { + isOpen: (record: HomeSessionRecord) => + sessionHasOpenTab(tabs.store, home.selection.value().server, record.session), + }, + } +} + +function directories(project: LocalProject) { + return [project.worktree, ...(project.sandboxes ?? [])] +} + +function buildHomeSessionRecords(input: { + sessions: () => Session[] + projectDirectories: () => string[] + projects: () => LocalProject[] + projectByID: () => Map +}) { + const directories = new Set(input.projectDirectories().map(pathKey)) + const sessions = input.sessions().filter((session) => directories.has(pathKey(session.directory))) + return [...new Map(sessions.map((session) => [session.id, session] as const)).values()] + .sort(compareSessionTime) + .flatMap((session) => { + const directory = pathKey(session.directory) + const project = + input + .projects() + .find( + (item) => + pathKey(item.worktree) === directory || item.sandboxes?.some((sandbox) => pathKey(sandbox) === directory), + ) ?? projectForSession(session, input.projects(), input.projectByID()) + if (!project) return [] + return { session, project, projectName: displayName(project) } + }) +} + +export function homeSessionSearchKey(record: HomeSessionRecord) { + return `${pathKey(record.session.directory)}:${record.session.id}` +} + +function groupSessions(records: HomeSessionRecord[], language: ReturnType): HomeSessionGroup[] { + const now = DateTime.local() + const yesterday = now.minus({ days: 1 }) + const todaySessions = records.filter((record) => + DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(now, "day"), + ) + const yesterdaySessions = records.filter((record) => + DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(yesterday, "day"), + ) + const olderSessions = records.filter((record) => { + const time = DateTime.fromMillis(record.session.time.updated ?? record.session.time.created) + return !time.hasSame(now, "day") && !time.hasSame(yesterday, "day") + }) + const olderTitle = + todaySessions.length === 0 && yesterdaySessions.length === 0 + ? language.t("sidebar.project.recentSessions") + : language.t("home.sessions.group.older") + return [ + { id: "today" as const, title: language.t("home.sessions.group.today"), sessions: todaySessions }, + { id: "yesterday" as const, title: language.t("home.sessions.group.yesterday"), sessions: yesterdaySessions }, + { id: "older" as const, title: olderTitle, sessions: olderSessions }, + ].filter((group) => group.sessions.length > 0) +} + +export type HomeSessionsController = ReturnType + +export function HomeSessionStatusController(props: { + server: Accessor + record: HomeSessionRecord + isOpenTab: (record: HomeSessionRecord) => boolean + render: (state: { unread: Accessor; loading: Accessor; open: Accessor }) => JSX.Element +}) { + const avatar = useSessionTabAvatarState( + props.server, + () => props.record.session.directory, + () => props.record.session.id, + ) + return props.render({ + unread: avatar.unread, + loading: avatar.loading, + open: () => props.isOpenTab(props.record), + }) +} diff --git a/packages/app/src/pages/home/home-sessions-view.tsx b/packages/app/src/pages/home/home-sessions-view.tsx new file mode 100644 index 0000000000000000000000000000000000000000..72f2f37616aef893712744282c97d4d8f227f3df --- /dev/null +++ b/packages/app/src/pages/home/home-sessions-view.tsx @@ -0,0 +1,550 @@ +import type { Session } from "@opencode-ai/sdk/v2/client" +import { type Accessor, createMemo, For, Show, Suspense } from "solid-js" +import { Spinner } from "@opencode-ai/ui/spinner" +import { ScrollView } from "@opencode-ai/ui/scroll-view" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { useLanguage } from "@/context/language" +import { ServerConnection } from "@/context/server" +import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar" +import { sessionTitle } from "@/utils/session-title" +import { shouldOpenSessionInBackground } from "../home-session-open" +import { + HomeSessionStatusController, + homeSessionSearchKey, + type HomeSessionGroup, + type HomeSessionRecord, + type OpenSessionOptions, +} from "./home-sessions-controller" + +const SHOW_HOME_SESSION_ARCHIVE = false +const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]" +const HOME_SESSION_SEARCH_RESULTS_ID = "home-session-search-results" + +// Middle-click or Cmd+click on macOS (Ctrl+click elsewhere) opens a session +// tab in the background without navigating, matching browser conventions. +function isBackgroundOpen(event: MouseEvent) { + return shouldOpenSessionInBackground({ + button: event.button, + mac: typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform), + meta: event.metaKey, + ctrl: event.ctrlKey, + shift: event.shiftKey, + alt: event.altKey, + }) +} + +export type HomeSessionsViewProps = { + language: ReturnType + groups: Accessor + showProjectName: Accessor + server: Accessor + canCreateSession: Accessor + searchValue: Accessor + searchPlaceholder: Accessor + searchOpen: Accessor + searchLoading: Accessor + searchResults: Accessor + searchActive: Accessor + searchNoResultsLabel: Accessor + titleOpacity: (id: HomeSessionGroup["id"]) => number + isOpenTab: (record: HomeSessionRecord) => boolean + onCreateSession: () => void + onOpenSession: (session: Session, options?: OpenSessionOptions) => void + onArchiveSession: (session: Session) => Promise + onSetHoverTarget: (element: HTMLElement) => void + onSetThumbTrack: (element: HTMLDivElement) => void + onSetContent: (element: HTMLDivElement) => void + onSetHeader: (id: HomeSessionGroup["id"], element: HTMLDivElement) => void + onWheel: (event: WheelEvent) => void + onSetSearchRoot: (element: HTMLDivElement) => void + onSetSearchInput: (element: HTMLInputElement) => void + onSetSearchList: (element: HTMLDivElement) => void + onSearchFocus: () => void + onSearchInput: (value: string) => void + onSearchClose: () => void + onSearchMove: (delta: number) => void + onSearchSelectActive: () => void + onSearchHighlight: (record: HomeSessionRecord) => void + onSearchSelect: (record: HomeSessionRecord, options?: OpenSessionOptions) => void +} + +export function HomeSessionsView(props: HomeSessionsViewProps) { + return ( +
+
+ + + 0 && props.canCreateSession()}> +
+ + {props.language.t("command.session.new")} + +
+
+
+
+ +
+ ) +} + +function HomeSessionLeadingController(props: { + server: HomeSessionsViewProps["server"] + isOpenTab: HomeSessionsViewProps["isOpenTab"] + record: HomeSessionRecord + revealProjectOnHover: boolean +}) { + return ( + ( + + )} + /> + ) +} + +function HomeSessionLeading(props: { + record: HomeSessionRecord + revealProjectOnHover: boolean + open: boolean + unread: boolean + loading: boolean +}) { + return ( +
+ + + +
+ ) +} + +function HomeSessionSearch(props: HomeSessionsViewProps) { + return ( +
+
+ +
+
+
+ + +
+ } + > + 0} + fallback={ +

+ {props.searchNoResultsLabel()} +

+ } + > +
+

+ {props.language.t("home.sessions.search.sessions")} +

+ +
+ + {(record) => ( + + )} + +
+
+
+
+ +
+
+
+ + +
+ + ) +} + +function HomeSessionSearchResultRow( + props: HomeSessionsViewProps & { + record: HomeSessionRecord + selected: boolean + }, +) { + const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) + const showProjectName = () => props.showProjectName() && props.record.projectName + const key = () => homeSessionSearchKey(props.record) + + return ( + + ) +} + +function HomeSessionGroupHeader(props: { + title: string + titleOpacity: number + onSetRef: (element: HTMLDivElement) => void + elevated?: boolean +}) { + return ( +
+ +
+ ) +} + +function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionRecord }) { + const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) + const showProjectName = () => props.showProjectName() && props.record.projectName + + return ( +
+ + +
+ + } + aria-label={props.language.t("common.archive")} + onClick={(event) => { + event.preventDefault() + event.stopPropagation() + void props.onArchiveSession(props.record.session) + }} + /> + +
+
+
+ ) +} + +function HomeSessionTitle(props: { title: string; showProjectName: boolean; search?: boolean }) { + return ( + + {props.title} + + ) +} + +function HomeSessionProjectName(props: { name: string; search?: boolean }) { + return ( + + {props.name} + + ) +} + +function HomeSessionsEmpty(props: { onNewSession?: () => void; language: ReturnType }) { + return ( +
+
+ {props.language.t("home.sessions.empty")} +
+

+ {props.language.t("home.sessions.empty.description")} +

+ + {(onNewSession) => ( + + {props.language.t("command.session.new")} + + )} + +
+ ) +} + +function HomeSessionSkeleton(props: { label: string }) { + return ( +
+
+ +
+ + ) +} diff --git a/packages/app/src/pages/home/home-sessions.tsx b/packages/app/src/pages/home/home-sessions.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7bda4dd3630f8395210a5299fd067825ebb8ac91 --- /dev/null +++ b/packages/app/src/pages/home/home-sessions.tsx @@ -0,0 +1,47 @@ +import type { HomeScrollController } from "./home-scroll-controller" +import type { HomeSessionSearchController } from "./home-session-search-controller" +import type { HomeSessionsController } from "./home-sessions-controller" +import { HomeSessionsView } from "./home-sessions-view" + +export function HomeSessions(props: { + sessions: HomeSessionsController + search: HomeSessionSearchController + scroll: HomeScrollController +}) { + return ( + + ) +} diff --git a/packages/app/src/pages/home/legacy-home.tsx b/packages/app/src/pages/home/legacy-home.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0556f7cf60f766ecf3cea2857543599d4fa2b3d3 --- /dev/null +++ b/packages/app/src/pages/home/legacy-home.tsx @@ -0,0 +1,142 @@ +import { DialogSelectServer } from "@/components/dialog-select-server" +import { useDirectoryPicker } from "@/components/directory-picker" +import { useGlobal } from "@/context/global" +import { useLanguage } from "@/context/language" +import { type ServerConnection, useServer } from "@/context/server" +import { useServerSync } from "@/context/server-sync" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Icon } from "@opencode-ai/ui/icon" +import { Logo } from "@opencode-ai/ui/logo" +import { useNavigate } from "@solidjs/router" +import { DateTime } from "luxon" +import { createMemo, For, Match, Switch } from "solid-js" + +export function LegacyHome() { + const sync = useServerSync() + const pickDirectory = useDirectoryPicker() + const dialog = useDialog() + const navigate = useNavigate() + const global = useGlobal() + const server = useServer() + const language = useLanguage() + const homedir = createMemo(() => sync().data.path.home) + const serverUnreachable = createMemo(() => global.servers.health[server.key]?.healthy === false) + const recent = createMemo(() => { + return sync() + .data.project.slice() + .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) + .slice(0, 5) + }) + + const serverDotClass = createMemo(() => { + const healthy = global.servers.health[server.key]?.healthy + if (healthy === true) return "bg-icon-success-base" + if (healthy === false) return "bg-icon-critical-base" + return "bg-border-weak-base" + }) + + function openProject(conn: ServerConnection.Any, directory: string) { + const serverCtx = global.ensureServerCtx(conn) + serverCtx.projects.open(directory) + serverCtx.projects.touch(directory) + navigate(`/${base64Encode(directory)}`) + } + + function chooseProject() { + if (serverUnreachable()) return + const conn = server.current + if (!conn) return + + const resolve = (result: string | string[] | null) => { + if (Array.isArray(result)) { + result.forEach((directory) => openProject(conn, directory)) + return + } + if (result) openProject(conn, result) + } + + pickDirectory({ + server: conn, + title: language.t("command.project.open"), + multiple: true, + onSelect: resolve, + }) + } + + return ( +
+ + + + 0}> +
+
+
{language.t("home.recentProjects")}
+ +
+
    + + {(project) => ( + + )} + +
+
+
+ +
+
{language.t("common.loading")}
+ +
+
+ +
+ +
+
{language.t("home.empty.title")}
+
{language.t("home.empty.description")}
+
+ +
+
+
+
+ ) +} diff --git a/packages/app/src/pages/layout-new.tsx b/packages/app/src/pages/layout-new.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6d76ca840178e78185160f7af7e10c91d3aefd01 --- /dev/null +++ b/packages/app/src/pages/layout-new.tsx @@ -0,0 +1,49 @@ +import { createEffect, Suspense, type ParentProps } from "solid-js" +import { createStore } from "solid-js/store" +import { DebugBar } from "@/components/debug-bar" +import { TabsInfoPopup } from "@/components/help-button" +import { Titlebar, type TitlebarUpdate } from "@/components/titlebar" +import { usePlatform } from "@/context/platform" +import { setV2Toast, ToastRegion } from "@/utils/toast" + +export default function NewLayout(props: ParentProps) { + const platform = usePlatform() + const [state, setState] = createStore({ debugTools: true }) + + createEffect(() => setV2Toast(true)) + + const update: TitlebarUpdate = { + version: () => { + const state = platform.updater?.state() + if (state?.status !== "ready") return + return state.version + }, + installing: () => platform.updater?.state().status === "installing", + install: () => void platform.updater?.install(), + } + + return ( +
+ setState("debugTools", (value) => !value) } + : undefined + } + /> +
+ {props.children} +
+ {import.meta.env.DEV && state.debugTools && } + + +
+ ) +} diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..98548ab2333b46f961609302e993efcc268f4a9f --- /dev/null +++ b/packages/app/src/pages/layout.tsx @@ -0,0 +1,2444 @@ +import { + createEffect, + createMemo, + createResource, + For, + on, + onCleanup, + onMount, + ParentProps, + Show, + untrack, + type Accessor, +} from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import { useNavigate, useParams } from "@solidjs/router" +import { useLayout, LocalProject } from "@/context/layout" +import { useServerSync } from "@/context/server-sync" +import { Persist, persisted } from "@/utils/persist" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { decode64 } from "@/utils/base64" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { Button } from "@opencode-ai/ui/button" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Dialog } from "@opencode-ai/ui/dialog" +import { getFilename } from "@opencode-ai/core/util/path" +import { Session } from "@opencode-ai/sdk/v2/client" +import { usePlatform } from "@/context/platform" +import { useSettings } from "@/context/settings" +import { createStore, produce, reconcile } from "solid-js/store" +import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" +import type { DragEvent } from "@thisbeyond/solid-dnd" +import { useProviders } from "@/hooks/use-providers" +import { dismissToast, setV2Toast, showToast, ToastRegion } from "@/utils/toast" +import { useServerSDK } from "@/context/server-sdk" +import { normalizeProjectInfo } from "@/context/global-sync/utils" +import { clearWorkspaceTerminals } from "@/context/terminal" +import { pickSessionCacheEvictions } from "@/context/global-sync/session-cache" +import { useNotification } from "@/context/notification" +import { usePermission } from "@/context/permission" +import { Binary } from "@opencode-ai/core/util/binary" +import { retry } from "@opencode-ai/core/util/retry" +import { playSoundById } from "@/utils/sound" +import { createAim } from "@/utils/aim" +import { Worktree as WorktreeState } from "@/utils/worktree" +import { setSessionHandoff } from "@/pages/session/handoff" +import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope" +import { listAllSessions } from "@/utils/session" + +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" +import { useCommand, type CommandOption } from "@/context/command" +import { ConstrainDragXAxis, getDraggableId } from "@/utils/solid-dnd" +import { DebugBar } from "@/components/debug-bar" +import { TabsInfoPopup } from "@/components/help-button" +import { Titlebar, type TitlebarUpdate } from "@/components/titlebar" +import { useDirectoryPicker } from "@/components/directory-picker" +import { ServerConnection, useServer } from "@/context/server" +import { useLanguage, type Locale } from "@/context/language" +import { pathKey } from "@/utils/path-key" +import { + displayName, + effectiveWorkspaceOrder, + errorMessage, + latestRootSession, + sortedRootSessions, +} from "./layout/helpers" +import { + collectNewSessionDeepLinks, + collectOpenProjectDeepLinks, + deepLinkEvent, + drainPendingDeepLinks, +} from "./layout/deep-links" +import { createInlineEditorController } from "./layout/inline-editor" +import { + LocalWorkspace, + SortableWorkspace, + WorkspaceDragOverlay, + type WorkspaceSidebarContext, +} from "./layout/sidebar-workspace" +import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project" +import { SidebarContent } from "./layout/sidebar-shell" + +export default function LegacyLayout(props: ParentProps) { + const serverSDK = useServerSDK() + const [store, setStore, , ready] = persisted( + Persist.serverGlobal(serverSDK().scope, "layout.page", ["layout.page.v1"]), + createStore({ + lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } }, + activeProject: undefined as string | undefined, + activeWorkspace: undefined as string | undefined, + workspaceOrder: {} as Record, + workspaceName: {} as Record, + workspaceBranchName: {} as Record>, + workspaceExpanded: {} as Record, + gettingStartedDismissed: false, + }), + ) + + const pageReady = createMemo(() => ready()) + + let scrollContainerRef: HTMLDivElement | undefined + let dialogRun = 0 + let dialogDead = false + + const params = useParams() + const serverSync = useServerSync() + const layout = useLayout() + const layoutReady = createMemo(() => layout.ready()) + const platform = usePlatform() + const pickDirectory = useDirectoryPicker() + const settings = useSettings() + const server = useServer() + const notification = useNotification() + const permission = usePermission() + const navigate = useNavigate() + const providers = useProviders(() => undefined) + const dialog = useDialog() + const command = useCommand() + const theme = useTheme() + const language = useLanguage() + createEffect(() => setV2Toast(false)) + const initialDirectory = decode64(params.dir) + const route = createMemo(() => { + const slug = params.dir + if (!slug) return { slug, dir: "" } + const dir = decode64(slug) + if (!dir) return { slug, dir: "" } + const store = serverSync().peek(dir, { bootstrap: false }) + return { + slug, + store, + dir: store[0].path.directory || dir, + } + }) + const availableThemeEntries = createMemo(() => theme.ids().map((id) => [id, theme.themes()[id]] as const)) + const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"] + const colorSchemeKey: Record = { + system: "theme.scheme.system", + light: "theme.scheme.light", + dark: "theme.scheme.dark", + } + const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme]) + const currentDir = createMemo(() => route().dir) + + const [state, setState] = createStore({ + autoselect: !initialDirectory, + busyWorkspaces: {} as Record, + hoverProject: undefined as string | undefined, + scrollSessionKey: undefined as string | undefined, + nav: undefined as HTMLElement | undefined, + sortNow: Date.now(), + sizing: false, + peek: undefined as string | undefined, + peeked: false, + debugTools: true, + }) + + const updateVersion = () => { + const state = platform.updater?.state() + if (state?.status !== "ready") return + return state.version + } + const installUpdate = () => void platform.updater?.install() + const titlebarUpdate: TitlebarUpdate = { + version: updateVersion, + installing: () => platform.updater?.state().status === "installing", + install: installUpdate, + } + + const editor = createInlineEditorController() + const setBusy = (directory: string, value: boolean) => { + const key = pathKey(directory) + if (value) { + setState("busyWorkspaces", key, true) + return + } + setState( + "busyWorkspaces", + produce((draft) => { + delete draft[key] + }), + ) + } + const isBusy = (directory: string) => !!state.busyWorkspaces[pathKey(directory)] + const navLeave = { current: undefined as number | undefined } + const sortNow = () => state.sortNow + let sizet: number | undefined + let sortNowInterval: ReturnType | undefined + const sortNowTimeout = setTimeout( + () => { + setState("sortNow", Date.now()) + sortNowInterval = setInterval(() => setState("sortNow", Date.now()), 60_000) + }, + 60_000 - (Date.now() % 60_000), + ) + + const aim = createAim({ + enabled: () => !layout.sidebar.opened(), + active: () => state.hoverProject, + el: () => state.nav?.querySelector("[data-component='sidebar-rail']") ?? state.nav, + onActivate: (directory) => { + serverSync().child(directory) + setState("hoverProject", directory) + }, + }) + + onCleanup(() => { + dialogDead = true + dialogRun += 1 + if (navLeave.current !== undefined) clearTimeout(navLeave.current) + clearTimeout(sortNowTimeout) + if (sortNowInterval) clearInterval(sortNowInterval) + if (sizet !== undefined) clearTimeout(sizet) + if (peekt !== undefined) clearTimeout(peekt) + aim.reset() + }) + + onMount(() => { + const stop = () => setState("sizing", false) + const blur = () => reset() + const hide = () => { + if (document.visibilityState !== "hidden") return + reset() + } + makeEventListener(window, "pointerup", stop) + makeEventListener(window, "pointercancel", stop) + makeEventListener(window, "blur", stop) + makeEventListener(window, "blur", blur) + makeEventListener(document, "visibilitychange", hide) + }) + + const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined) + const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering()) + const setHoverProject = (value: string | undefined) => { + setState("hoverProject", value) + if (value !== undefined) return + aim.reset() + } + const clearHoverProjectSoon = () => queueMicrotask(() => setHoverProject(undefined)) + + const disarm = () => { + if (navLeave.current === undefined) return + clearTimeout(navLeave.current) + navLeave.current = undefined + } + + const reset = () => { + disarm() + setHoverProject(undefined) + } + + const arm = () => { + if (layout.sidebar.opened()) return + if (state.hoverProject === undefined) return + disarm() + navLeave.current = window.setTimeout(() => { + navLeave.current = undefined + setHoverProject(undefined) + }, 300) + } + + let peekt: number | undefined + + const hoverProjectData = createMemo(() => { + const id = state.hoverProject + if (!id) return + return layout.projects.list().find((project) => project.worktree === id) + }) + + const peekProject = createMemo(() => { + const id = state.peek + if (!id) return + return layout.projects.list().find((project) => project.worktree === id) + }) + + createEffect(() => { + const p = hoverProjectData() + if (p) { + if (peekt !== undefined) { + clearTimeout(peekt) + peekt = undefined + } + setState("peek", p.worktree) + setState("peeked", true) + return + } + + setState("peeked", false) + if (state.peek === undefined) return + if (peekt !== undefined) clearTimeout(peekt) + peekt = window.setTimeout(() => { + peekt = undefined + setState("peek", undefined) + }, 180) + }) + + createEffect(() => { + if (!layout.sidebar.opened()) return + setHoverProject(undefined) + }) + + createEffect(() => { + if (!state.autoselect) return + const dir = params.dir + if (!dir) return + const directory = decode64(dir) + if (!directory) return + setState("autoselect", false) + }) + + const editorOpen = editor.editorOpen + const openEditor = editor.openEditor + const closeEditor = editor.closeEditor + const setEditor = editor.setEditor + const InlineEditor = editor.InlineEditor + + const clearSidebarHoverState = () => { + if (layout.sidebar.opened()) return + reset() + } + + const navigateWithSidebarReset = (href: string) => { + clearSidebarHoverState() + navigate(href) + layout.mobileSidebar.hide() + } + + function cycleTheme(direction = 1) { + const ids = availableThemeEntries().map(([id]) => id) + if (ids.length === 0) return + const currentIndex = ids.indexOf(theme.themeId()) + const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + ids.length) % ids.length + const nextThemeId = ids[nextIndex] + theme.setTheme(nextThemeId) + showToast({ + title: language.t("toast.theme.title"), + description: theme.name(nextThemeId), + }) + } + + function cycleColorScheme(direction = 1) { + const current = theme.colorScheme() + const currentIndex = colorSchemeOrder.indexOf(current) + const nextIndex = + currentIndex === -1 ? 0 : (currentIndex + direction + colorSchemeOrder.length) % colorSchemeOrder.length + const next = colorSchemeOrder[nextIndex] + theme.setColorScheme(next) + showToast({ + title: language.t("toast.scheme.title"), + description: colorSchemeLabel(next), + }) + } + + function setLocale(next: Locale) { + if (next === language.locale()) return + language.setLocale(next) + showToast({ + title: language.t("toast.language.title"), + description: language.t("toast.language.description", { language: language.label(next) }), + }) + } + + function cycleLanguage(direction = 1) { + const locales = language.locales + const currentIndex = locales.indexOf(language.locale()) + const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + locales.length) % locales.length + const next = locales[nextIndex] + if (!next) return + setLocale(next) + } + + const useSDKNotificationToasts = () => + onMount(() => { + const toastBySession = new Map() + const alertedAtBySession = new Map() + const cooldownMs = 5000 + + const dismissSessionAlert = (sessionKey: string) => { + const toastId = toastBySession.get(sessionKey) + if (toastId === undefined) return + dismissToast(toastId) + toastBySession.delete(sessionKey) + alertedAtBySession.delete(sessionKey) + } + + const unsub = serverSDK().event.listen((e) => { + if (e.details?.type === "worktree.ready") { + setBusy(e.name, false) + WorktreeState.ready(serverSDK().scope, e.name) + return + } + + if (e.details?.type === "worktree.failed") { + setBusy(e.name, false) + WorktreeState.failed( + serverSDK().scope, + e.name, + e.details.properties?.message ?? language.t("common.requestFailed"), + ) + return + } + + if ( + e.details?.type === "question.replied" || + e.details?.type === "question.rejected" || + e.details?.type === "permission.replied" + ) { + const props = e.details.properties as { sessionID: string } + const sessionKey = `${e.name}:${props.sessionID}` + dismissSessionAlert(sessionKey) + return + } + + if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return + const title = + e.details.type === "permission.asked" + ? language.t("notification.permission.title") + : language.t("notification.question.title") + const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const) + const directory = e.name + const props = e.details.properties + if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return + + const [store] = serverSync().child(directory, { bootstrap: false }) + const session = store.session.find((s) => s.id === props.sessionID) + const sessionKey = `${directory}:${props.sessionID}` + + const sessionTitle = session?.title ?? language.t("command.session.new") + const projectName = getFilename(directory) + const description = + e.details.type === "permission.asked" + ? language.t("notification.permission.description", { sessionTitle, projectName }) + : language.t("notification.question.description", { sessionTitle, projectName }) + const href = `/${base64Encode(directory)}/session/${props.sessionID}` + + const now = Date.now() + const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0 + if (now - lastAlerted < cooldownMs) return + alertedAtBySession.set(sessionKey, now) + + if (e.details.type === "permission.asked") { + if (settings.sounds.permissionsEnabled()) { + void playSoundById(settings.sounds.permissions()) + } + if (settings.notifications.permissions()) { + void platform.notify(title, description, () => navigate(href)) + } + } + + if (e.details.type === "question.asked") { + if (settings.notifications.agent()) { + void platform.notify(title, description, () => navigate(href)) + } + } + + const currentSession = params.id + if (pathKey(directory) === pathKey(currentDir()) && props.sessionID === currentSession) return + if (pathKey(directory) === pathKey(currentDir()) && session?.parentID === currentSession) return + + dismissSessionAlert(sessionKey) + + const toastId = showToast({ + persistent: true, + icon, + title, + description, + actions: [ + { + label: language.t("notification.action.goToSession"), + onClick: () => navigate(href), + }, + { + label: language.t("common.dismiss"), + onClick: "dismiss", + }, + ], + }) + toastBySession.set(sessionKey, toastId) + }) + onCleanup(unsub) + + createEffect(() => { + const currentSession = params.id + if (!currentDir() || !currentSession) return + const sessionKey = `${currentDir()}:${currentSession}` + dismissSessionAlert(sessionKey) + const [store] = serverSync().child(currentDir(), { bootstrap: false }) + const childSessions = store.session.filter((s) => s.parentID === currentSession) + for (const child of childSessions) { + dismissSessionAlert(`${currentDir()}:${child.id}`) + } + }) + }) + + useSDKNotificationToasts() + + function scrollToSession(sessionId: string, sessionKey: string) { + if (!scrollContainerRef) return + if (state.scrollSessionKey === sessionKey) return + const element = scrollContainerRef.querySelector(`[data-session-id="${sessionId}"]`) + if (!element) return + const containerRect = scrollContainerRef.getBoundingClientRect() + const elementRect = element.getBoundingClientRect() + if (elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom) { + setState("scrollSessionKey", sessionKey) + return + } + setState("scrollSessionKey", sessionKey) + element.scrollIntoView({ block: "nearest", behavior: "smooth" }) + } + + const currentProject = createMemo(() => { + const directory = currentDir() + if (!directory) return + const key = pathKey(directory) + + const projects = layout.projects.list() + + const sandbox = projects.find((p) => p.sandboxes?.some((item) => pathKey(item) === key)) + if (sandbox) return sandbox + + const direct = projects.find((p) => pathKey(p.worktree) === key) + if (direct) return direct + + const [child] = serverSync().child(directory, { bootstrap: false }) + const id = child.project + if (!id) return + + const meta = serverSync().data.project.find((p) => p.id === id) + const root = meta?.worktree + if (!root) return + + return projects.find((p) => p.worktree === root) + }) + + const [autoselecting] = createResource(async () => { + await ready.promise + await layout.ready.promise + if (!untrack(() => state.autoselect)) return + + const list = layout.projects.list() + const last = server.projects.last() + + if (list.length === 0) { + if (!last) return + await openProject(last, true) + } else { + const next = list.find((project) => project.worktree === last) ?? list[0] + if (!next) return + await openProject(next.worktree, true) + } + }) + + const workspaceName = (directory: string, projectId?: string, branch?: string) => { + const key = pathKey(directory) + const direct = store.workspaceName[key] ?? store.workspaceName[directory] + if (direct) return direct + if (!projectId) return + if (!branch) return + return store.workspaceBranchName[projectId]?.[branch] + } + + const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => { + const key = pathKey(directory) + setStore("workspaceName", key, next) + if (!projectId) return + if (!branch) return + if (!store.workspaceBranchName[projectId]) { + setStore("workspaceBranchName", projectId, {}) + } + setStore("workspaceBranchName", projectId, branch, next) + } + + const workspaceLabel = (directory: string, branch?: string, projectId?: string) => + workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) + + const workspaceSetting = createMemo(() => { + const project = currentProject() + if (!project) return false + if (project.vcs !== "git") return false + return layout.sidebar.workspaces(project.worktree)() + }) + + const visibleSessionDirs = createMemo(() => { + const project = currentProject() + if (!project) return [] as string[] + if (!workspaceSetting()) return [project.worktree] + + const activeDir = currentDir() + return workspaceIds(project).filter((directory) => { + const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree + const active = pathKey(directory) === pathKey(activeDir) + return expanded || active + }) + }) + + createEffect(() => { + if (!pageReady()) return + if (!layoutReady()) return + const projects = layout.projects.list() + for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) { + if (!expanded) continue + const key = pathKey(directory) + const project = projects.find( + (item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key), + ) + if (!project) continue + if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue + setStore("workspaceExpanded", directory, false) + } + }) + + const currentSessions = createMemo(() => { + const now = Date.now() + const dirs = visibleSessionDirs() + if (dirs.length === 0) return [] as Session[] + + const result: Session[] = [] + for (const dir of dirs) { + const [dirStore] = serverSync().child(dir, { bootstrap: true }) + const dirSessions = sortedRootSessions(dirStore, now) + result.push(...dirSessions) + } + return result + }) + + type PrefetchQueue = { + inflight: Set + pending: string[] + pendingSet: Set + running: number + } + + const prefetchChunk = 200 + const prefetchConcurrency = 2 + const prefetchPendingLimit = 10 + const span = 4 + const prefetchToken = { value: 0 } + const prefetchQueues = new Map() + + const PREFETCH_MAX_SESSIONS_PER_DIR = 10 + const prefetchedByDir = new Map>() + + const lruFor = (directory: string) => { + const existing = prefetchedByDir.get(directory) + if (existing) return existing + const created = new Set() + prefetchedByDir.set(directory, created) + return created + } + + const markPrefetched = (directory: string, sessionID: string) => { + const lru = lruFor(directory) + return pickSessionCacheEvictions({ + seen: lru, + keep: sessionID, + limit: PREFETCH_MAX_SESSIONS_PER_DIR, + preserve: params.id && pathKey(directory) === pathKey(currentDir()) ? [params.id] : undefined, + }) + } + + createEffect(() => { + const active = new Set(visibleSessionDirs()) + for (const directory of prefetchedByDir.keys()) { + if (active.has(directory)) continue + prefetchedByDir.delete(directory) + } + }) + + createEffect(() => { + route() + serverSDK().url + + prefetchToken.value += 1 + prefetchQueues.clear() + }) + + createEffect(() => { + const visible = new Set(visibleSessionDirs()) + for (const [directory, q] of prefetchQueues) { + if (visible.has(directory)) continue + q.pending.length = 0 + q.pendingSet.clear() + if (q.running === 0) prefetchQueues.delete(directory) + } + }) + + const queueFor = (directory: string) => { + const existing = prefetchQueues.get(directory) + if (existing) return existing + + const created: PrefetchQueue = { + inflight: new Set(), + pending: [], + pendingSet: new Set(), + running: 0, + } + prefetchQueues.set(directory, created) + return created + } + + async function prefetchMessages(directory: string, sessionID: string, token: number) { + await serverSync() + .session.prefetch(sessionID, prefetchChunk) + .catch(() => {}) + if (prefetchToken.value !== token) return + for (const stale of markPrefetched(directory, sessionID)) serverSync().session.evict(stale) + } + + const pumpPrefetch = (directory: string) => { + const q = queueFor(directory) + if (q.running >= prefetchConcurrency) return + + const sessionID = q.pending.shift() + if (!sessionID) return + + q.pendingSet.delete(sessionID) + q.inflight.add(sessionID) + q.running += 1 + + const token = prefetchToken.value + + void prefetchMessages(directory, sessionID, token).finally(() => { + q.running -= 1 + q.inflight.delete(sessionID) + pumpPrefetch(directory) + }) + } + + const prefetchSession = (session: Session, priority: "high" | "low" = "low") => { + const directory = session.directory + if (!directory) return + + const cached = untrack(() => !serverSync().session.shouldPrefetch(session.id, prefetchChunk)) + if (cached) return + + const q = queueFor(directory) + if (q.inflight.has(session.id)) return + if (q.pendingSet.has(session.id)) { + if (priority !== "high") return + const index = q.pending.indexOf(session.id) + if (index > 0) { + q.pending.splice(index, 1) + q.pending.unshift(session.id) + } + return + } + + const lru = lruFor(directory) + const known = lru.has(session.id) + if (!known && lru.size >= PREFETCH_MAX_SESSIONS_PER_DIR && priority !== "high") return + + if (priority === "high") q.pending.unshift(session.id) + if (priority !== "high") q.pending.push(session.id) + q.pendingSet.add(session.id) + + while (q.pending.length > prefetchPendingLimit) { + const dropped = q.pending.pop() + if (!dropped) continue + q.pendingSet.delete(dropped) + } + + pumpPrefetch(directory) + } + + const warm = (sessions: Session[], index: number) => { + for (let offset = 1; offset <= span; offset++) { + const next = sessions[index + offset] + if (next) prefetchSession(next, offset === 1 ? "high" : "low") + + const prev = sessions[index - offset] + if (prev) prefetchSession(prev, offset === 1 ? "high" : "low") + } + } + + createEffect(() => { + const sessions = currentSessions() + if (sessions.length === 0) return + + const index = params.id ? sessions.findIndex((s) => s.id === params.id) : 0 + if (index === -1) return + + if (!params.id) { + const first = sessions[index] + if (first) prefetchSession(first, "high") + } + + warm(sessions, index) + }) + + function navigateSessionByOffset(offset: number) { + const sessions = currentSessions() + if (sessions.length === 0) return + + const sessionIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1 + + let targetIndex: number + if (sessionIndex === -1) { + targetIndex = offset > 0 ? 0 : sessions.length - 1 + } else { + targetIndex = (sessionIndex + offset + sessions.length) % sessions.length + } + + const session = sessions[targetIndex] + if (!session) return + + prefetchSession(session, "high") + warm(sessions, targetIndex) + + navigateToSession(session) + } + + function navigateProjectByOffset(offset: number) { + const projects = layout.projects.list() + if (projects.length === 0) return + + const current = currentProject()?.worktree + const fallback = currentDir() ? projectRoot(currentDir()) : undefined + const active = current ?? fallback + const index = active ? projects.findIndex((project) => project.worktree === active) : -1 + + const target = + index === -1 + ? offset > 0 + ? projects[0] + : projects[projects.length - 1] + : projects[(index + offset + projects.length) % projects.length] + if (!target) return + + // warm up child store to prevent flicker + serverSync().child(target.worktree) + void openProject(target.worktree) + } + + function navigateToProjectIndex(index: number) { + const projects = layout.projects.list() + const target = projects[index] + if (!target) return + + serverSync().child(target.worktree) + void openProject(target.worktree) + } + + function navigateSessionByUnseen(offset: number) { + const sessions = currentSessions() + if (sessions.length === 0) return + + const hasUnseen = sessions.some((session) => notification.session.unseenCount(session.id) > 0) + if (!hasUnseen) return + + const activeIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1 + const start = activeIndex === -1 ? (offset > 0 ? -1 : 0) : activeIndex + + for (let i = 1; i <= sessions.length; i++) { + const index = offset > 0 ? (start + i) % sessions.length : (start - i + sessions.length) % sessions.length + const session = sessions[index] + if (!session) continue + if (notification.session.unseenCount(session.id) === 0) continue + + prefetchSession(session, "high") + warm(sessions, index) + + navigateToSession(session) + return + } + } + + async function archiveSession(session: Session) { + if ((await serverSDK().protocol) !== "v1") return + const [store, setStore] = serverSync().child(session.directory) + const sessions = store.session ?? [] + const index = sessions.findIndex((s) => s.id === session.id) + const nextSession = sessions[index + 1] ?? sessions[index - 1] + + await serverSDK().client.session.update({ + sessionID: session.id, + directory: session.directory, + time: { archived: Date.now() }, + }) + setStore( + produce((draft) => { + const match = Binary.search(draft.session, session.id, (s) => s.id) + if (match.found) draft.session.splice(match.index, 1) + }), + ) + if (session.id === params.id) { + if (nextSession) { + navigate(`/${params.dir}/session/${nextSession.id}`) + } else { + navigate(`/${params.dir}/session`) + } + } + } + + command.register("layout", () => { + const commands: CommandOption[] = [ + { + id: "sidebar.toggle", + title: language.t("command.sidebar.toggle"), + category: language.t("command.category.view"), + keybind: "mod+b", + onSelect: () => layout.sidebar.toggle(), + }, + { + id: "project.open", + title: language.t("command.project.open"), + category: language.t("command.category.project"), + keybind: "mod+o", + onSelect: () => chooseProject(), + }, + { + id: "project.previous", + title: language.t("command.project.previous"), + category: language.t("command.category.project"), + keybind: "mod+alt+arrowup", + onSelect: () => navigateProjectByOffset(-1), + }, + { + id: "project.next", + title: language.t("command.project.next"), + category: language.t("command.category.project"), + keybind: "mod+alt+arrowdown", + onSelect: () => navigateProjectByOffset(1), + }, + { + id: "provider.connect", + title: language.t("command.provider.connect"), + category: language.t("command.category.provider"), + onSelect: () => connectProvider(), + }, + { + id: "server.switch", + title: language.t("command.server.switch"), + category: language.t("command.category.server"), + onSelect: () => openServer(), + }, + { + id: "settings.open", + title: language.t("command.settings.open"), + category: language.t("command.category.settings"), + keybind: "mod+comma", + onSelect: () => openSettings(), + }, + { + id: "session.previous", + title: language.t("command.session.previous"), + category: language.t("command.category.session"), + keybind: "alt+arrowup", + onSelect: () => navigateSessionByOffset(-1), + }, + { + id: "session.next", + title: language.t("command.session.next"), + category: language.t("command.category.session"), + keybind: "alt+arrowdown", + onSelect: () => navigateSessionByOffset(1), + }, + { + id: "session.previous.unseen", + title: language.t("command.session.previous.unseen"), + category: language.t("command.category.session"), + keybind: "shift+alt+arrowup", + onSelect: () => navigateSessionByUnseen(-1), + }, + { + id: "session.next.unseen", + title: language.t("command.session.next.unseen"), + category: language.t("command.category.session"), + keybind: "shift+alt+arrowdown", + onSelect: () => navigateSessionByUnseen(1), + }, + { + id: "workspace.new", + title: language.t("workspace.new"), + category: language.t("command.category.workspace"), + keybind: "mod+shift+w", + disabled: !workspaceSetting(), + onSelect: () => { + const project = currentProject() + if (!project) return + return createWorkspace(project) + }, + }, + { + id: "workspace.toggle", + title: language.t("command.workspace.toggle"), + description: language.t("command.workspace.toggle.description"), + category: language.t("command.category.workspace"), + slash: "workspace", + disabled: !currentProject() || currentProject()?.vcs !== "git", + onSelect: () => { + const project = currentProject() + if (!project) return + if (project.vcs !== "git") return + const wasEnabled = layout.sidebar.workspaces(project.worktree)() + layout.sidebar.toggleWorkspaces(project.worktree) + showToast({ + title: wasEnabled + ? language.t("toast.workspace.disabled.title") + : language.t("toast.workspace.enabled.title"), + description: wasEnabled + ? language.t("toast.workspace.disabled.description") + : language.t("toast.workspace.enabled.description"), + }) + }, + }, + { + id: "theme.cycle", + title: language.t("command.theme.cycle"), + category: language.t("command.category.theme"), + keybind: "mod+shift+t", + onSelect: () => cycleTheme(1), + }, + ] + + Array.from({ length: 9 }, (_, i) => { + const index = i + const number = index + 1 + commands.push({ + id: `project.${number}`, + category: language.t("command.category.project"), + title: `Open Project {number}`, + keybind: `mod+${number}`, + disabled: layout.projects.list().length <= index, + hidden: true, + onSelect: () => navigateToProjectIndex(index), + }) + }) + + for (const [id] of availableThemeEntries()) { + commands.push({ + id: `theme.set.${id}`, + title: language.t("command.theme.set", { theme: theme.name(id) }), + category: language.t("command.category.theme"), + onSelect: () => theme.commitPreview(), + onHighlight: () => { + theme.previewTheme(id) + return () => theme.cancelPreview() + }, + }) + } + + commands.push({ + id: "theme.scheme.cycle", + title: language.t("command.theme.scheme.cycle"), + category: language.t("command.category.theme"), + keybind: "mod+shift+s", + onSelect: () => cycleColorScheme(1), + }) + + for (const scheme of colorSchemeOrder) { + commands.push({ + id: `theme.scheme.${scheme}`, + title: language.t("command.theme.scheme.set", { scheme: colorSchemeLabel(scheme) }), + category: language.t("command.category.theme"), + onSelect: () => theme.commitPreview(), + onHighlight: () => { + theme.previewColorScheme(scheme) + return () => theme.cancelPreview() + }, + }) + } + + commands.push({ + id: "language.cycle", + title: language.t("command.language.cycle"), + category: language.t("command.category.language"), + onSelect: () => cycleLanguage(1), + }) + + for (const locale of language.locales) { + commands.push({ + id: `language.set.${locale}`, + title: language.t("command.language.set", { language: language.label(locale) }), + category: language.t("command.category.language"), + onSelect: () => setLocale(locale), + }) + } + + return commands + }) + + function connectProvider() { + const run = ++dialogRun + void import("@/components/dialog-connect-provider").then((x) => { + if (dialogDead || dialogRun !== run) return + void dialog.show(() => ) + }) + } + + function openServer() { + const run = ++dialogRun + void import("@/components/dialog-select-server").then((x) => { + if (dialogDead || dialogRun !== run) return + dialog.show(() => ) + }) + } + + function openSettings() { + const run = ++dialogRun + const module = settings.general.newLayoutDesigns() + ? import("@/components/settings-v2") + : import("@/components/dialog-settings") + void module.then((x) => { + if (dialogDead || dialogRun !== run) return + dialog.show(() => ) + }) + } + + function projectRoot(directory: string) { + const key = pathKey(directory) + const project = layout.projects + .list() + .find((item) => pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key)) + if (project) return project.worktree + + const known = Object.entries(store.workspaceOrder).find( + ([root, dirs]) => pathKey(root) === key || dirs.some((item) => pathKey(item) === key), + ) + if (known) return known[0] + + const [child] = serverSync().child(directory, { bootstrap: false }) + const id = child.project + if (!id) return directory + + const meta = serverSync().data.project.find((item) => item.id === id) + return meta?.worktree ?? directory + } + + function activeProjectRoot(directory: string) { + return currentProject()?.worktree ?? projectRoot(directory) + } + + function rememberSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) { + setStore("lastProjectSession", root, { directory, id, at: Date.now() }) + return root + } + + function clearLastProjectSession(root: string) { + if (!store.lastProjectSession[root]) return + setStore( + "lastProjectSession", + produce((draft) => { + delete draft[root] + }), + ) + } + + function syncSessionRoute(directory: string, id: string, root = activeProjectRoot(directory)) { + rememberSessionRoute(directory, id, root) + notification.session.markViewed(id) + const expanded = untrack(() => store.workspaceExpanded[directory]) + if (expanded === false) { + setStore("workspaceExpanded", directory, true) + } + requestAnimationFrame(() => scrollToSession(id, `${directory}:${id}`)) + return root + } + + async function navigateToProject(directory: string | undefined) { + if (!directory) return + const root = projectRoot(directory) + server.projects.touch(root) + const project = layout.projects.list().find((item) => item.worktree === root) + let dirs = project + ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root]) + : [root] + const canOpen = (value: string | undefined) => { + if (!value) return false + return dirs.some((item) => pathKey(item) === pathKey(value)) + } + const refreshDirs = async (target?: string) => { + if (!target || target === root || canOpen(target)) return canOpen(target) + const listed = await Promise.resolve( + project?.id ?? serverSDK().api.project.current({ location: { directory: root } }), + ) + .then((value) => (typeof value === "string" ? value : value.id)) + .then((projectID) => serverSDK().api.project.directories({ projectID, location: { directory: root } })) + .then((items) => items.map((item) => item.directory).filter((item) => pathKey(item) !== pathKey(root))) + .catch(() => [] as string[]) + dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root]) + return canOpen(target) + } + const openSession = async (target: { directory: string; id: string }) => { + if (!canOpen(target.directory)) return false + const sync = serverSync().ensureDirSyncContext(target.directory) + if (sync.session.get(target.id)) { + setStore("lastProjectSession", root, { directory: target.directory, id: target.id, at: Date.now() }) + navigateWithSidebarReset(`/${base64Encode(target.directory)}/session/${target.id}`) + return true + } + const resolved = await sync.session + .sync(target.id) + .then(() => sync.session.get(target.id)) + .catch(() => undefined) + if (!resolved?.directory) return false + if (!canOpen(resolved.directory)) return false + setStore("lastProjectSession", root, { directory: resolved.directory, id: resolved.id, at: Date.now() }) + navigateWithSidebarReset(`/${base64Encode(resolved.directory)}/session/${resolved.id}`) + return true + } + + const projectSession = store.lastProjectSession[root] + if (projectSession?.id) { + await refreshDirs(projectSession.directory) + const opened = await openSession(projectSession) + if (opened) return + clearLastProjectSession(root) + } + + const latest = latestRootSession( + dirs.map((item) => serverSync().child(item, { bootstrap: false })[0]), + Date.now(), + ) + if (latest && (await openSession(latest))) { + return + } + + const fetched = latestRootSession( + await Promise.all( + dirs.map(async (item) => ({ + path: { directory: item }, + session: await listAllSessions(serverSDK().api.session, { + directory: item, + parentID: null, + order: "desc", + }).catch(() => []), + })), + ), + Date.now(), + ) + if (fetched && (await openSession(fetched))) { + return + } + + navigateWithSidebarReset(`/${base64Encode(root)}/session`) + } + + function navigateToSession(session: Session | undefined) { + if (!session) return + navigateWithSidebarReset(`/${base64Encode(session.directory)}/session/${session.id}`) + } + + function openProject(directory: string, navigate = true) { + layout.projects.open(directory) + if (navigate) return navigateToProject(directory) + } + + const handleDeepLinks = (urls: string[]) => { + if (!server.isLocal()) return + + for (const directory of collectOpenProjectDeepLinks(urls)) { + void openProject(directory) + } + + for (const link of collectNewSessionDeepLinks(urls)) { + void openProject(link.directory, false) + const slug = base64Encode(link.directory) + if (link.prompt) { + setSessionHandoff(SessionStateKey.from(server.scope(), SessionRouteKey.fromLegacy(slug)), { + prompt: link.prompt, + }) + } + const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session` + navigateWithSidebarReset(href) + } + } + + onMount(() => { + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ urls: string[] }>).detail + const urls = detail?.urls ?? [] + if (urls.length === 0) return + handleDeepLinks(urls) + } + + handleDeepLinks(drainPendingDeepLinks(window)) + makeEventListener(window, deepLinkEvent, handler as EventListener) + }) + + async function renameProject(project: LocalProject, next: string) { + const current = displayName(project) + if (next === current) return + const name = next === getFilename(project.worktree) ? "" : next + + if (project.id && project.id !== "global") { + const sdk = serverSDK() + if ((await sdk.protocol) !== "v1") return + const result = await sdk.client.project + .update({ projectID: project.id, directory: project.worktree, name }) + .then((response) => response.data) + if (!result) return + // const result = await serverSDK().api.project.update({ projectID: project.id, name }) + serverSync().set("project", (items) => + items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), + ) + return + } + + serverSync().project.meta(project.worktree, { name }) + } + + const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { + const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) + if (current === next) return + setWorkspaceName(directory, next, projectId, branch) + } + + function closeProject(directory: string) { + const list = layout.projects.list() + const key = pathKey(directory) + const index = list.findIndex((x) => pathKey(x.worktree) === key) + const active = pathKey(currentProject()?.worktree ?? "") === key + if (index === -1) return + + if (!active) { + layout.projects.close(directory) + return + } + + if (list.length === 1) { + layout.projects.close(directory) + navigate("/") + return + } + + const next = list[index + 1] ?? list[index - 1] + + navigateWithSidebarReset(`/${base64Encode(next.worktree)}/session`) + layout.projects.close(directory) + queueMicrotask(() => { + void navigateToProject(next.worktree) + }) + } + + function toggleProjectWorkspaces(project: LocalProject) { + const enabled = layout.sidebar.workspaces(project.worktree)() + if (enabled) { + layout.sidebar.toggleWorkspaces(project.worktree) + return + } + if (project.vcs !== "git") return + layout.sidebar.toggleWorkspaces(project.worktree) + } + + const showEditProjectDialog = (conn: ServerConnection.Any, project: LocalProject) => { + const run = ++dialogRun + void import("@/components/dialog-edit-project").then((x) => { + if (dialogDead || dialogRun !== run) return + dialog.show(() => ) + }) + } + + function chooseProject() { + const conn = server.current + if (!conn) return + function resolve(result: string | string[] | null) { + if (Array.isArray(result)) { + for (const directory of result) { + void openProject(directory, false) + } + void navigateToProject(result[0]) + } else if (result) { + void openProject(result) + } + } + + pickDirectory({ + server: conn, + title: language.t("command.project.open"), + multiple: true, + onSelect: resolve, + }) + } + + const deleteWorkspace = async (root: string, directory: string, leaveDeletedWorkspace = false) => { + if (directory === root) return + + const current = currentDir() + const currentKey = pathKey(current) + const deletedKey = pathKey(directory) + const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey) + if (!leaveDeletedWorkspace && shouldLeave) { + navigateWithSidebarReset(`/${base64Encode(root)}/session`) + } + + setBusy(directory, true) + + const result = await serverSDK() + .client.worktree.remove({ directory: root, worktreeRemoveInput: { directory } }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("workspace.delete.failed.title"), + description: errorMessage(err, language.t("common.requestFailed")), + }) + return false + }) + + setBusy(directory, false) + + if (!result) return + + if (pathKey(store.lastProjectSession[root]?.directory ?? "") === pathKey(directory)) { + clearLastProjectSession(root) + } + + serverSync().set( + "project", + produce((draft) => { + const project = draft.find((item) => item.worktree === root) + if (!project) return + project.sandboxes = (project.sandboxes ?? []).filter((sandbox) => sandbox !== directory) + }), + ) + setStore("workspaceOrder", root, (order) => (order ?? []).filter((workspace) => workspace !== directory)) + + layout.projects.close(directory) + layout.projects.open(root) + + if (shouldLeave) return + + const nextCurrent = currentDir() + const nextKey = pathKey(nextCurrent) + const project = layout.projects.list().find((item) => item.worktree === root) + const dirs = project + ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root]) + : [root] + const valid = dirs.some((item) => pathKey(item) === nextKey) + + if (params.dir && projectRoot(nextCurrent) === root && !valid) { + navigateWithSidebarReset(`/${base64Encode(root)}/session`) + } + } + + const resetWorkspace = async (root: string, directory: string) => { + if (directory === root) return + setBusy(directory, true) + + const progress = showToast({ + persistent: true, + title: language.t("workspace.resetting.title"), + description: language.t("workspace.resetting.description"), + }) + const dismiss = () => dismissToast(progress) + + const sessions = await listAllSessions(serverSDK().api.session, { directory, order: "desc" }).catch(() => []) + + clearWorkspaceTerminals( + directory, + sessions.map((s) => s.id), + platform, + serverSDK().scope, + ) + await serverSDK() + .client.instance.dispose({ directory }) + .catch(() => undefined) + + const result = await serverSDK() + .client.worktree.reset({ directory: root, worktreeResetInput: { directory } }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("workspace.reset.failed.title"), + description: errorMessage(err, language.t("common.requestFailed")), + }) + return false + }) + + if (!result) { + setBusy(directory, false) + dismiss() + return + } + + if ((await serverSDK().protocol) === "v1") + await Promise.all( + sessions + .filter((session) => session.time.archived === undefined) + .map((session) => + serverSDK() + .client.session.update({ + sessionID: session.id, + directory: session.directory, + time: { archived: Date.now() }, + }) + .catch(() => undefined), + ), + ) + + setBusy(directory, false) + dismiss() + + showToast({ + title: language.t("workspace.reset.success.title"), + description: language.t("workspace.reset.success.description"), + actions: [ + { + label: language.t("command.session.new"), + onClick: () => { + const href = `/${base64Encode(directory)}/session` + navigate(href) + layout.mobileSidebar.hide() + }, + }, + { + label: language.t("common.dismiss"), + onClick: "dismiss", + }, + ], + }) + } + + function DialogDeleteWorkspace(props: { root: string; directory: string }) { + const name = createMemo(() => getFilename(props.directory)) + const [data, setData] = createStore({ + status: "loading" as "loading" | "ready" | "error", + dirty: false, + }) + + onMount(() => { + serverSDK() + .api.vcs.status({ location: { directory: props.directory } }) + .then((result) => { + const files = result.data + const dirty = files.length > 0 + setData({ status: "ready", dirty }) + }) + .catch(() => { + setData({ status: "error", dirty: false }) + }) + }) + + const handleDelete = () => { + const leaveDeletedWorkspace = !!params.dir && pathKey(currentDir()) === pathKey(props.directory) + if (leaveDeletedWorkspace) { + navigateWithSidebarReset(`/${base64Encode(props.root)}/session`) + } + dialog.close() + void deleteWorkspace(props.root, props.directory, leaveDeletedWorkspace) + } + + const description = () => { + if (data.status === "loading") return language.t("workspace.status.checking") + if (data.status === "error") return language.t("workspace.status.error") + if (!data.dirty) return language.t("workspace.status.clean") + return language.t("workspace.status.dirty") + } + + return ( + +
+
+ + {language.t("workspace.delete.confirm", { name: name() })} + + {description()} +
+
+ + +
+
+
+ ) + } + + function DialogResetWorkspace(props: { root: string; directory: string }) { + const name = createMemo(() => getFilename(props.directory)) + const [state, setState] = createStore({ + status: "loading" as "loading" | "ready" | "error", + dirty: false, + sessions: [] as Session[], + }) + + const refresh = async () => { + const sessions = await listAllSessions(serverSDK().api.session, { + directory: props.directory, + order: "desc", + }).catch(() => []) + const active = sessions.filter((session) => session.time.archived === undefined) + setState({ sessions: active }) + } + + onMount(() => { + serverSDK() + .api.vcs.status({ location: { directory: props.directory } }) + .then((result) => { + const files = result.data + const dirty = files.length > 0 + setState({ status: "ready", dirty }) + void refresh() + }) + .catch(() => { + setState({ status: "error", dirty: false }) + }) + }) + + const handleReset = () => { + dialog.close() + void resetWorkspace(props.root, props.directory) + } + + const archivedCount = () => state.sessions.length + + const description = () => { + if (state.status === "loading") return language.t("workspace.status.checking") + if (state.status === "error") return language.t("workspace.status.error") + if (!state.dirty) return language.t("workspace.status.clean") + return language.t("workspace.status.dirty") + } + + const archivedLabel = () => { + const count = archivedCount() + if (count === 0) return language.t("workspace.reset.archived.none") + if (count === 1) return language.t("workspace.reset.archived.one") + return language.t("workspace.reset.archived.many", { count }) + } + + return ( + +
+
+ + {language.t("workspace.reset.confirm", { name: name() })} + + + {description()} {archivedLabel()} {language.t("workspace.reset.note")} + +
+
+ + +
+
+
+ ) + } + + const activeRoute = { + session: "", + sessionProject: "", + directory: "", + } + + createEffect( + on( + () => { + return [pageReady(), route().slug, params.id, currentProject()?.worktree, currentDir()] as const + }, + ([ready, slug, id, root, dir]) => { + if (!ready || !slug || !dir) { + activeRoute.session = "" + activeRoute.sessionProject = "" + activeRoute.directory = "" + return + } + + if (!id) { + activeRoute.session = "" + activeRoute.sessionProject = "" + activeRoute.directory = "" + return + } + + const session = `${slug}/${id}` + + if (!root) { + activeRoute.session = session + activeRoute.directory = dir + activeRoute.sessionProject = "" + return + } + + if (server.projects.last() !== root) server.projects.touch(root) + + const changed = session !== activeRoute.session || dir !== activeRoute.directory + if (changed) { + activeRoute.session = session + activeRoute.directory = dir + activeRoute.sessionProject = syncSessionRoute(dir, id, root) + return + } + + if (root === activeRoute.sessionProject) return + activeRoute.directory = dir + activeRoute.sessionProject = rememberSessionRoute(dir, id, root) + }, + ), + ) + + createEffect(() => { + document.documentElement.style.setProperty( + "--dialog-left-margin", + `${layout.sidebar.opened() ? layout.sidebar.width() : 48}px`, + ) + }) + + const side = createMemo(() => Math.max(layout.sidebar.width(), 244)) + const panel = createMemo(() => Math.max(side() - 64, 0)) + + const loadedSessionDirs = new Set() + + createEffect( + on( + visibleSessionDirs, + (dirs) => { + if (dirs.length === 0) { + loadedSessionDirs.clear() + return + } + + const next = new Set(dirs) + for (const directory of next) { + if (loadedSessionDirs.has(directory)) continue + void serverSync().project.loadSessions(directory) + } + + loadedSessionDirs.clear() + for (const directory of next) { + loadedSessionDirs.add(directory) + } + }, + { defer: true }, + ), + ) + + function handleDragStart(event: unknown) { + const id = getDraggableId(event) + if (!id) return + setHoverProject(undefined) + setStore("activeProject", id) + } + + function handleDragOver(event: DragEvent) { + const { draggable, droppable } = event + if (draggable && droppable) { + const projects = layout.projects.list() + const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString()) + const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString()) + if (fromIndex !== toIndex && toIndex !== -1) { + layout.projects.move(draggable.id.toString(), toIndex) + } + } + } + + function handleDragEnd() { + setStore("activeProject", undefined) + } + + function workspaceIds(project: LocalProject | undefined) { + if (!project) return [] + const local = project.worktree + const dirs = [local, ...(project.sandboxes ?? [])] + const active = currentProject() + const directory = pathKey(active?.worktree ?? "") === pathKey(project.worktree) ? currentDir() : undefined + const extra = + directory && pathKey(directory) !== pathKey(local) && !dirs.some((item) => pathKey(item) === pathKey(directory)) + ? directory + : undefined + const pending = extra ? WorktreeState.get(serverSDK().scope, extra)?.status === "pending" : false + + const ordered = effectiveWorkspaceOrder(local, dirs, store.workspaceOrder[project.worktree]) + if (pending && extra) return [local, extra, ...ordered.filter((item) => item !== local)] + if (!extra) return ordered + if (pending) return ordered + return [...ordered, extra] + } + + const sidebarProject = createMemo(() => { + if (layout.sidebar.opened()) return currentProject() + const hovered = hoverProjectData() + if (hovered) return hovered + return currentProject() + }) + + function handleWorkspaceDragStart(event: unknown) { + const id = getDraggableId(event) + if (!id) return + setStore("activeWorkspace", id) + } + + function handleWorkspaceDragOver(event: DragEvent) { + const { draggable, droppable } = event + if (!draggable || !droppable) return + + const project = sidebarProject() + if (!project) return + + const ids = workspaceIds(project) + const fromIndex = ids.findIndex((dir) => dir === draggable.id.toString()) + const toIndex = ids.findIndex((dir) => dir === droppable.id.toString()) + if (fromIndex === -1 || toIndex === -1) return + if (fromIndex === toIndex) return + + const result = ids.slice() + const [item] = result.splice(fromIndex, 1) + if (!item) return + result.splice(toIndex, 0, item) + setStore( + "workspaceOrder", + project.worktree, + result.filter((directory) => pathKey(directory) !== pathKey(project.worktree)), + ) + } + + function handleWorkspaceDragEnd() { + setStore("activeWorkspace", undefined) + } + + const createWorkspace = async (project: LocalProject) => { + clearSidebarHoverState() + const created = await serverSDK() + .client.worktree.create({ directory: project.worktree }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("workspace.create.failed.title"), + description: errorMessage(err, language.t("common.requestFailed")), + }) + return undefined + }) + + if (!created?.directory) return + + setWorkspaceName(created.directory, created.branch ?? getFilename(created.directory), project.id, created.branch) + + const local = project.worktree + const key = pathKey(created.directory) + const root = pathKey(local) + + setBusy(created.directory, true) + WorktreeState.pending(serverSDK().scope, created.directory) + setStore("workspaceExpanded", key, true) + if (key !== created.directory) { + setStore("workspaceExpanded", created.directory, true) + } + setStore("workspaceOrder", project.worktree, (prev) => { + const existing = prev ?? [] + const next = existing.filter((item) => { + const id = pathKey(item) + return id !== root && id !== key + }) + return [created.directory, ...next] + }) + + serverSync().child(created.directory) + navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`) + } + + const workspaceSidebarCtx: WorkspaceSidebarContext = { + currentDir, + navList: currentSessions, + sidebarExpanded, + sidebarHovering, + clearHoverProjectSoon, + prefetchSession, + archiveSession, + workspaceName, + renameWorkspace, + editorOpen, + openEditor, + closeEditor, + setEditor, + InlineEditor, + isBusy, + workspaceExpanded: (directory, local) => store.workspaceExpanded[directory] ?? local, + setWorkspaceExpanded: (directory, value) => setStore("workspaceExpanded", directory, value), + showResetWorkspaceDialog: (root, directory) => + dialog.show(() => ), + showDeleteWorkspaceDialog: (root, directory) => + dialog.show(() => ), + setScrollContainerRef: (el, mobile) => { + if (!mobile) scrollContainerRef = el + }, + } + + const projectSidebarCtx: ProjectSidebarContext = { + currentDir, + currentProject, + sidebarOpened: () => layout.sidebar.opened(), + sidebarHovering, + hoverProject: () => state.hoverProject, + onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event), + onProjectMouseLeave: (worktree) => aim.leave(worktree), + onProjectFocus: (worktree) => aim.activate(worktree), + onHoverOpenChanged: (worktree, hoverOpen) => { + if (!hoverOpen && state.hoverProject && state.hoverProject !== worktree) return + setState("hoverProject", hoverOpen ? worktree : undefined) + }, + navigateToProject, + openSidebar: () => layout.sidebar.open(), + closeProject, + showEditProjectDialog: (proj) => showEditProjectDialog(server.current!, proj), + toggleProjectWorkspaces, + workspacesEnabled: (project) => project.vcs === "git" && layout.sidebar.workspaces(project.worktree)(), + workspaceIds, + workspaceLabel, + sessionProps: { + navList: currentSessions, + sidebarExpanded, + clearHoverProjectSoon, + prefetchSession, + archiveSession, + }, + } + + const SidebarPanel = (panelProps: { + project: Accessor + mobile?: boolean + merged?: boolean + }) => { + const project = panelProps.project + const merged = createMemo(() => panelProps.mobile || (panelProps.merged ?? layout.sidebar.opened())) + const hover = createMemo(() => !panelProps.mobile && panelProps.merged === false && !layout.sidebar.opened()) + const empty = createMemo(() => !params.dir && layout.projects.list().length === 0) + const projectName = createMemo(() => { + const item = project() + if (!item) return "" + return item.name || getFilename(item.worktree) + }) + const projectId = createMemo(() => project()?.id ?? "") + const worktree = createMemo(() => project()?.worktree ?? "") + const slug = createMemo(() => { + const dir = worktree() + if (!dir) return "" + return base64Encode(dir) + }) + const workspaces = createMemo(() => { + const item = project() + if (!item) return [] as string[] + return workspaceIds(item) + }) + const unseenCount = createMemo(() => + workspaces().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + const clearNotifications = () => + workspaces() + .filter((directory) => notification.project.unseenCount(directory) > 0) + .forEach((directory) => notification.project.markViewed(directory)) + const workspacesEnabled = createMemo(() => { + const item = project() + if (!item) return false + if (item.vcs !== "git") return false + return layout.sidebar.workspaces(item.worktree)() + }) + const canToggle = createMemo(() => { + const item = project() + if (!item) return false + return item.vcs === "git" || layout.sidebar.workspaces(item.worktree)() + }) + const homedir = createMemo(() => serverSync().data.path.home) + + return ( +
+ +
+
+
+
{language.t("sidebar.empty.title")}
+
+ {language.t("sidebar.empty.description")} +
+
+ +
+
+
+ } + keyed + > + {(project) => ( + <> +
+
+
+ { + void renameProject(project, next) + }} + class="text-14-medium text-text-strong truncate" + displayClass="text-14-medium text-text-strong truncate" + stopPropagation + /> + + + + {worktree().replace(homedir(), "~")} + + +
+ + + + + + { + showEditProjectDialog(server.current!, project) + }} + > + {language.t("common.edit")} + + { + toggleProjectWorkspaces(project) + }} + > + + {workspacesEnabled() + ? language.t("sidebar.workspaces.disable") + : language.t("sidebar.workspaces.enable")} + + + + + {language.t("sidebar.project.clearNotifications")} + + + + { + const dir = worktree() + if (!dir) return + closeProject(dir) + }} + > + {language.t("common.close")} + + + + +
+
+ +
+ +
+ +
+
+ +
+ + } + > + <> +
+ +
+
+ + + +
{ + if (!panelProps.mobile) scrollContainerRef = el + }} + class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]" + > + + + {(directory) => ( + + )} + + +
+ + store.activeWorkspace} + workspaceLabel={workspaceLabel} + /> + +
+
+ +
+
+ + )} + + +
0 && providers.paid().length === 0), + }} + > +
+
+
+
{language.t("sidebar.gettingStarted.title")}
+
+ {language.t("sidebar.gettingStarted.line1")} +
+
+ {language.t("sidebar.gettingStarted.line2")} +
+
+
+ + +
+
+
+
+
+ ) + } + + const projects = () => layout.projects.list() + const projectOverlay = () => store.activeProject} /> + const sidebarContent = (mobile?: boolean) => ( + layout.sidebar.opened()} + aimMove={aim.move} + projects={projects} + renderProject={(project) => ( + + )} + handleDragStart={handleDragStart} + handleDragEnd={handleDragEnd} + handleDragOver={handleDragOver} + openProjectLabel={language.t("command.project.open")} + openProjectKeybind={() => command.keybind("project.open")} + onOpenProject={chooseProject} + renderProjectOverlay={projectOverlay} + settingsLabel={() => language.t("sidebar.settings")} + settingsKeybind={() => command.keybind("settings.open")} + onOpenSettings={openSettings} + helpLabel={() => language.t("sidebar.help")} + onOpenHelp={() => platform.openExternal("https://opencode.ai/desktop-feedback")} + renderPanel={() => + mobile ? : + } + /> + ) + + return ( +
+ {autoselecting() ?? ""} + setState("debugTools", (value) => !value) } + : undefined + } + /> + + + +
+
+
+ + + + + + + + + +
+ ) +} + +function UpdateAvailableToast(props: { + version: string + install: () => void + language: ReturnType +}) { + let toastId: number | undefined + + onMount(() => { + toastId = showToast({ + persistent: true, + icon: "download", + title: props.language.t("toast.update.title"), + description: props.language.t("toast.update.description", { version: props.version }), + actions: [ + { + label: props.language.t("toast.update.action.installRestart"), + onClick: props.install, + }, + { + label: props.language.t("toast.update.action.notYet"), + onClick: "dismiss", + }, + ], + }) + }) + + onCleanup(() => { + if (toastId === undefined) return + dismissToast(toastId) + }) + + return null +} diff --git a/packages/app/src/pages/layout/deep-links.ts b/packages/app/src/pages/layout/deep-links.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dca421f74984f1a56728482bf0d9e375ed0dfc4 --- /dev/null +++ b/packages/app/src/pages/layout/deep-links.ts @@ -0,0 +1,50 @@ +export const deepLinkEvent = "opencode:deep-link" + +const parseUrl = (input: string) => { + if (!input.startsWith("opencode://")) return + if (typeof URL.canParse === "function" && !URL.canParse(input)) return + try { + return new URL(input) + } catch { + return + } +} + +export const parseDeepLink = (input: string) => { + const url = parseUrl(input) + if (!url) return + if (url.hostname !== "open-project") return + const directory = url.searchParams.get("directory") + if (!directory) return + return directory +} + +export const parseNewSessionDeepLink = (input: string) => { + const url = parseUrl(input) + if (!url) return + if (url.hostname !== "new-session") return + const directory = url.searchParams.get("directory") + if (!directory) return + const prompt = url.searchParams.get("prompt") || undefined + if (!prompt) return { directory } + return { directory, prompt } +} + +export const collectOpenProjectDeepLinks = (urls: string[]) => + urls.map(parseDeepLink).filter((directory): directory is string => !!directory) + +export const collectNewSessionDeepLinks = (urls: string[]) => + urls.map(parseNewSessionDeepLink).filter((link): link is { directory: string; prompt?: string } => !!link) + +type OpenCodeWindow = Window & { + __OPENCODE__?: { + deepLinks?: string[] + } +} + +export const drainPendingDeepLinks = (target: OpenCodeWindow) => { + const pending = target.__OPENCODE__?.deepLinks ?? [] + if (pending.length === 0) return [] + if (target.__OPENCODE__) target.__OPENCODE__.deepLinks = [] + return pending +} diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb58b6f8b6d60db3ec718f011d4a25873968f8c4 --- /dev/null +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -0,0 +1,347 @@ +import { describe, expect, test } from "bun:test" +import { + collectNewSessionDeepLinks, + collectOpenProjectDeepLinks, + drainPendingDeepLinks, + parseDeepLink, + parseNewSessionDeepLink, +} from "./deep-links" +import { type Session } from "@opencode-ai/sdk/v2/client" +import { + childSessionOnPath, + closeHomeProject, + compareSessionTime, + displayName, + effectiveWorkspaceOrder, + errorMessage, + hasProjectPermissions, + homeProjectNavigation, + homeProjectDirectories, + homeSessionServerStatus, + latestRootSession, + sortedRootSessions, + toggleHomeProjectSelection, +} from "./helpers" +import { pathKey } from "@/utils/path-key" +import { ServerConnection } from "@/context/server" + +const serverKey = ServerConnection.Key.make + +const session = (input: Partial & Pick) => + ({ + title: "", + version: "v2", + parentID: undefined, + messageCount: 0, + permissions: { session: {}, share: {} }, + time: { created: 0, updated: 0, archived: undefined }, + ...input, + }) as Session + +describe("layout deep links", () => { + test("parses open-project deep links", () => { + expect(parseDeepLink("opencode://open-project?directory=/tmp/demo")).toBe("/tmp/demo") + }) + + test("ignores non-project deep links", () => { + expect(parseDeepLink("opencode://other?directory=/tmp/demo")).toBeUndefined() + expect(parseDeepLink("https://example.com")).toBeUndefined() + }) + + test("ignores malformed deep links safely", () => { + expect(() => parseDeepLink("opencode://open-project/%E0%A4%A%")).not.toThrow() + expect(parseDeepLink("opencode://open-project/%E0%A4%A%")).toBeUndefined() + }) + + test("parses links when URL.canParse is unavailable", () => { + const original = Object.getOwnPropertyDescriptor(URL, "canParse") + Object.defineProperty(URL, "canParse", { configurable: true, value: undefined }) + try { + expect(parseDeepLink("opencode://open-project?directory=/tmp/demo")).toBe("/tmp/demo") + } finally { + if (original) Object.defineProperty(URL, "canParse", original) + if (!original) Reflect.deleteProperty(URL, "canParse") + } + }) + + test("ignores open-project deep links without directory", () => { + expect(parseDeepLink("opencode://open-project")).toBeUndefined() + expect(parseDeepLink("opencode://open-project?directory=")).toBeUndefined() + }) + + test("collects only valid open-project directories", () => { + const result = collectOpenProjectDeepLinks([ + "opencode://open-project?directory=/a", + "opencode://other?directory=/b", + "opencode://open-project?directory=/c", + ]) + expect(result).toEqual(["/a", "/c"]) + }) + + test("parses new-session deep links with optional prompt", () => { + expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo")).toEqual({ directory: "/tmp/demo" }) + expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo&prompt=hello%20world")).toEqual({ + directory: "/tmp/demo", + prompt: "hello world", + }) + }) + + test("ignores new-session deep links without directory", () => { + expect(parseNewSessionDeepLink("opencode://new-session")).toBeUndefined() + expect(parseNewSessionDeepLink("opencode://new-session?directory=")).toBeUndefined() + }) + + test("collects only valid new-session deep links", () => { + const result = collectNewSessionDeepLinks([ + "opencode://new-session?directory=/a", + "opencode://open-project?directory=/b", + "opencode://new-session?directory=/c&prompt=ship%20it", + ]) + expect(result).toEqual([{ directory: "/a" }, { directory: "/c", prompt: "ship it" }]) + }) + + test("drains global deep links once", () => { + const target = { + __OPENCODE__: { + deepLinks: ["opencode://open-project?directory=/a"], + }, + } as unknown as Window & { __OPENCODE__?: { deepLinks?: string[] } } + + expect(drainPendingDeepLinks(target)).toEqual(["opencode://open-project?directory=/a"]) + expect(drainPendingDeepLinks(target)).toEqual([]) + }) +}) + +describe("layout workspace helpers", () => { + test("normalizes trailing slash in workspace key", () => { + expect(String(pathKey("/tmp/demo///"))).toBe("/tmp/demo") + expect(String(pathKey("C:\\tmp\\demo\\\\"))).toBe("C:/tmp/demo") + }) + + test("preserves posix and drive roots in workspace key", () => { + expect(String(pathKey("/"))).toBe("/") + expect(String(pathKey("///"))).toBe("/") + expect(String(pathKey("C:\\"))).toBe("C:/") + expect(String(pathKey("C://"))).toBe("C:/") + expect(String(pathKey("C:///"))).toBe("C:/") + }) + + test("keeps local first while preserving known order", () => { + const result = effectiveWorkspaceOrder("/root", ["/root", "/b", "/c"], ["/root", "/c", "/a", "/b"]) + expect(result).toEqual(["/root", "/c", "/b"]) + }) + + test("finds the latest root session across workspaces", () => { + const result = latestRootSession( + [ + { + path: { directory: "/root" }, + session: [session({ id: "root", directory: "/root", time: { created: 1, updated: 1, archived: undefined } })], + }, + { + path: { directory: "/workspace" }, + session: [ + session({ + id: "workspace", + directory: "/workspace", + time: { created: 2, updated: 2, archived: undefined }, + }), + ], + }, + ], + 120_000, + ) + + expect(result?.id).toBe("workspace") + }) + + test("sorts recent sessions by persisted update time instead of id", () => { + const result = sortedRootSessions( + { + path: { directory: "/workspace" }, + session: [ + session({ id: "ses_z", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }), + session({ id: "ses_a", directory: "/workspace", time: { created: 1, updated: 3, archived: undefined } }), + ], + }, + 3, + ) + + expect(result.map((item) => item.id)).toEqual(["ses_a", "ses_z"]) + }) + + test("uses id only to break equal session timestamps", () => { + const sessions = [ + session({ id: "ses_z", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }), + session({ id: "ses_a", directory: "/workspace", time: { created: 1, updated: 2, archived: undefined } }), + ] + + expect(sessions.sort(compareSessionTime).map((item) => item.id)).toEqual(["ses_a", "ses_z"]) + }) + + test("detects project permissions with a filter", () => { + const result = hasProjectPermissions( + { + root: [{ id: "perm-root" }, { id: "perm-hidden" }], + child: [{ id: "perm-child" }], + }, + (item) => item.id === "perm-child", + ) + + expect(result).toBe(true) + }) + + test("ignores project permissions filtered out", () => { + const result = hasProjectPermissions( + { + root: [{ id: "perm-root" }], + }, + () => false, + ) + + expect(result).toBe(false) + }) + + test("ignores archived and child sessions when finding latest root session", () => { + const result = latestRootSession( + [ + { + path: { directory: "/workspace" }, + session: [ + session({ + id: "archived", + directory: "/workspace", + time: { created: 10, updated: 10, archived: 10 }, + }), + session({ + id: "child", + directory: "/workspace", + parentID: "parent", + time: { created: 20, updated: 20, archived: undefined }, + }), + session({ + id: "root", + directory: "/workspace", + time: { created: 30, updated: 30, archived: undefined }, + }), + ], + }, + ], + 120_000, + ) + + expect(result?.id).toBe("root") + }) + + test("finds the direct child on the active session path", () => { + const list = [ + session({ id: "root", directory: "/workspace" }), + session({ id: "child", directory: "/workspace", parentID: "root" }), + session({ id: "leaf", directory: "/workspace", parentID: "child" }), + ] + + expect(childSessionOnPath(list, "root", "leaf")?.id).toBe("child") + expect(childSessionOnPath(list, "child", "leaf")?.id).toBe("leaf") + expect(childSessionOnPath(list, "root", "root")).toBeUndefined() + expect(childSessionOnPath(list, "root", "other")).toBeUndefined() + }) + + test("formats fallback project display name", () => { + expect(displayName({ worktree: "/tmp/app" })).toBe("app") + expect(displayName({ worktree: "/tmp/app", name: "My App" })).toBe("My App") + expect(displayName({ worktree: "/" })).toBe("/") + }) + + test("scopes home project selection by server", () => { + expect( + toggleHomeProjectSelection(undefined, serverKey("https://debian.example"), "/home/luke/repos/amazon"), + ).toEqual({ + server: serverKey("https://debian.example"), + directory: "/home/luke/repos/amazon", + }) + expect( + toggleHomeProjectSelection( + { server: serverKey("https://windows.example"), directory: "/home/luke/repos/amazon" }, + serverKey("https://debian.example"), + "/home/luke/repos/amazon", + ), + ).toEqual({ server: serverKey("https://debian.example"), directory: "/home/luke/repos/amazon" }) + expect( + toggleHomeProjectSelection( + { server: serverKey("https://debian.example"), directory: "/home/luke/repos/amazon" }, + serverKey("https://debian.example"), + "/home/luke/repos/amazon", + ), + ).toEqual({ server: serverKey("https://debian.example") }) + }) + + test("closes a home project through its server context", () => { + const closed: string[] = [] + + expect( + closeHomeProject( + { server: serverKey("https://windows.example"), directory: "/shared" }, + serverKey("https://debian.example"), + { close: (directory) => closed.push(directory) }, + "/shared", + ), + ).toEqual({ server: serverKey("https://windows.example"), directory: "/shared" }) + expect(closed).toEqual(["/shared"]) + expect( + closeHomeProject( + { server: serverKey("https://debian.example"), directory: "/shared" }, + serverKey("https://debian.example"), + { close: (directory) => closed.push(directory) }, + "/shared", + ), + ).toEqual({ server: serverKey("https://debian.example") }) + }) + + test("defers home project navigation until its server is active", () => { + expect( + homeProjectNavigation(serverKey("sidecar"), serverKey("https://debian.example"), "/YW1hem9u/session"), + ).toEqual({ + server: serverKey("https://debian.example"), + href: "/YW1hem9u/session", + }) + expect( + homeProjectNavigation( + serverKey("https://debian.example"), + serverKey("https://debian.example"), + "/YW1hem9u/session", + ), + ).toEqual({ + href: "/YW1hem9u/session", + }) + }) + + test("preserves picker order when adding multiple projects", () => { + expect(homeProjectDirectories(["/first", "/second"])).toEqual(["/first", "/second"]) + expect(homeProjectDirectories("/only")).toEqual(["/only"]) + expect(homeProjectDirectories(null)).toEqual([]) + }) + + test("hides status derived from an inactive server", () => { + let reads = 0 + const status = () => { + reads++ + return { working: true, tint: "red" } + } + expect(homeSessionServerStatus(false, status)).toEqual({ + working: false, + tint: undefined, + }) + expect(reads).toBe(0) + expect(homeSessionServerStatus(true, status)).toEqual({ + working: true, + tint: "red", + }) + expect(reads).toBe(1) + }) + + test("extracts api error message and fallback", () => { + expect(errorMessage({ data: { message: "boom" } }, "fallback")).toBe("boom") + expect(errorMessage(new Error("broken"), "fallback")).toBe("broken") + expect(errorMessage("unknown", "fallback")).toBe("fallback") + }) +}) diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..5245ba6f16daa913634096078e3d45179b3e4ae5 --- /dev/null +++ b/packages/app/src/pages/layout/helpers.ts @@ -0,0 +1,142 @@ +import { getFilename } from "@opencode-ai/core/util/path" +import { type Session } from "@opencode-ai/sdk/v2/client" +import { pathKey } from "@/utils/path-key" +import type { ServerConnection } from "@/context/server" +import type { HomeProjectSelection } from "@/context/layout" + +type SessionStore = { + session?: Session[] + path: { directory: string } +} + +export function compareSessionTime(a: Session, b: Session) { + const updated = (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created) + if (updated !== 0) return updated + return a.id < b.id ? -1 : a.id > b.id ? 1 : 0 +} + +const isRootVisibleSession = (session: Session, directory: string) => + pathKey(session.directory) === pathKey(directory) && !session.parentID && !session.time?.archived + +export const roots = (store: SessionStore) => + (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory)) + +export const sortedRootSessions = (store: SessionStore, _now: number) => roots(store).sort(compareSessionTime) + +export const latestRootSession = (stores: SessionStore[], _now: number) => + stores.flatMap(roots).sort(compareSessionTime)[0] + +export function hasProjectPermissions( + request: Record | undefined, + include: (item: T) => boolean = () => true, +) { + return Object.values(request ?? {}).some((list) => list?.some(include)) +} + +export const childSessionOnPath = (sessions: Session[] | undefined, rootID: string, activeID?: string) => { + if (!activeID || activeID === rootID) return + const map = new Map((sessions ?? []).map((session) => [session.id, session])) + let id = activeID + + while (id) { + const session = map.get(id) + if (!session?.parentID) return + if (session.parentID === rootID) return session + id = session.parentID + } +} + +export const displayName = (project: { name?: string; worktree: string }) => + project.name || getFilename(project.worktree) || project.worktree + +export function toggleHomeProjectSelection( + current: HomeProjectSelection | undefined, + server: ServerConnection.Key, + directory: string, +): HomeProjectSelection { + if (current?.server === server && current.directory === directory) return { server } + return { server, directory } +} + +export function closeHomeProject( + selected: HomeProjectSelection | undefined, + server: ServerConnection.Key, + projects: { close: (directory: string) => void }, + directory: string, +) { + projects.close(directory) + if (selected?.server === server && selected.directory === directory) return { server } + return selected +} + +export function homeProjectNavigation(active: ServerConnection.Key, server: ServerConnection.Key, href: string) { + if (active === server) return { href } + return { server, href } +} + +export function homeProjectDirectories(result: string | string[] | null) { + if (!result) return [] + return Array.isArray(result) ? result : [result] +} + +export function homeSessionServerStatus(active: boolean, status: () => { working: boolean; tint?: string }) { + if (!active) return { working: false, tint: undefined } + return status() +} + +const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750" + +export function getProjectAvatarSource(id?: string, icon?: { color?: string; url?: string; override?: string }) { + if (id === OPENCODE_PROJECT_ID) return "https://opencode.ai/favicon.svg" + if (icon?.override) return icon.override + if (icon?.color) return undefined + return icon?.url +} + +export function projectForSession( + session: Session, + projects: T[], + byID: Map = new Map(projects.flatMap((project) => (project.id ? [[project.id, project] as const] : []))), +) { + const direct = byID.get(session.projectID) + if (direct) return direct + const directory = pathKey(session.directory) + return projects.find( + (project) => + pathKey(project.worktree) === directory || project.sandboxes?.some((sandbox) => pathKey(sandbox) === directory), + ) +} + +export const errorMessage = (err: unknown, fallback: string) => { + if (err && typeof err === "object" && "data" in err) { + const data = (err as { data?: { message?: string } }).data + if (data?.message) return data.message + } + if (err instanceof Error) return err.message + return fallback +} + +export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => { + const root = pathKey(local) + const live = new Map() + + for (const dir of dirs) { + const key = pathKey(dir) + if (key === root) continue + if (!live.has(key)) live.set(key, dir) + } + + if (!persisted?.length) return [local, ...live.values()] + + const result = [local] + for (const dir of persisted) { + const key = pathKey(dir) + if (key === root) continue + const match = live.get(key) + if (!match) continue + result.push(match) + live.delete(key) + } + + return [...result, ...live.values()] +} diff --git a/packages/app/src/pages/layout/inline-editor.tsx b/packages/app/src/pages/layout/inline-editor.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4189e4a72a08a69031e535010e58cfaa39df8374 --- /dev/null +++ b/packages/app/src/pages/layout/inline-editor.tsx @@ -0,0 +1,126 @@ +import { createStore } from "solid-js/store" +import { onCleanup, Show, type Accessor } from "solid-js" +import { InlineInput } from "@opencode-ai/ui/inline-input" + +export function createInlineEditorController() { + // This controller intentionally supports one active inline editor at a time. + const [editor, setEditor] = createStore({ + active: "" as string, + value: "", + }) + + const editorOpen = (id: string) => editor.active === id + const editorValue = () => editor.value + const openEditor = (id: string, value: string) => { + if (!id) return + setEditor({ active: id, value }) + } + const closeEditor = () => setEditor({ active: "", value: "" }) + + const saveEditor = (callback: (next: string) => void) => { + const next = editor.value.trim() + if (!next) { + closeEditor() + return + } + closeEditor() + callback(next) + } + + const editorKeyDown = (event: KeyboardEvent, callback: (next: string) => void) => { + if (event.key === "Enter") { + event.preventDefault() + saveEditor(callback) + return + } + if (event.key !== "Escape") return + event.preventDefault() + closeEditor() + } + + const InlineEditor = (props: { + id: string + value: Accessor + onSave: (next: string) => void + class?: string + displayClass?: string + editing?: boolean + stopPropagation?: boolean + openOnDblClick?: boolean + }) => { + let frame: number | undefined + + onCleanup(() => { + if (frame === undefined) return + cancelAnimationFrame(frame) + }) + + const isEditing = () => props.editing ?? editorOpen(props.id) + const stopEvents = () => props.stopPropagation ?? false + const allowDblClick = () => props.openOnDblClick ?? true + const stopPropagation = (event: Event) => { + if (!stopEvents()) return + event.stopPropagation() + } + const handleDblClick = (event: MouseEvent) => { + if (!allowDblClick()) return + stopPropagation(event) + openEditor(props.id, props.value()) + } + + return ( + + {props.value()} + + } + > + { + if (frame !== undefined) cancelAnimationFrame(frame) + frame = requestAnimationFrame(() => { + frame = undefined + if (!el.isConnected) return + el.focus() + }) + }} + value={editorValue()} + class={props.class} + onInput={(event) => setEditor("value", event.currentTarget.value)} + onKeyDown={(event) => { + event.stopPropagation() + editorKeyDown(event, props.onSave) + }} + onBlur={closeEditor} + onPointerDown={stopPropagation} + onClick={stopPropagation} + onDblClick={stopPropagation} + onMouseDown={stopPropagation} + onMouseUp={stopPropagation} + onTouchStart={stopPropagation} + /> + + ) + } + + return { + editor, + editorOpen, + editorValue, + openEditor, + closeEditor, + saveEditor, + editorKeyDown, + setEditor, + InlineEditor, + } +} diff --git a/packages/app/src/pages/layout/project-avatar-state.ts b/packages/app/src/pages/layout/project-avatar-state.ts new file mode 100644 index 0000000000000000000000000000000000000000..236f6bd40531b887a87d219338f02a6c7e6b7bc4 --- /dev/null +++ b/packages/app/src/pages/layout/project-avatar-state.ts @@ -0,0 +1,49 @@ +import { createMemo, type Accessor } from "solid-js" +import { useGlobal } from "@/context/global" +import { useNotification } from "@/context/notification" +import { usePermission } from "@/context/permission" +import { sessionPermissionRequest, sessionQuestionRequest } from "@/pages/session/composer/session-request-tree" +import { ServerConnection } from "@/context/server" + +export function useSessionTabAvatarState( + server: Accessor, + directory: Accessor, + sessionId: Accessor, +) { + const global = useGlobal() + const notification = useNotification() + const permission = usePermission() + const connection = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === server())) + const sync = createMemo(() => { + const conn = connection() + if (conn) return global.ensureServerCtx(conn).sync + }) + const hasPermissions = createMemo(() => { + const serverSync = sync() + if (!serverSync) return false + const permissionState = permission.ensureServerState(server()) + const [store] = serverSync.child(directory(), { bootstrap: false }) + return !!sessionPermissionRequest(store.session, serverSync.session.data.permission, sessionId(), (item) => { + return !permissionState.autoResponds(item, directory()) + }) + }) + const hasQuestions = createMemo(() => { + const serverSync = sync() + if (!serverSync) return false + const [store] = serverSync.child(directory(), { bootstrap: false }) + return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId()) + }) + const needsAttention = createMemo(() => hasPermissions() || hasQuestions()) + const notificationState = createMemo(() => { + if (!connection()) return + return notification.ensureServerState(server()) + }) + const unread = createMemo(() => needsAttention() || (notificationState()?.session.unseenCount(sessionId()) ?? 0) > 0) + const loading = createMemo(() => { + const serverSync = sync() + if (!serverSync) return false + if (needsAttention()) return false + return serverSync.session.data.session_working(sessionId()) + }) + return { unread, loading } +} diff --git a/packages/app/src/pages/layout/session-tab-avatar.tsx b/packages/app/src/pages/layout/session-tab-avatar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..090217364337b522300b60923d3a0674e39a36d8 --- /dev/null +++ b/packages/app/src/pages/layout/session-tab-avatar.tsx @@ -0,0 +1,60 @@ +import type { LocalProject } from "@/context/layout" +import { getProjectAvatarVariant } from "@/context/layout" +import type { ServerConnection } from "@/context/server" +import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" +import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state" +import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" +import { SessionProgressIndicatorV2 } from "@opencode-ai/session-ui/v2/session-progress-indicator-v2" +import { Show } from "solid-js" + +export function SessionTabAvatar(props: { + project?: LocalProject + directory: string + sessionId: string + server: ServerConnection.Key + revealProjectOnHover?: boolean +}) { + const state = useSessionTabAvatarState( + () => props.server, + () => props.directory, + () => props.sessionId, + ) + return ( + + ) +} + +export function SessionTabAvatarView(props: { + project?: LocalProject + directory: string + revealProjectOnHover?: boolean + unread: boolean + loading: boolean +}) { + const projectAvatar = () => ( + + ) + return ( + + + + + + + + + ) +} diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx new file mode 100644 index 0000000000000000000000000000000000000000..776b81ae5a838f5b006bfe62864dc8659da7dc0d --- /dev/null +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -0,0 +1,335 @@ +import type { Session } from "@opencode-ai/sdk/v2/client" +import { Avatar } from "@opencode-ai/ui/avatar" +import { Icon } from "@opencode-ai/ui/icon" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Spinner } from "@opencode-ai/ui/spinner" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { getFilename } from "@opencode-ai/core/util/path" +import { A, useParams } from "@solidjs/router" +import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js" +import { useServerSync } from "@/context/server-sync" +import { useLanguage } from "@/context/language" +import { getAvatarColors, type LocalProject, useLayout } from "@/context/layout" +import { useNotification } from "@/context/notification" +import { usePermission } from "@/context/permission" +import { messageAgentColor } from "@/utils/agent" +import { sessionTitle } from "@/utils/session-title" +import { sessionPermissionRequest } from "../session/composer/session-request-tree" +import { childSessionOnPath, getProjectAvatarSource, hasProjectPermissions } from "./helpers" + +export const ProjectIcon = (props: { + project: LocalProject + class?: string + notify?: boolean + working?: boolean +}): JSX.Element => { + const serverSync = useServerSync() + const notification = useNotification() + const permission = usePermission() + const dirs = createMemo(() => [props.project.worktree, ...(props.project.sandboxes ?? [])]) + const unseenCount = createMemo(() => + dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + const hasError = createMemo(() => dirs().some((directory) => notification.project.unseenHasError(directory))) + const hasPermissions = createMemo(() => + dirs().some((directory) => { + return hasProjectPermissions(serverSync().session.data.permission, (item) => { + if (serverSync().session.get(item.sessionID)?.directory !== directory) return false + return !permission.autoResponds(item, directory) + }) + }), + ) + const notify = createMemo(() => props.notify && (hasPermissions() || unseenCount() > 0)) + const name = createMemo(() => props.project.name || getFilename(props.project.worktree)) + + return ( +
+
+ +
+ +
+ + +
+ +
+
+
+ ) +} + +export type SessionItemProps = { + session: Session + list: Session[] + navList?: Accessor + slug: string + mobile?: boolean + dense?: boolean + showTooltip?: boolean + showChild?: boolean + level?: number + sidebarExpanded: Accessor + clearHoverProjectSoon: () => void + prefetchSession: (session: Session, priority?: "high" | "low") => void + archiveSession: (session: Session) => Promise +} + +const SessionRow = (props: { + session: Session + slug: string + mobile?: boolean + dense?: boolean + tint: Accessor + isWorking: Accessor + hasPermissions: Accessor + hasError: Accessor + unseenCount: Accessor + clearHoverProjectSoon: () => void + sidebarOpened: Accessor + warmPress: () => void + warmFocus: () => void +}): JSX.Element => { + const title = () => sessionTitle(props.session.title) + + return ( + { + if (props.sidebarOpened()) return + props.clearHoverProjectSoon() + }} + > + 0}> +
+ + + + + +
+ + +
+ + 0}> +
+ + +
+ + {title()} +
+ ) +} + +export const SessionItem = (props: SessionItemProps): JSX.Element => { + const params = useParams() + const layout = useLayout() + const language = useLanguage() + const notification = useNotification() + const permission = usePermission() + const serverSync = useServerSync() + const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id)) + const hasError = createMemo(() => notification.session.unseenHasError(props.session.id)) + const [sessionStore] = serverSync().child(props.session.directory) + const hasPermissions = createMemo(() => { + return !!sessionPermissionRequest( + sessionStore.session, + serverSync().session.data.permission, + props.session.id, + (item) => { + return !permission.autoResponds(item, props.session.directory) + }, + ) + }) + const isWorking = createMemo(() => { + if (hasPermissions()) return false + return serverSync().session.data.session_working(props.session.id) + }) + + const tint = createMemo(() => + messageAgentColor(serverSync().session.data.message[props.session.id], sessionStore.agent), + ) + const tooltip = createMemo(() => props.showTooltip ?? (props.mobile || !props.sidebarExpanded())) + const currentChild = createMemo(() => { + if (!props.showChild) return + return childSessionOnPath(sessionStore.session, props.session.id, params.id) + }) + + const warm = (span: number, priority: "high" | "low") => { + const nav = props.navList?.() + const list = nav?.some((item) => item.id === props.session.id && item.directory === props.session.directory) + ? nav + : props.list + + props.prefetchSession(props.session, priority) + + const idx = list.findIndex((item) => item.id === props.session.id && item.directory === props.session.directory) + if (idx === -1) return + + for (let step = 1; step <= span; step++) { + const next = list[idx + step] + if (next) props.prefetchSession(next, step === 1 ? "high" : priority) + + const prev = list[idx - step] + if (prev) props.prefetchSession(prev, step === 1 ? "high" : priority) + } + } + + const item = ( + warm(2, "high")} + warmFocus={() => warm(2, "high")} + /> + ) + + return ( + <> +
+
+
+ + {item} + + } + > + {item} + +
+ + +
+ + { + event.preventDefault() + event.stopPropagation() + void props.archiveSession(props.session) + }} + /> + +
+
+
+
+ + {(child) => ( +
+ +
+ )} +
+ + ) +} + +export const NewSessionItem = (props: { + slug: string + mobile?: boolean + dense?: boolean + sidebarExpanded: Accessor + clearHoverProjectSoon: () => void +}): JSX.Element => { + const layout = useLayout() + const language = useLanguage() + const label = language.t("command.session.new") + const tooltip = () => props.mobile || !props.sidebarExpanded() + const item = ( + { + if (layout.sidebar.opened()) return + props.clearHoverProjectSoon() + }} + > +
+ +
+ {label} +
+ ) + + return ( +
+ + {item} + + } + > + {item} + +
+ ) +} + +export const SessionSkeleton = (props: { count?: number }): JSX.Element => { + const items = Array.from({ length: props.count ?? 4 }, (_, index) => index) + return ( +
+ + {() =>
} + +
+ ) +} diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9d29a1b495f6b26682a7fe839f6d7608d56a9b4b --- /dev/null +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -0,0 +1,377 @@ +import { createMemo, For, Show, type Accessor, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { Button } from "@opencode-ai/ui/button" +import { ContextMenu } from "@opencode-ai/ui/context-menu" +import { HoverCard } from "@opencode-ai/ui/hover-card" +import { Icon } from "@opencode-ai/ui/icon" +import { createSortable } from "@thisbeyond/solid-dnd" +import { useLayout, type LocalProject } from "@/context/layout" +import { useServerSync } from "@/context/server-sync" +import { useLanguage } from "@/context/language" +import { useNotification } from "@/context/notification" +import { ProjectIcon, SessionItem, type SessionItemProps } from "./sidebar-items" +import { displayName, sortedRootSessions } from "./helpers" + +export type ProjectSidebarContext = { + currentDir: Accessor + currentProject: Accessor + sidebarOpened: Accessor + sidebarHovering: Accessor + hoverProject: Accessor + onProjectMouseEnter: (worktree: string, event: MouseEvent) => void + onProjectMouseLeave: (worktree: string) => void + onProjectFocus: (worktree: string) => void + onHoverOpenChanged: (worktree: string, hovered: boolean) => void + navigateToProject: (directory: string) => void + openSidebar: () => void + closeProject: (directory: string) => void + showEditProjectDialog: (project: LocalProject) => void + toggleProjectWorkspaces: (project: LocalProject) => void + workspacesEnabled: (project: LocalProject) => boolean + workspaceIds: (project: LocalProject) => string[] + workspaceLabel: (directory: string, branch?: string, projectId?: string) => string + sessionProps: Omit +} + +export const ProjectDragOverlay = (props: { + projects: Accessor + activeProject: Accessor +}): JSX.Element => { + const project = createMemo(() => props.projects().find((p) => p.worktree === props.activeProject())) + return ( + + {(p) => ( +
+ +
+ )} +
+ ) +} + +const ProjectTile = (props: { + project: LocalProject + mobile?: boolean + sidebarHovering: Accessor + selected: Accessor + active: Accessor + isWorking: Accessor + overlay: Accessor + suppressHover: Accessor + dirs: Accessor + onProjectMouseEnter: (worktree: string, event: MouseEvent) => void + onProjectMouseLeave: (worktree: string) => void + onProjectFocus: (worktree: string) => void + navigateToProject: (directory: string) => void + showEditProjectDialog: (project: LocalProject) => void + toggleProjectWorkspaces: (project: LocalProject) => void + workspacesEnabled: (project: LocalProject) => boolean + closeProject: (directory: string) => void + setMenu: (value: boolean) => void + setOpen: (value: boolean) => void + setSuppressHover: (value: boolean) => void + language: ReturnType +}): JSX.Element => { + const notification = useNotification() + const layout = useLayout() + const unseenCount = createMemo(() => + props.dirs().reduce((total, directory) => total + notification.project.unseenCount(directory), 0), + ) + + const clear = () => + props + .dirs() + .filter((directory) => notification.project.unseenCount(directory) > 0) + .forEach((directory) => notification.project.markViewed(directory)) + + return ( + { + props.setMenu(value) + props.setSuppressHover(value) + if (value) props.setOpen(false) + }} + > + { + if (event.button === 0 && !event.ctrlKey) { + props.setOpen(false) + props.setSuppressHover(true) + return + } + if (!props.overlay()) return + if (event.button !== 2 && !(event.button === 0 && event.ctrlKey)) return + props.setOpen(false) + props.setSuppressHover(true) + event.preventDefault() + }} + onMouseEnter={(event: MouseEvent) => { + if (!props.overlay()) return + if (props.suppressHover()) return + props.onProjectMouseEnter(props.project.worktree, event) + }} + onMouseLeave={() => { + if (props.suppressHover()) props.setSuppressHover(false) + if (!props.overlay()) return + props.onProjectMouseLeave(props.project.worktree) + }} + onFocus={() => { + if (!props.overlay()) return + if (props.suppressHover()) return + props.onProjectFocus(props.project.worktree) + }} + onClick={() => { + props.setOpen(false) + if (props.selected()) { + layout.sidebar.toggle() + return + } + props.navigateToProject(props.project.worktree) + }} + onBlur={() => props.setOpen(false)} + > + + + + + props.showEditProjectDialog(props.project)}> + {props.language.t("common.edit")} + + props.toggleProjectWorkspaces(props.project)} + > + + {props.workspacesEnabled(props.project) + ? props.language.t("sidebar.workspaces.disable") + : props.language.t("sidebar.workspaces.enable")} + + + + {props.language.t("sidebar.project.clearNotifications")} + + + props.closeProject(props.project.worktree)} + > + {props.language.t("common.close")} + + + + + ) +} + +const ProjectPreviewPanel = (props: { + project: LocalProject + mobile?: boolean + selected: Accessor + workspaceEnabled: Accessor + workspaces: Accessor + label: (directory: string) => string + projectSessions: Accessor> + workspaceSessions: (directory: string) => ReturnType + ctx: ProjectSidebarContext + language: ReturnType +}): JSX.Element => ( +
+
+
{displayName(props.project)}
+
+
{props.language.t("sidebar.project.recentSessions")}
+
+ + {(session) => ( + + )} + + } + > + + {(directory) => { + const sessions = createMemo(() => props.workspaceSessions(directory)) + return ( +
+
+
+ +
+ {props.label(directory)} +
+ + {(session) => ( + + )} + +
+ ) + }} +
+
+
+
+ +
+
+) + +export const SortableProject = (props: { + project: LocalProject + mobile?: boolean + ctx: ProjectSidebarContext + sortNow: Accessor +}): JSX.Element => { + const serverSync = useServerSync() + const language = useLanguage() + const sortable = createSortable(props.project.worktree) + const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree) + const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2)) + const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project)) + const dirs = createMemo(() => props.ctx.workspaceIds(props.project)) + const [state, setState] = createStore({ + menu: false, + suppressHover: false, + }) + + const isHoverProject = () => props.ctx.hoverProject() === props.project.worktree + const preview = createMemo(() => !props.mobile && props.ctx.sidebarOpened()) + const overlay = createMemo(() => !props.mobile && !props.ctx.sidebarOpened()) + const active = createMemo(() => state.menu || (preview() ? isHoverProject() : overlay() && isHoverProject())) + + const hoverOpen = () => isHoverProject() && preview() && !selected() && !state.menu + + const label = (directory: string) => { + const [data] = serverSync().child(directory, { bootstrap: false }) + const kind = + directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") + const name = props.ctx.workspaceLabel(directory, data.vcs?.branch, props.project.id) + return `${kind} : ${name}` + } + + const projectStore = createMemo(() => serverSync().child(props.project.worktree, { bootstrap: false })[0]) + const isWorking = createMemo(() => + dirs().some((directory) => { + return Object.keys(serverSync().session.data.session_status).some((id) => { + if (serverSync().session.get(id)?.directory !== directory) return false + return serverSync().session.data.session_working(id) + }) + }), + ) + const projectSessions = createMemo(() => sortedRootSessions(projectStore(), props.sortNow())) + const workspaceSessions = (directory: string) => { + const [data] = serverSync().child(directory, { bootstrap: false }) + return sortedRootSessions(data, props.sortNow()) + } + const tile = () => ( + state.suppressHover} + dirs={dirs} + onProjectMouseEnter={props.ctx.onProjectMouseEnter} + onProjectMouseLeave={props.ctx.onProjectMouseLeave} + onProjectFocus={props.ctx.onProjectFocus} + navigateToProject={props.ctx.navigateToProject} + showEditProjectDialog={props.ctx.showEditProjectDialog} + toggleProjectWorkspaces={props.ctx.toggleProjectWorkspaces} + workspacesEnabled={props.ctx.workspacesEnabled} + closeProject={props.ctx.closeProject} + setMenu={(value) => setState("menu", value)} + setOpen={(value) => props.ctx.onHoverOpenChanged(props.project.worktree, value)} + setSuppressHover={(value) => setState("suppressHover", value)} + language={language} + /> + ) + + return ( + // @ts-ignore +
+ + { + if (state.menu) return + if (value && state.suppressHover) return + props.ctx.onHoverOpenChanged(props.project.worktree, value) + }} + > + + + +
+ ) +} diff --git a/packages/app/src/pages/layout/sidebar-shell.tsx b/packages/app/src/pages/layout/sidebar-shell.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ca36af2a421ca090f7830d3c505c90b2f213e1e4 --- /dev/null +++ b/packages/app/src/pages/layout/sidebar-shell.tsx @@ -0,0 +1,125 @@ +import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" +import { + DragDropProvider, + DragDropSensors, + DragOverlay, + SortableProvider, + closestCenter, + type DragEvent, +} from "@thisbeyond/solid-dnd" +import { ConstrainDragXAxis } from "@/utils/solid-dnd" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { type LocalProject } from "@/context/layout" + +export const SidebarContent = (props: { + mobile?: boolean + opened: Accessor + aimMove: (event: MouseEvent) => void + projects: Accessor + renderProject: (project: LocalProject) => JSX.Element + handleDragStart: (event: unknown) => void + handleDragEnd: () => void + handleDragOver: (event: DragEvent) => void + openProjectLabel: JSX.Element + openProjectKeybind: Accessor + onOpenProject: () => void + renderProjectOverlay: () => JSX.Element + settingsLabel: Accessor + settingsKeybind: Accessor + onOpenSettings: () => void + helpLabel: Accessor + onOpenHelp: () => void + renderPanel: () => JSX.Element +}): JSX.Element => { + const expanded = createMemo(() => !!props.mobile || props.opened()) + const placement = () => (props.mobile ? "bottom" : "right") + let panel: HTMLDivElement | undefined + + createEffect(() => { + const el = panel + if (!el) return + if (expanded()) { + el.removeAttribute("inert") + return + } + el.setAttribute("inert", "") + }) + + return ( +
+
+
+ + + +
+ p.worktree)}> + {(project) => props.renderProject(project)} + + + {props.openProjectLabel} + + {props.openProjectKeybind()} + +
+ } + > + + +
+ {props.renderProjectOverlay()} + +
+
+ + + + + + +
+
+ +
{ + panel = el + }} + classList={{ "flex-1 flex h-full min-h-0 min-w-0 overflow-hidden": true, "pointer-events-none": !expanded() }} + aria-hidden={!expanded()} + > + {props.renderPanel()} +
+
+ ) +} diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe34bdd402e974155b3137fa15aabf66085dc4de --- /dev/null +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -0,0 +1,488 @@ +import { useNavigate, useParams } from "@solidjs/router" +import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { createSortable } from "@thisbeyond/solid-dnd" +import { createMediaQuery } from "@solid-primitives/media" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { getFilename } from "@opencode-ai/core/util/path" +import { Button } from "@opencode-ai/ui/button" +import { Collapsible } from "@opencode-ai/ui/collapsible" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { Spinner } from "@opencode-ai/ui/spinner" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { type Session } from "@opencode-ai/sdk/v2/client" +import { type LocalProject } from "@/context/layout" +import { useServerSync, useQueryOptions } from "@/context/server-sync" +import { useLanguage } from "@/context/language" +import { pathKey } from "@/utils/path-key" +import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" +import { sortedRootSessions } from "./helpers" +import { useIsFetching } from "@tanstack/solid-query" + +type InlineEditorComponent = (props: { + id: string + value: Accessor + onSave: (next: string) => void + class?: string + displayClass?: string + editing?: boolean + stopPropagation?: boolean + openOnDblClick?: boolean +}) => JSX.Element + +export type WorkspaceSidebarContext = { + currentDir: Accessor + navList: Accessor + sidebarExpanded: Accessor + sidebarHovering: Accessor + clearHoverProjectSoon: () => void + prefetchSession: (session: Session, priority?: "high" | "low") => void + archiveSession: (session: Session) => Promise + workspaceName: (directory: string, projectId?: string, branch?: string) => string | undefined + renameWorkspace: (directory: string, next: string, projectId?: string, branch?: string) => void + editorOpen: (id: string) => boolean + openEditor: (id: string, value: string) => void + closeEditor: () => void + setEditor: (key: "value", value: string) => void + InlineEditor: InlineEditorComponent + isBusy: (directory: string) => boolean + workspaceExpanded: (directory: string, local: boolean) => boolean + setWorkspaceExpanded: (directory: string, value: boolean) => void + showResetWorkspaceDialog: (root: string, directory: string) => void + showDeleteWorkspaceDialog: (root: string, directory: string) => void + setScrollContainerRef: (el: HTMLDivElement | undefined, mobile?: boolean) => void +} + +export const WorkspaceDragOverlay = (props: { + sidebarProject: Accessor + activeWorkspace: Accessor + workspaceLabel: (directory: string, branch?: string, projectId?: string) => string +}): JSX.Element => { + const serverSync = useServerSync() + const language = useLanguage() + const label = createMemo(() => { + const project = props.sidebarProject() + if (!project) return + const directory = props.activeWorkspace() + if (!directory) return + + const [workspaceStore] = serverSync().child(directory, { bootstrap: false }) + const kind = + directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox") + const name = props.workspaceLabel(directory, workspaceStore.vcs?.branch, project.id) + return `${kind} : ${name}` + }) + + return ( + + {(value) =>
{value()}
} +
+ ) +} + +const WorkspaceHeader = (props: { + local: Accessor + busy: Accessor + open: Accessor + directory: string + language: ReturnType + branch: Accessor + workspaceValue: Accessor + workspaceEditActive: Accessor + InlineEditor: WorkspaceSidebarContext["InlineEditor"] + renameWorkspace: WorkspaceSidebarContext["renameWorkspace"] + setEditor: WorkspaceSidebarContext["setEditor"] + projectId?: string +}): JSX.Element => ( +
+
+ }> + + +
+ + {props.local() ? props.language.t("workspace.type.local") : props.language.t("workspace.type.sandbox")} : + + + {props.branch() ?? getFilename(props.directory)} + + } + > + { + const trimmed = next.trim() + if (!trimmed) return + props.renameWorkspace(props.directory, trimmed, props.projectId, props.branch()) + props.setEditor("value", props.workspaceValue()) + }} + class="text-14-medium text-text-base min-w-0 truncate" + displayClass="text-14-medium text-text-base min-w-0 truncate" + editing={props.workspaceEditActive()} + stopPropagation={false} + openOnDblClick={false} + /> + +
+ +
+
+) + +const WorkspaceActions = (props: { + directory: string + local: Accessor + busy: Accessor + menuOpen: Accessor + pendingRename: Accessor + setMenuOpen: (open: boolean) => void + setPendingRename: (value: boolean) => void + sidebarHovering: Accessor + touch: Accessor + language: ReturnType + workspaceValue: Accessor + openEditor: WorkspaceSidebarContext["openEditor"] + showResetWorkspaceDialog: WorkspaceSidebarContext["showResetWorkspaceDialog"] + showDeleteWorkspaceDialog: WorkspaceSidebarContext["showDeleteWorkspaceDialog"] + root: string + clearHoverProjectSoon: WorkspaceSidebarContext["clearHoverProjectSoon"] + navigateToNewSession: () => void +}): JSX.Element => ( +
+ props.setMenuOpen(open)} + > + + + + + { + if (!props.pendingRename()) return + event.preventDefault() + props.setPendingRename(false) + props.openEditor(`workspace:${props.directory}`, props.workspaceValue()) + }} + > + { + props.setPendingRename(true) + props.setMenuOpen(false) + }} + > + {props.language.t("common.rename")} + + props.showResetWorkspaceDialog(props.root, props.directory)} + > + {props.language.t("common.reset")} + + props.showDeleteWorkspaceDialog(props.root, props.directory)} + > + {props.language.t("common.delete")} + + + + + + + } + variant="ghost" + size="small" + class="size-6 rounded-md opacity-0 pointer-events-none group-hover/workspace:opacity-100 group-hover/workspace:pointer-events-auto group-focus-within/workspace:opacity-100 group-focus-within/workspace:pointer-events-auto" + data-action="workspace-new-session" + data-workspace={base64Encode(props.directory)} + aria-label={props.language.t("command.session.new")} + onClick={(event) => { + event.preventDefault() + event.stopPropagation() + props.clearHoverProjectSoon() + props.navigateToNewSession() + }} + /> + + +
+) + +const WorkspaceSessionList = (props: { + slug: Accessor + mobile?: boolean + ctx: WorkspaceSidebarContext + showNew: Accessor + loading: Accessor + sessions: Accessor + hasMore: Accessor + loadMore: () => Promise + language: ReturnType +}): JSX.Element => ( + +) + +export const SortableWorkspace = (props: { + ctx: WorkspaceSidebarContext + directory: string + project: LocalProject + sortNow: Accessor + mobile?: boolean +}): JSX.Element => { + const navigate = useNavigate() + const params = useParams() + const serverSync = useServerSync() + const queryOptions = useQueryOptions() + const language = useLanguage() + const sortable = createSortable(props.directory) + const [workspaceStore, setWorkspaceStore] = serverSync().child(props.directory, { bootstrap: false }) + const [menu, setMenu] = createStore({ + open: false, + pendingRename: false, + }) + const slug = createMemo(() => base64Encode(props.directory)) + const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow())) + const local = createMemo(() => props.directory === props.project.worktree) + const active = createMemo(() => pathKey(props.ctx.currentDir()) === pathKey(props.directory)) + const workspaceValue = createMemo(() => { + const branch = workspaceStore.vcs?.branch + const name = branch ?? getFilename(props.directory) + return props.ctx.workspaceName(props.directory, props.project.id, branch) ?? name + }) + const open = createMemo(() => props.ctx.workspaceExpanded(props.directory, local())) + const boot = createMemo(() => open() || active()) + const count = createMemo(() => sessions()?.length ?? 0) + const hasMore = createMemo(() => workspaceStore.sessionTotal > count()) + const fetching = useIsFetching(() => queryOptions().sessions(pathKey(props.directory))) + const busy = createMemo(() => props.ctx.isBusy(props.directory)) + const loading = () => fetching() > 0 && count() === 0 + const touch = createMediaQuery("(hover: none)") + const showNew = createMemo(() => !loading() && (touch() || count() === 0 || (active() && !params.id))) + const loadMore = async () => { + setWorkspaceStore("limit", (limit) => (limit ?? 0) + 5) + await serverSync().project.loadSessions(props.directory) + } + + const workspaceEditActive = createMemo(() => props.ctx.editorOpen(`workspace:${props.directory}`)) + const header = () => ( + workspaceStore.vcs?.branch} + workspaceValue={workspaceValue} + workspaceEditActive={workspaceEditActive} + InlineEditor={props.ctx.InlineEditor} + renameWorkspace={props.ctx.renameWorkspace} + setEditor={props.ctx.setEditor} + projectId={props.project.id} + /> + ) + + const openWrapper = (value: boolean) => { + props.ctx.setWorkspaceExpanded(props.directory, value) + if (value) return + if (props.ctx.editorOpen(`workspace:${props.directory}`)) props.ctx.closeEditor() + } + + createEffect(() => { + if (!boot()) return + serverSync().child(props.directory, { bootstrap: true }) + }) + + return ( +
+ +
+
+
+ + {header()} + + } + > +
+ {header()} +
+
+ menu.open} + pendingRename={() => menu.pendingRename} + setMenuOpen={(open) => setMenu("open", open)} + setPendingRename={(value) => setMenu("pendingRename", value)} + sidebarHovering={props.ctx.sidebarHovering} + touch={touch} + language={language} + workspaceValue={workspaceValue} + openEditor={props.ctx.openEditor} + showResetWorkspaceDialog={props.ctx.showResetWorkspaceDialog} + showDeleteWorkspaceDialog={props.ctx.showDeleteWorkspaceDialog} + root={props.project.worktree} + clearHoverProjectSoon={props.ctx.clearHoverProjectSoon} + navigateToNewSession={() => navigate(`/${slug()}/session`)} + /> +
+
+
+ + + + +
+
+ ) +} + +export const LocalWorkspace = (props: { + ctx: WorkspaceSidebarContext + project: LocalProject + sortNow: Accessor + mobile?: boolean +}): JSX.Element => { + const serverSync = useServerSync() + const queryOptions = useQueryOptions() + const language = useLanguage() + const workspace = createMemo(() => { + const [store, setStore] = serverSync().child(props.project.worktree) + return { store, setStore } + }) + const slug = createMemo(() => base64Encode(props.project.worktree)) + const sessions = createMemo(() => sortedRootSessions(workspace().store, props.sortNow())) + const count = createMemo(() => sessions()?.length ?? 0) + const fetching = useIsFetching(() => queryOptions().sessions(pathKey(props.project.worktree))) + const hasMore = createMemo(() => workspace().store.sessionTotal > count()) + const loading = () => fetching() > 0 && count() === 0 + const loadMore = async () => { + workspace().setStore("limit", (limit) => (limit ?? 0) + 5) + await serverSync().project.loadSessions(props.project.worktree) + } + + return ( +
props.ctx.setScrollContainerRef(el, props.mobile)} + class="size-full flex flex-col py-2 overflow-y-auto no-scrollbar [overflow-anchor:none]" + > + false} + loading={loading} + sessions={sessions} + hasMore={hasMore} + loadMore={loadMore} + language={language} + /> +
+ ) +} diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7c7b89f58927064cad0d9ab5132b8ddc480bed6f --- /dev/null +++ b/packages/app/src/pages/new-session.tsx @@ -0,0 +1,49 @@ +import { createPromptProjectController } from "@/components/prompt-project-selector" +import { useTitlebarRightMount } from "@/components/titlebar" +import { useSettings } from "@/context/settings" +import { createEffect, createResource } from "solid-js" +import { createNewSessionDraftController } from "./new-session/new-session-draft-controller" +import { NewSessionStatus, NewSessionView } from "./new-session/new-session-view" +import { createNewSessionWorkspaceController } from "./new-session/new-session-workspace-controller" +import { useNewSessionCommands } from "./new-session/use-new-session-commands" + +/** The draft-only V2 session page. Submitting promotes the draft into a real session. */ +export default function NewSessionPage() { + const settings = useSettings() + const rightMount = useTitlebarRightMount() + const workspace = createNewSessionWorkspaceController() + const draft = createNewSessionDraftController({ + worktree: workspace.selection.value, + resetWorktree: workspace.selection.reset, + }) + const project = createPromptProjectController({ + controls: draft.project.controls, + onDone: draft.input.restoreFocus, + }) + useNewSessionCommands({ + restoreFocus: draft.input.restoreFocus, + project: { + empty: project.empty, + open: () => project.setOpen(true), + }, + }) + createEffect(() => { + if (!draft.prompt.ready()) return + draft.input.restoreFocus() + }) + const ready = Promise.resolve() + const [suspendUntilPromptReady] = createResource( + () => draft.prompt.readyPromise() ?? ready, + (promise) => promise.then(() => true), + ) + + return ( +
+ {suspendUntilPromptReady()} + +
+ +
+
+ ) +} diff --git a/packages/app/src/pages/new-session/new-session-draft-controller.ts b/packages/app/src/pages/new-session/new-session-draft-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf22834e48d923267158808b2011ae407583b163 --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-draft-controller.ts @@ -0,0 +1,64 @@ +import { useSearchParams } from "@solidjs/router" +import { createEffect, untrack } from "solid-js" +import { usePromptInputV2Controller } from "@/components/prompt-input-v2" +import { useComments } from "@/context/comments" +import { useLocal } from "@/context/local" +import { usePrompt } from "@/context/prompt" +import { useServerSync } from "@/context/server-sync" +import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer" +import { createPromptModelSelection } from "@/pages/session/composer/prompt-model-selection" +import { useSessionKey } from "@/pages/session/session-layout" +import { useComposerCommands } from "@/pages/session/use-composer-commands" + +export function createNewSessionDraftController(workspace: { worktree: () => string; resetWorktree: () => void }) { + const prompt = usePrompt() + const serverSync = useServerSync() + const comments = useComments() + const local = useLocal() + const route = useSessionKey() + const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() + const model = createPromptModelSelection({ agent: () => local.agent.current() }) + + useComposerCommands({ model }) + + const controls = createPromptInputController({ + sessionKey: route.sessionKey, + sessionID: () => route.params.id, + queryOptions: serverSync().queryOptions, + model, + }) + const projectControls = createPromptProjectControls() + const input = usePromptInputV2Controller({ + get controls() { + return controls() + }, + get newSessionWorktree() { + return workspace.worktree() + }, + onNewSessionWorktreeReset: workspace.resetWorktree, + onSubmit: comments.clear, + }) + + createEffect(() => { + if (!prompt.ready()) return + untrack(() => { + const text = searchParams.prompt + if (!text) return + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + setSearchParams({ ...searchParams, prompt: undefined }) + }) + }) + + return { + input, + prompt: { + ready: prompt.ready, + readyPromise: () => prompt.ready.promise, + }, + project: { + controls: projectControls, + }, + } +} + +export type NewSessionDraftController = ReturnType diff --git a/packages/app/src/pages/new-session/new-session-view.tsx b/packages/app/src/pages/new-session/new-session-view.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5960e6433526868634d94b3e44ea0ac5a46fd06f --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-view.tsx @@ -0,0 +1,162 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2" +import { Show, createMemo, createSignal, type Accessor } from "solid-js" +import { createStore } from "solid-js/store" +import { Portal } from "solid-js/web" +import createPresence from "solid-presence" +import { PromptInputV2Composer } from "@/components/prompt-input-v2" +import { PromptGitStatus, PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" +import { + PromptProjectAddButton, + PromptProjectSelector, + type PromptProjectController, +} from "@/components/prompt-project-selector" +import { StatusPopoverV2 } from "@/components/status-popover" +import { useLanguage } from "@/context/language" +import { useSDK } from "@/context/sdk" +import { useServerSync } from "@/context/server-sync" +import { useProviders } from "@/hooks/use-providers" +import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" +import { Persist, persisted } from "@/utils/persist" +import type { NewSessionDraftController } from "./new-session-draft-controller" +import type { NewSessionWorkspaceController } from "./new-session-workspace-controller" + +const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000 + +export function NewSessionView(props: { + input: NewSessionDraftController["input"] + project: PromptProjectController + workspace: NewSessionWorkspaceController +}) { + return ( +
+
+
+
+ +
+ + + + + +
+ + + } + > + + +
+
+
+
+
+ +
+
+ ) +} + +export function NewSessionStatus(props: { mount: Accessor; visible: Accessor }) { + const language = useLanguage() + + return ( + + {(mount) => ( + + + + + + + + )} + + ) +} + +function ProviderTip() { + const language = useLanguage() + const dialog = useDialog() + const sdk = useSDK() + const serverSync = useServerSync() + const providers = useProviders(() => sdk().directory) + const [persistedState, setPersistedState, , persistedReady] = persisted( + Persist.global("new-session.provider-tip"), + createStore({ dismissedAt: 0 }), + ) + const visible = createMemo( + () => + serverSync().child(sdk().directory)[0].provider_ready && + persistedReady() && + providers.paid().length === 0 && + Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, + ) + const [ref, setRef] = createSignal() + const presence = createPresence({ + show: visible, + element: () => ref() ?? null, + }) + const openProviders = () => { + void import("@/components/dialog-connect-provider").then(({ DialogConnectProvider }) => { + void dialog.show(() => sdk().directory} />) + }) + } + + return ( + +
+
+ + + + +
+
+
+ ) +} diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a79ae77fa7aae0c0c24810b8aab965b4d287016 --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from "bun:test" +import { + normalizeNewSessionWorktree, + resolveNewSessionBranch, + resolveNewSessionWorktree, +} from "./new-session-workspace-controller" + +describe("new session workspace selection", () => { + test("uses main when the workspace bar is unavailable", () => { + expect( + resolveNewSessionWorktree({ + enabled: false, + selected: "/project/feature", + directory: "/project/feature", + projectWorktree: "/project", + }), + ).toBe("main") + }) + + test("derives an existing worktree from the current directory", () => { + expect( + resolveNewSessionWorktree({ enabled: true, directory: "/project/feature", projectWorktree: "/project" }), + ).toBe("/project/feature") + expect(resolveNewSessionWorktree({ enabled: true, directory: "/project", projectWorktree: "/project" })).toBe( + "main", + ) + }) + + test("normalizes main to the project root outside the main worktree", () => { + expect(normalizeNewSessionWorktree("main", "/project/feature", "/project")).toBe("/project") + expect(normalizeNewSessionWorktree("main", "/project", "/project")).toBe("main") + }) + + test("falls back to the local branch for main, create, and unknown worktrees", () => { + const branch = (worktree: string) => (worktree === "/project/feature" ? "feature" : undefined) + expect(resolveNewSessionBranch({ worktree: "main", local: "dev", worktreeBranch: branch })).toBe("dev") + expect(resolveNewSessionBranch({ worktree: "create", local: "dev", worktreeBranch: branch })).toBe("dev") + expect(resolveNewSessionBranch({ worktree: "/project/feature", local: "dev", worktreeBranch: branch })).toBe( + "feature", + ) + expect(resolveNewSessionBranch({ worktree: "/missing", local: "dev", worktreeBranch: branch })).toBe("dev") + }) +}) diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3fc9b2708d4d164aa814eb97945b8ba29ad2eee --- /dev/null +++ b/packages/app/src/pages/new-session/new-session-workspace-controller.ts @@ -0,0 +1,77 @@ +import { createMemo, createSignal } from "solid-js" +import { useSDK } from "@/context/sdk" +import { useServerSync } from "@/context/server-sync" +import { useSync } from "@/context/sync" + +const workspaceBarEnabled = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" + +export function resolveNewSessionWorktree(input: { + enabled: boolean + selected?: string + directory: string + projectWorktree?: string +}) { + if (!input.enabled) return "main" + if (input.selected) return input.selected + if (input.projectWorktree && input.directory !== input.projectWorktree) return input.directory + return "main" +} + +export function normalizeNewSessionWorktree(value: string, directory: string, projectWorktree?: string) { + if (value === "main" && projectWorktree !== directory) return projectWorktree + return value +} + +export function resolveNewSessionBranch(input: { + worktree: string + local?: string + worktreeBranch: (worktree: string) => string | undefined +}) { + if (input.worktree === "main" || input.worktree === "create") return input.local + return input.worktreeBranch(input.worktree) ?? input.local +} + +export function createNewSessionWorkspaceController() { + const sdk = useSDK() + const sync = useSync() + const serverSync = useServerSync() + const [worktree, setWorktree] = createSignal() + const visible = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") + const value = createMemo(() => + resolveNewSessionWorktree({ + enabled: visible(), + selected: worktree(), + directory: sdk().directory, + projectWorktree: sync().project?.worktree, + }), + ) + const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) + const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) + const branch = createMemo(() => + resolveNewSessionBranch({ + worktree: value(), + local: localBranch(), + worktreeBranch: (worktree) => serverSync().child(worktree)[0].vcs?.branch, + }), + ) + + return { + selection: { + value, + reset: () => setWorktree(), + set: (worktree: string) => + setWorktree(normalizeNewSessionWorktree(worktree, sdk().directory, sync().project?.worktree)), + }, + project: { + root: projectRoot, + workspaces: () => sync().project?.sandboxes ?? [], + git: () => sync().project?.vcs === "git", + }, + bar: { + visible, + branch, + }, + } +} + +export type NewSessionWorkspaceController = ReturnType diff --git a/packages/app/src/pages/new-session/use-new-session-commands.tsx b/packages/app/src/pages/new-session/use-new-session-commands.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a0d835f97f53eacd9d66a12e8c32941d2567ed29 --- /dev/null +++ b/packages/app/src/pages/new-session/use-new-session-commands.tsx @@ -0,0 +1,44 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useSettingsCommand } from "@/components/settings-dialog" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" + +export function useNewSessionCommands(input: { + restoreFocus: () => void + project: { + empty: () => boolean + open: () => void + } +}) { + const command = useCommand() + const dialog = useDialog() + const language = useLanguage() + + useSettingsCommand() + command.register("new-session", () => [ + { + id: "command.palette", + title: language.t("command.palette"), + hidden: true, + onSelect: async () => { + const { DialogSelectFile } = await import("@/components/dialog-select-file") + void dialog.show(() => ) + }, + }, + { + id: "input.focus", + title: language.t("command.input.focus"), + category: language.t("command.category.view"), + keybind: "ctrl+l", + onSelect: input.restoreFocus, + }, + { + id: "project.select", + title: language.t("session.new.project.search"), + category: language.t("command.category.project"), + keybind: "mod+shift+o", + disabled: input.project.empty(), + onSelect: input.project.open, + }, + ]) +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2e647e0f4789c55c18483973ea8f11d23932b9fc --- /dev/null +++ b/packages/app/src/pages/session.tsx @@ -0,0 +1,2391 @@ +import type { FilePart, Project, UserMessage, VcsFileDiff } from "@opencode-ai/sdk/v2" +import { getFilename } from "@opencode-ai/core/util/path" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { createQuery, skipToken, useMutation, useQueryClient } from "@tanstack/solid-query" +import { + batch, + ErrorBoundary, + onCleanup, + Suspense, + Show, + Match, + Switch, + createMemo, + createEffect, + createComputed, + createSignal, + on, + onMount, + type ParentProps, + untrack, +} from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import { createMediaQuery } from "@solid-primitives/media" +import { createResizeObserver } from "@solid-primitives/resize-observer" +import { debounce } from "@solid-primitives/scheduled" +import { useLocal } from "@/context/local" +import { FileProvider, selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" +import { createStore } from "solid-js/store" +import type { SessionReviewLineComment } from "@opencode-ai/session-ui/session-review" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { Select } from "@opencode-ai/ui/select" +import { SelectV2 } from "@opencode-ai/ui/v2/select-v2" +import { isScrollKeyTarget, scrollKey, scrollKeyOwner } from "@opencode-ai/ui/scroll-view" +import { Tabs } from "@opencode-ai/ui/tabs" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { createAutoScroll } from "@opencode-ai/ui/hooks" +import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-bridge" +import { Button } from "@opencode-ai/ui/button" +import { showToast } from "@/utils/toast" +import { base64Encode, checksum } from "@opencode-ai/core/util/encode" +import { useLocation, useNavigate, useParams, useSearchParams } from "@solidjs/router" +import { NewSessionView, SessionHeader } from "@/components/session" +import { ErrorPage } from "@/pages/error" +import { CommentsProvider, useComments } from "@/context/comments" +import { useCommand } from "@/context/command" +import { DirectoryDataProvider } from "@/pages/directory-layout" +import { useServerSync } from "@/context/server-sync" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { ModelsProvider } from "@/context/models" +import { useNotification } from "@/context/notification" +import { PromptProvider, usePrompt } from "@/context/prompt" +import { usePlatform } from "@/context/platform" +import { SDKProvider, useSDK } from "@/context/sdk" +import { useServerSDK } from "@/context/server-sdk" +import { ServerConnection, serverName, useServer } from "@/context/server" +import { useSettings } from "@/context/settings" +import { useSync } from "@/context/sync" +import { useTabs } from "@/context/tabs" +import { TerminalProvider, useTerminal } from "@/context/terminal" +import { PromptInput } from "@/components/prompt-input" +import { PromptInputV2Composer, usePromptInputV2Controller } from "@/components/prompt-input-v2" +import { useSettingsCommand } from "@/components/settings-dialog" +import { setCursorPosition } from "@/components/prompt-input/editor-dom" +import { promptLength } from "@/components/prompt-input/history" +import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit" +import { + createPromptInputController, + createSessionComposerController, + createSessionComposerRegionController, + SessionComposerRegion, +} from "@/pages/session/composer" +import { createOpenReviewFile, createSessionTabs, createSizing, shouldShowFileTree } from "@/pages/session/helpers" +import { MessageTimeline } from "@/pages/session/timeline/message-timeline" +import { createTimelineModel } from "@/pages/session/timeline/model" +import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab" +import { useSessionLayout } from "@/pages/session/session-layout" +import { restorePromptModel, syncPromptModel, syncSessionModel } from "@/pages/session/session-model-helpers" +import { + clampSessionPanelWidth, + SESSION_PANEL_WIDTH_MIN, + sessionPanelWidthMax, +} from "@/pages/session/session-panel-width" +import { SessionSidePanel } from "@/pages/session/session-side-panel" +import { sessionPanelLayout } from "@/pages/session/session-panel-layout" +import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2" +import { SessionReviewEmptyNoGitV2 } from "@opencode-ai/session-ui/v2/session-review-empty-no-git-v2" +import { SessionReviewV2SidebarToggle } from "@opencode-ai/session-ui/v2/session-review-v2" +import { ReviewPanelV2 } from "@/pages/session/v2/review-panel-v2" +import { createReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state" +import { reviewDiffDirectory, reviewDiffNeedsLoad, reviewRootDirectory } from "@/pages/session/v2/review-diff-kinds" +import { TerminalPanel } from "@/pages/session/terminal-panel" +import { TerminalPanelV2 } from "@/pages/session/terminal-panel-v2" +import { useComposerCommands } from "@/pages/session/use-composer-commands" +import { useSessionCommands } from "@/pages/session/use-session-commands" +import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" +import { Identifier } from "@/utils/id" +import { diffs as list } from "@/utils/diffs" +import { Persist, persisted } from "@/utils/persist" +import { extractPromptFromParts } from "@/utils/prompt" +import { formatServerError, isLocalSessionNotFoundError, isSessionNotFoundError } from "@/utils/server-errors" +import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" +import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs" +import { createSessionOwnership } from "./session/session-ownership" +import { createSessionLineage } from "./session/session-lineage" + +type FollowupItem = FollowupDraft & { id: string } +type FollowupEdit = Pick +const emptyFollowups: FollowupItem[] = [] + +type ChangeMode = "git" | "branch" | "turn" +type VcsMode = "git" | "branch" + +const sessionViewState = () => ({ + messageId: undefined as string | undefined, + mobileTab: "session" as "session" | "changes", +}) + +function isCurrentSessionNotFoundError(error: unknown, sessionID: string | undefined) { + if (!sessionID) return false + return isSessionNotFoundError(error, sessionID) || isLocalSessionNotFoundError(error, sessionID) +} + +async function runPromptRollbackMutation(input: { + capturePrompt: () => { current: () => T[]; set: (value: T[]) => void; reset: () => void } + optimistic: (prompt: { set: (value: T[]) => void; reset: () => void }) => void + request: () => Promise + complete: (result: R) => void + rollback: () => void + fail: (error: unknown) => void +}) { + const prompt = input.capturePrompt() + const previous = prompt.current().slice() + batch(() => input.optimistic(prompt)) + await input + .request() + .then(input.complete) + .catch((error) => { + batch(() => { + input.rollback() + prompt.set(previous) + }) + input.fail(error) + }) +} + +export function SessionPage() { + return ( + + + + ) +} + +// Rendered under app.tsx's TargetSessionRoute, which owns the per-server keyed +// remount around the server-scoped providers. Nothing here may key on the +// session ID: session tabs on the same server share this route instance, and +// workspace-scoped state (terminal, directory providers) lives below. +export function TargetSessionRouteContent() { + const params = useParams<{ serverKey: string; id: string }>() + const serverSync = useServerSync() + const directory = createMemo(() => serverSync().session.lineage.peek(params.id)?.session.directory) + return ( + // Settings must keep the target-server SDK, sync, and models context and remain registered + // when session content falls back to the route error boundary. + params.id}> + + + + + + ) +} + +function TargetSessionSettingsCommand() { + useSettingsCommand() + return null +} + +export function SessionRouteErrorBoundary( + props: ParentProps<{ sessionID?: string; serverKey?: ServerConnection.Key; padded?: boolean }>, +) { + const settings = useSettings() + return ( + + settings.general.newLayoutDesigns() ? ( + + + + + + ) : ( + + ) + } + > + {props.children} + + ) +} + +function SessionErrorFallback(props: { error: unknown; sessionID?: string; serverKey?: ServerConnection.Key }) { + const language = useLanguage() + const server = useServer() + const tabs = useTabs() + const displayServer = createMemo(() => { + const key = props.serverKey ?? server.key + const conn = server.list.find((item) => ServerConnection.key(item) === key) + return conn ? serverName(conn) : key + }) + const closeTab = () => { + if (!props.sessionID) return + tabs.removeSessionTab({ server: props.serverKey ?? server.key, sessionId: props.sessionID }) + } + if (isCurrentSessionNotFoundError(props.error, props.sessionID)) { + return ( +
+
+
+
{language.t("session.error.notFound")}
+
+ {language.t("session.error.notFound.description")} +
+
+ + {(sessionID) => ( +
+
{displayServer()}
+ + {sessionID()} + +
+ )} +
+ + {language.t("session.error.notFound.closeTab")} + +
+
+ ) + } + return +} + +function ResolvedTargetSessionRoute() { + const params = useParams<{ serverKey: string; id: string }>() + const tabs = useTabs() + const sync = useServerSync() + const serverKey = createMemo(() => requireServerKey(params.serverKey)) + const current = createSessionLineage( + () => params.id, + () => sync().session.lineage, + ) + const directory = createMemo(() => current()?.session.directory) + const targetDirectory = () => directory()! + + createEffect(() => { + const session = current() + if (!session) return + tabs.addSessionTab({ + server: serverKey(), + sessionId: session.root.id, + }) + }) + + return ( + // Non-keyed: closes only while the target's directory is unknown (uncached + // lineage mid-resolution), which tears down the workspace subtree including + // the terminal. Same-workspace tab switches keep it open because warm + // targets resolve synchronously from the sync cache. + + + + + + + + ) +} + +// Owns the workspace-identity remount. Must not include the session ID in the +// key: SessionPage handles session changes reactively, and remounting here +// destroys workspace-scoped state (terminal PTYs, file/prompt providers). +function TargetSessionPage() { + const sdk = useSDK() + const serverSDK = useServerSDK() + return ( + + + + ) +} + +function TargetServerScopedProviders( + props: ParentProps<{ directory?: () => string | undefined; sessionID?: () => string | undefined }>, +) { + return ( + <> + + {props.children} + + ) +} + +function MarkSessionNotificationsViewed(props: { sessionID?: () => string | undefined }) { + const notification = useNotification() + createEffect(() => { + const sessionID = props.sessionID?.() + if (!notification.ready() || !sessionID) return + if (notification.session.unseenCount(sessionID) === 0) return + notification.session.markViewed(sessionID) + }) + return null +} + +function SessionProviders(props: ParentProps) { + return ( + + + + {props.children} + + + + ) +} + +function SessionRouteFrame(props: ParentProps<{ padded?: boolean }>) { + return ( +
+ {props.children} +
+ ) +} + +function SessionPanelFrame(props: ParentProps<{ newLayout: boolean; raised?: boolean }>) { + return ( +
+ {props.children} +
+ ) +} + +export default function Page() { + const serverSync = useServerSync() + const layout = useLayout() + const local = useLocal() + const file = useFile() + const sync = useSync() + const queryClient = useQueryClient() + const dialog = useDialog() + const language = useLanguage() + const sdk = useSDK() + const serverSDK = useServerSDK() + const settings = useSettings() + const platform = usePlatform() + const prompt = usePrompt() + const comments = useComments() + const command = useCommand() + const terminal = useTerminal() + const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() + const location = useLocation() + const navigate = useNavigate() + const { params, sessionKey, workspaceKey, tabs, view } = useSessionLayout() + const reviewMode = () => view().review.mode() ?? "git" + const reviewFile = () => view().review.file() + const sessionOwnership = createSessionOwnership(sessionKey) + const newSessionDesign = createMemo(() => settings.general.newLayoutDesigns()) + + createEffect(() => { + if (!prompt.ready()) return + untrack(() => { + if (params.id) return + const text = searchParams.prompt + if (!text) return + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + setSearchParams({ ...searchParams, prompt: undefined }) + }) + }) + + const [ui, setUi] = createStore({ + pendingMessage: undefined as string | undefined, + reviewSnap: false, + scrollGesture: 0, + scroll: { + overflow: false, + bottom: true, + jump: false, + }, + }) + + const composer = createSessionComposerController() + const inputController = createPromptInputController({ + sessionKey, + sessionID: () => params.id, + queryOptions: serverSync().queryOptions, + }) + + const workspaceTabs = createMemo(() => layout.tabs(workspaceKey)) + const sessionPanelKey = createMemo(() => (params.id ? `${serverSDK().scope}\0${params.id}` : undefined)) + + createEffect( + on( + () => params.id, + (id, prev) => { + if (!id) return + if (prev) return + + const pending = layout.handoff.tabs() + if (!pending) return + if (Date.now() - pending.at > 60_000) { + layout.handoff.clearTabs() + return + } + if (pending.scope !== serverSDK().scope) return + + if (pending.id !== id) return + layout.handoff.clearTabs() + if (pending.dir !== base64Encode(sdk().directory)) return + + const from = workspaceTabs().tabs() + if (from.all.length === 0 && !from.active) return + + const current = tabs().tabs() + if (current.all.length > 0 || current.active) return + + const all = normalizeTabs(from.all) + const active = from.active ? normalizeTab(from.active) : undefined + tabs().setAll(all) + tabs().setActive(active && all.includes(active) ? active : all[0]) + + workspaceTabs().setAll([]) + workspaceTabs().setActive(undefined) + }, + { defer: true }, + ), + ) + + const isDesktop = createMediaQuery("(min-width: 768px)") + const size = createSizing() + const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const desktopV2ReviewOpen = createMemo(() => newSessionDesign() && desktopReviewOpen() && !!params.id) + const terminalOpen = createMemo(() => view().terminal.opened()) + const desktopTerminalOpen = createMemo(() => isDesktop() && terminalOpen()) + const desktopInlineTerminalOnlyOpen = createMemo( + () => newSessionDesign() && desktopTerminalOpen() && !desktopV2ReviewOpen(), + ) + const desktopFileTreeOpen = createMemo( + () => + isDesktop() && + shouldShowFileTree({ + visible: settings.visibility.fileTree(), + opened: layout.fileTree.opened(), + }), + ) + const desktopSessionResizeOpen = createMemo(() => + newSessionDesign() ? desktopV2ReviewOpen() || desktopTerminalOpen() : desktopReviewOpen(), + ) + const desktopSidePanelOpen = createMemo(() => desktopSessionResizeOpen() || desktopFileTreeOpen()) + let panelRow: HTMLDivElement | undefined + const [panelRowWidth, setPanelRowWidth] = createSignal() + createResizeObserver( + () => panelRow, + ({ width }) => setPanelRowWidth(width), + ) + const splitReview = createMemo( + () => (newSessionDesign() ? desktopV2ReviewOpen() : desktopReviewOpen()) && layout.review.diffStyle() === "split", + ) + // The observer reports the content-box width, which already excludes the row + // padding; only the flex gap between the panels remains to subtract. + const sessionPanelAvailable = createMemo(() => { + const width = panelRowWidth() + if (width === undefined) return undefined + return width - (settings.general.newLayoutDesigns() ? 8 : 0) + }) + const sessionPanelMax = createMemo(() => { + const available = sessionPanelAvailable() + if (available === undefined) return 1000 + return sessionPanelWidthMax({ available, split: splitReview() }) + }) + // Clamp at render time so window or sidebar resizes squeeze the chat panel + // instead of the review pane, without overwriting the persisted width. + const sessionPanelResizedWidth = createMemo(() => + clampSessionPanelWidth({ + width: layout.session.width(), + available: sessionPanelAvailable(), + split: splitReview(), + }), + ) + const sessionPanelWidth = createMemo(() => { + if (!desktopSidePanelOpen()) return "100%" + if (desktopSessionResizeOpen()) return `${sessionPanelResizedWidth()}px` + return `calc(100% - ${layout.fileTree.width()}px)` + }) + const centered = createMemo(() => isDesktop() && (newSessionDesign() || !desktopReviewOpen())) + const desktopV2PanelLayout = createMemo(() => + sessionPanelLayout({ + review: desktopV2ReviewOpen(), + terminal: desktopTerminalOpen(), + files: desktopFileTreeOpen(), + }), + ) + + function normalizeTab(tab: string) { + if (!tab.startsWith("file://")) return tab + return file.tab(tab) + } + + function normalizeTabs(list: string[]) { + const seen = new Set() + const next: string[] = [] + for (const item of list) { + const value = normalizeTab(item) + if (seen.has(value)) continue + seen.add(value) + next.push(value) + } + return next + } + + const openReviewPanel = () => { + if (!view().reviewPanel.opened()) view().reviewPanel.open() + } + + const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined)) + const isChildSession = createMemo(() => !!info()?.parentID) + const canReview = createMemo(() => !!sync().project) + const reviewTab = createMemo(() => isDesktop()) + const tabState = createSessionTabs({ + tabs, + pathFromTab: file.pathFromTab, + normalizeTab, + review: reviewTab, + hasReview: canReview, + }) + const activeTab = tabState.activeTab + const activeFileTab = tabState.activeFileTab + const revertMessageID = createMemo(() => info()?.revert?.messageID) + const timeline = createTimelineModel({ sessionID: () => params.id, revertMessageID }) + const historyLoading = timeline.history.loading + const historyMore = timeline.history.more + const lastUserMessage = timeline.lastUserMessage + const messages = timeline.messages + const messagesReady = timeline.ready + const sessionSync = timeline.resource + const userMessages = timeline.userMessages + const visibleUserMessages = timeline.visibleUserMessages + + createEffect(() => { + const tab = activeFileTab() + if (!tab) return + + const path = file.pathFromTab(tab) + if (path) void file.load(path) + }) + + createEffect( + on( + () => lastUserMessage()?.id, + () => { + const msg = lastUserMessage() + if (!msg) return + syncSessionModel(local, msg) + }, + ), + ) + + let restoredModelSession: string | undefined + createEffect(() => { + const id = params.id + if (!id || !prompt.ready() || !local.session.ready()) return + if (restoredModelSession !== id) { + restoredModelSession = id + if (restorePromptModel(local, prompt)) return + } + syncPromptModel(local, prompt) + }) + + createEffect( + on( + () => ({ dir: sdk().directory, id: params.id }), + (next, prev) => { + if (!prev) return + if (next.dir === prev.dir && next.id === prev.id) return + if (prev.id && !next.id) local.session.reset() + }, + { defer: true }, + ), + ) + + const [store, setStore] = createStore({ + ...sessionViewState(), + newSessionWorktree: "main", + deferRender: false, + }) + + const [followup, setFollowup] = persisted( + Persist.serverWorkspace(serverSDK().scope, sdk().directory, "followup", ["followup.v1"]), + createStore<{ + items: Record + failed: Record + paused: Record + edit: Record + }>({ + items: {}, + failed: {}, + paused: {}, + edit: {}, + }), + ) + + createComputed((prev) => { + const key = sessionKey() + if (key !== prev) { + setStore("deferRender", true) + const owner = sessionOwnership.capture() + requestAnimationFrame(() => { + setTimeout(() => owner.run(() => setStore("deferRender", false)), 0) + }) + } + return key + }) + + let reviewFrame: number | undefined + let todoFrame: number | undefined + let todoTimer: number | undefined + let diffFrame: number | undefined + let diffTimer: number | undefined + + createComputed((prev) => { + const open = desktopReviewOpen() + if (prev === undefined || prev === open) return open + + if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame) + setUi("reviewSnap", true) + reviewFrame = requestAnimationFrame(() => { + reviewFrame = undefined + setUi("reviewSnap", false) + }) + return open + }, desktopReviewOpen()) + + const turnDiffs = createMemo(() => list(lastUserMessage()?.summary?.diffs)) + const nogit = createMemo(() => { + const project = sync().project + return !!project && project.vcs !== "git" + }) + const changesOptions = createMemo(() => { + const list: ChangeMode[] = [] + const project = sync().project + const vcs = sync().data.vcs + if (project?.vcs === "git") list.push("git") + if (project?.vcs === "git" && vcs?.branch && vcs?.default_branch && vcs.branch !== vcs.default_branch) { + list.push("branch") + } + list.push("turn") + return list + }) + const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes") + const wantsReview = createMemo(() => + isDesktop() + ? desktopFileTreeOpen() || + (desktopReviewOpen() && (activeTab() === "review" || (newSessionDesign() && !!activeFileTab()))) + : store.mobileTab === "changes", + ) + const vcsMode = createMemo(() => { + const mode = reviewMode() + if (mode === "git" || mode === "branch") return mode + }) + const vcsKey = createMemo( + () => + ["session-vcs", sdk().directory, sync().data.vcs?.branch ?? "", sync().data.vcs?.default_branch ?? ""] as const, + ) + const vcsQuery = createQuery(() => { + const mode = vcsMode() + const enabled = wantsReview() && sync().project?.vcs === "git" + + return { + queryKey: [...vcsKey(), mode] as const, + enabled, + queryFn: mode + ? () => + sdk() + .api.vcs.diff({ location: { directory: sdk().directory }, mode: mode === "git" ? "working" : mode }) + .then((result) => result.data) + .catch((error) => { + console.debug("[session-review] failed to load vcs diff", { mode, error }) + return [] + }) + : skipToken, + } + }) + const refreshVcs = debounce(() => void queryClient.invalidateQueries({ queryKey: vcsKey() }), 100) + const reviewDiffs = () => { + if (reviewMode() === "git" || reviewMode() === "branch") + // avoids suspense + return vcsQuery.isFetched ? (vcsQuery.data ?? []) : [] + return turnDiffs() + } + const activeReviewFile = () => { + const diffs = reviewDiffs() + const selected = reviewFile() + if (selected && diffs.some((diff) => diff.file === selected)) return selected + return diffs[0]?.file + } + const reviewCount = () => reviewDiffs().length + const hasReview = () => reviewCount() > 0 + const reviewReady = () => { + if (reviewMode() === "git" || reviewMode() === "branch") return !vcsQuery.isPending + return true + } + const loadReviewDiff = async (file: string, version?: number): Promise => { + const mode = vcsMode() + if (!mode) return + const root = reviewRootDirectory(sync().project?.worktree ?? sdk().directory) + const directory = reviewDiffDirectory(root, file) + const source = reviewDiffs().find((diff) => diff.file === file) + const valid = (diff: VcsFileDiff | undefined) => { + if (!diff || !source) return + if (diff.additions !== source.additions || diff.deletions !== source.deletions) return + if (reviewDiffNeedsLoad(diff)) return + return diff + } + const request = (scope: string, context?: number) => + queryClient + .fetchQuery({ + queryKey: [serverSDK().scope, ...vcsKey(), mode, "directory", scope, context, version] as const, + staleTime: Number.POSITIVE_INFINITY, + retry: 2, + queryFn: () => + sdk() + .api.vcs.diff({ + location: { directory: scope }, + mode: mode === "git" ? "working" : mode, + context, + }) + .then((result) => result.data), + }) + .then((diffs) => diffs.find((diff) => diff.file === file)) + + if (directory !== root) { + try { + const scoped = valid(await request(directory)) + if (scoped) return scoped + } catch (error) { + console.debug("[session-review] failed to load scoped vcs diff", { mode, file, directory, error }) + } + } + try { + const bounded = valid(await request(root, 3)) + if (bounded) return bounded + } catch (error) { + console.debug("[session-review] failed to load bounded vcs diff", { mode, file, root, error }) + } + } + + const newSessionWorktree = createMemo(() => { + if (store.newSessionWorktree === "create") return "create" + const project = sync().project + if (project && sdk().directory !== project.worktree) return sdk().directory + return "main" + }) + + const setActiveMessage = (message: UserMessage | undefined) => { + messageMark = scrollMark + setStore("messageId", message?.id) + } + + const anchor = (id: string) => `message-${id}` + + const cursor = () => { + const root = scroller + if (!root) return store.messageId + + const box = root.getBoundingClientRect() + const line = box.top + 100 + const list = [...root.querySelectorAll("[data-message-id]")] + .map((el) => { + const id = el.dataset.messageId + if (!id) return + + const rect = el.getBoundingClientRect() + return { id, top: rect.top, bottom: rect.bottom } + }) + .filter((item): item is { id: string; top: number; bottom: number } => !!item) + + const shown = list.filter((item) => item.bottom > box.top && item.top < box.bottom) + const hit = shown.find((item) => item.top <= line && item.bottom >= line) + if (hit) return hit.id + + const near = [...shown].sort((a, b) => { + const da = Math.abs(a.top - line) + const db = Math.abs(b.top - line) + if (da !== db) return da - db + return a.top - b.top + })[0] + if (near) return near.id + + return list.filter((item) => item.top <= line).at(-1)?.id ?? list[0]?.id ?? store.messageId + } + + function navigateMessageByOffset(offset: number) { + const msgs = visibleUserMessages() + if (msgs.length === 0) return + + const current = store.messageId && messageMark === scrollMark ? store.messageId : cursor() + const base = current ? msgs.findIndex((m) => m.id === current) : msgs.length + const currentIndex = base === -1 ? msgs.length : base + const targetIndex = currentIndex + offset + if (targetIndex < 0 || targetIndex > msgs.length) return + + if (targetIndex === msgs.length) { + resumeScroll() + return + } + + autoScroll.pause() + scrollToMessage(msgs[targetIndex], "auto") + } + + function upsert(next: Project) { + const list = serverSync().data.project + sync().set("project", next.id) + const idx = list.findIndex((item) => item.id === next.id) + if (idx >= 0) { + serverSync().set( + "project", + list.map((item, i) => (i === idx ? { ...item, ...next } : item)), + ) + return + } + const at = list.findIndex((item) => item.id > next.id) + if (at >= 0) { + serverSync().set("project", [...list.slice(0, at), next, ...list.slice(at)]) + return + } + serverSync().set("project", [...list, next]) + } + + const gitMutation = useMutation(() => ({ + mutationFn: () => sdk().client.project.initGit(), + onSuccess: (x) => { + if (!x.data) return + upsert(x.data) + }, + onError: (err) => { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: formatServerError(err, language.t), + }) + }, + })) + + function initGit() { + if (gitMutation.isPending) return + gitMutation.mutate() + } + + let inputRef!: HTMLDivElement + let promptDock: HTMLDivElement | undefined + let dockHeight = 0 + let scroller: HTMLDivElement | undefined + let content: HTMLDivElement | undefined + let revealMessage = (_id: string) => {} + let scrollToEnd = () => {} + let scrollMark = 0 + let messageMark = 0 + + const scrollGestureWindowMs = 250 + + const markScrollGesture = (target?: EventTarget | null) => { + const root = scroller + if (!root) return + + const el = target instanceof Element ? target : undefined + const nested = el?.closest("[data-scrollable]") + if (nested && nested !== root) return + + setUi("scrollGesture", Date.now()) + } + + const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs + + createEffect( + on( + () => { + const id = params.id + return [ + sdk().directory, + id, + id ? (sync().data.session_status[id]?.type ?? "idle") : "idle", + id ? composer.blocked() : false, + ] as const + }, + ([dir, id, status, blocked]) => { + if (todoFrame !== undefined) cancelAnimationFrame(todoFrame) + if (todoTimer !== undefined) window.clearTimeout(todoTimer) + todoFrame = undefined + todoTimer = undefined + if (!id) return + if (status === "idle" && !blocked) return + const cached = untrack(() => sync().data.todo[id] !== undefined) + + todoFrame = requestAnimationFrame(() => { + todoFrame = undefined + todoTimer = window.setTimeout(() => { + todoTimer = undefined + if (sdk().directory !== dir || params.id !== id) return + untrack(() => { + void sync().session.todo(id, cached ? { force: true } : undefined) + }) + }, 0) + }) + }, + { defer: true }, + ), + ) + + createEffect( + on( + () => visibleUserMessages().at(-1)?.id, + (lastId, prevLastId) => { + if (lastId && prevLastId && lastId > prevLastId) { + setStore("messageId", undefined) + } + }, + { defer: true }, + ), + ) + + createEffect( + on( + sessionKey, + () => { + setStore(sessionViewState()) + setUi("pendingMessage", undefined) + }, + { defer: true }, + ), + ) + + const stopVcs = sdk().event.listen((evt) => { + const details = evt.details as { type: string; properties?: unknown } + if (details.type !== "file.watcher.updated" && details.type !== "filesystem.changed") return + const props = + typeof details.properties === "object" && details.properties + ? (details.properties as Record) + : undefined + const file = typeof props?.file === "string" ? props.file : undefined + if (!file || file.startsWith(".git/")) return + refreshVcs() + }) + onCleanup(stopVcs) + + createEffect( + on( + () => sdk().directory, + (dir) => { + if (!dir) return + setStore("newSessionWorktree", "main") + }, + { defer: true }, + ), + ) + + const selectionPreview = (path: string, selection: FileSelection) => { + const content = file.get(path)?.content?.content + if (!content) return undefined + return previewSelectedLines(content, { start: selection.startLine, end: selection.endLine }) + } + + const addCommentToContext = (input: { + file: string + selection: SelectedLineRange + comment: string + preview?: string + origin?: "review" | "file" + }) => { + const selection = selectionFromLines(input.selection) + const preview = input.preview ?? selectionPreview(input.file, selection) + const saved = comments.add({ + file: input.file, + selection: input.selection, + comment: input.comment, + }) + prompt.context.add({ + type: "file", + path: input.file, + selection, + comment: input.comment, + commentID: saved.id, + commentOrigin: input.origin, + preview, + }) + } + + const updateCommentInContext = (input: { + id: string + file: string + selection: SelectedLineRange + comment: string + preview?: string + }) => { + comments.update(input.file, input.id, input.comment) + prompt.context.updateComment(input.file, input.id, { + comment: input.comment, + ...(input.preview ? { preview: input.preview } : {}), + }) + } + + const removeCommentFromContext = (input: { id: string; file: string }) => { + comments.remove(input.file, input.id) + prompt.context.removeComment(input.file, input.id) + } + + const reviewCommentActions = createMemo(() => ({ + moreLabel: language.t("common.moreOptions"), + editLabel: language.t("common.edit"), + deleteLabel: language.t("common.delete"), + saveLabel: language.t("common.save"), + })) + + const isEditableTarget = (target: EventTarget | null | undefined) => { + if (!(target instanceof HTMLElement)) return false + return /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(target.tagName) || target.isContentEditable + } + + const deepActiveElement = () => { + let current: Element | null = document.activeElement + while (current instanceof HTMLElement && current.shadowRoot?.activeElement) { + current = current.shadowRoot.activeElement + } + return current instanceof HTMLElement ? current : undefined + } + + const handleKeyDown = (event: KeyboardEvent) => { + const path = event.composedPath() + const target = path.find((item): item is HTMLElement => item instanceof HTMLElement) + const activeElement = deepActiveElement() + + const protectedTarget = path.some( + (item) => item instanceof HTMLElement && item.closest("[data-prevent-autofocus]") !== null, + ) + if (protectedTarget || isEditableTarget(target)) return + + if (activeElement) { + const isProtected = activeElement.closest("[data-prevent-autofocus]") + const isInput = isEditableTarget(activeElement) + if (isProtected || isInput) return + } + if (dialog.active) return + + if (activeElement === inputRef) { + if (event.key === "Escape") inputRef?.blur() + return + } + + const key = scrollKey(event) + if (key) { + if (!scroller || !isScrollKeyTarget(target ?? null, key)) return + if (scrollKeyOwner(scroller, target ?? null, key) !== scroller) return + markScrollGesture(scroller) + return + } + + if (event.key.length === 1 && event.key !== "Unidentified" && !(event.ctrlKey || event.metaKey)) { + if (composer.blocked() || isChildSession()) return + const input = inputRef + if (!input) return + input.focus() + setCursorPosition(input, prompt.cursor() ?? promptLength(prompt.current())) + } + } + + createEffect(() => { + if (!layout.ready()) return + if (sync().status !== "complete") return + if (!sync().project) return + const list = changesOptions() + const mode = reviewMode() + if (list.includes(mode)) return + const next = list[0] + if (!next) return + view().review.setMode(next) + }) + + createEffect( + on( + () => sync().data.session_status[params.id ?? ""]?.type, + (next, prev) => { + if (next !== "idle" || prev === undefined || prev === "idle") return + refreshVcs() + }, + { defer: true }, + ), + ) + + const fileTreeTab = () => layout.fileTree.tab() + const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value) + + const [tree, setTree] = createStore({ + reviewScroll: undefined as HTMLDivElement | undefined, + pendingDiff: undefined as string | undefined, + }) + + createEffect( + on( + sessionKey, + () => { + setTree({ + reviewScroll: undefined, + pendingDiff: undefined, + }) + }, + { defer: true }, + ), + ) + + const showAllFiles = () => { + if (fileTreeTab() !== "changes") return + setFileTreeTab("all") + } + + const focusInput = () => { + if (isChildSession()) return + inputRef?.focus() + } + + useComposerCommands() + useSessionCommands({ + navigateMessageByOffset, + setActiveMessage, + focusInput, + review: reviewTab, + fileBrowser: () => newSessionDesign() && isDesktop() && !!params.id, + }) + command.register("session-palette", () => [ + { + id: "command.palette", + title: language.t("command.palette"), + hidden: true, + onSelect: () => command.trigger("file.open", "palette"), + }, + ]) + + const openReviewFile = createOpenReviewFile({ + showAllFiles, + tabForPath: file.tab, + openTab: tabs().open, + setActive: tabs().setActive, + loadFile: file.load, + }) + + const changesLabel = (option: ChangeMode) => { + if (option === "git") return language.t("ui.sessionReview.title.git") + if (option === "branch") return language.t("ui.sessionReview.title.branch") + return language.t("ui.sessionReview.title.lastTurn") + } + + const changesTitle = () => { + if (!canReview()) { + return null + } + + return ( +
` + + const focused = focusTerminalById("one") + + expect(focused).toBe(true) + expect(document.activeElement?.tagName).toBe("TEXTAREA") + }) + + test("falls back to terminal element focus", () => { + document.body.innerHTML = `
` + const terminal = document.querySelector('[data-component="terminal"]') as HTMLElement + let pointerDown = false + terminal.addEventListener("pointerdown", () => { + pointerDown = true + }) + + const focused = focusTerminalById("two") + + expect(focused).toBe(true) + expect(document.activeElement).toBe(terminal) + expect(pointerDown).toBe(true) + }) +}) + +describe("getTabReorderIndex", () => { + test("returns target index for valid drag reorder", () => { + expect(getTabReorderIndex(["a", "b", "c"], "a", "c")).toBe(2) + }) + + test("returns undefined for unknown droppable id", () => { + expect(getTabReorderIndex(["a", "b", "c"], "a", "missing")).toBeUndefined() + }) +}) + +describe("createSessionTabs", () => { + test("normalizes the effective file tab", () => { + createRoot((dispose) => { + const [state] = createStore({ + active: undefined as string | undefined, + all: ["file://src/a.ts", "context"], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined), + normalizeTab: (tab) => (tab.startsWith("file://") ? `norm:${tab.slice("file://".length)}` : tab), + }) + + expect(result.activeTab()).toBe("norm:src/a.ts") + expect(result.activeFileTab()).toBe("norm:src/a.ts") + expect(result.closableTab()).toBe("norm:src/a.ts") + dispose() + }) + }) + + test("prefers context and review fallbacks when no file tab is active", () => { + createRoot((dispose) => { + const [state] = createStore({ + active: undefined as string | undefined, + all: ["context"], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: () => undefined, + normalizeTab: (tab) => tab, + review: () => true, + hasReview: () => true, + }) + + expect(result.activeTab()).toBe("context") + expect(result.closableTab()).toBe("context") + dispose() + }) + + createRoot((dispose) => { + const [state] = createStore({ + active: undefined as string | undefined, + all: [], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: () => undefined, + normalizeTab: (tab) => tab, + review: () => true, + hasReview: () => true, + }) + + expect(result.activeTab()).toBe("review") + expect(result.activeFileTab()).toBeUndefined() + expect(result.closableTab()).toBeUndefined() + dispose() + }) + }) + + test("exposes the Open File tab without treating it as a file tab", () => { + createRoot((dispose) => { + const [state] = createStore({ + active: SESSION_OPEN_FILE_TAB as string | undefined, + all: ["file://src/a.ts", SESSION_OPEN_FILE_TAB], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined), + normalizeTab: (tab) => tab, + fileBrowser: () => true, + }) + + expect(result.openFileOpen()).toBe(true) + expect(result.panelTabs()).toEqual(["file://src/a.ts", SESSION_OPEN_FILE_TAB]) + expect(result.openedTabs()).toEqual(["file://src/a.ts"]) + expect(result.activeTab()).toBe(SESSION_OPEN_FILE_TAB) + expect(result.activeFileTab()).toBeUndefined() + expect(result.closableTab()).toBe(SESSION_OPEN_FILE_TAB) + dispose() + }) + }) + + test("hides the Open File placeholder when the file browser is unavailable", () => { + createRoot((dispose) => { + const [state] = createStore({ + active: SESSION_OPEN_FILE_TAB as string | undefined, + all: ["file://src/a.ts", SESSION_OPEN_FILE_TAB], + }) + const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all })) + const result = createSessionTabs({ + tabs, + pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined), + normalizeTab: (tab) => tab, + fileBrowser: () => false, + }) + + expect(result.openFileOpen()).toBe(false) + expect(result.panelTabs()).toEqual(["file://src/a.ts"]) + expect(result.activeTab()).toBe("file://src/a.ts") + dispose() + }) + }) +}) diff --git a/packages/app/src/pages/session/helpers.ts b/packages/app/src/pages/session/helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..c43f6bc6a3654945582e6713e9a4949c73cd4187 --- /dev/null +++ b/packages/app/src/pages/session/helpers.ts @@ -0,0 +1,209 @@ +import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js" +import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" +import { same } from "@/utils/same" +import { SESSION_OPEN_FILE_TAB } from "@/context/layout-tabs" + +export { SESSION_OPEN_FILE_TAB } from "@/context/layout-tabs" + +const emptyTabs: string[] = [] + +type Tabs = { + active: Accessor + all: Accessor +} + +type TabsInput = { + tabs: Accessor + pathFromTab: (tab: string) => string | undefined + normalizeTab: (tab: string) => string + review?: Accessor + hasReview?: Accessor + fileBrowser?: Accessor +} + +export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}` + +export function shouldShowFileTree(input: { visible: boolean; opened: boolean }) { + return input.opened && input.visible +} + +export const createSessionTabs = (input: TabsInput) => { + const review = input.review ?? (() => false) + const hasReview = input.hasReview ?? (() => false) + const fileBrowser = input.fileBrowser ?? (() => false) + const contextOpen = createMemo(() => input.tabs().active() === "context" || input.tabs().all().includes("context")) + const openFileOpen = createMemo( + () => + fileBrowser() && + (input.tabs().active() === SESSION_OPEN_FILE_TAB || input.tabs().all().includes(SESSION_OPEN_FILE_TAB)), + ) + const panelTabs = createMemo( + () => { + const seen = new Set() + return input + .tabs() + .all() + .flatMap((tab) => { + if (tab === "context" || tab === "review") return [] + if (tab === SESSION_OPEN_FILE_TAB && !fileBrowser()) return [] + const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab + if (seen.has(value)) return [] + seen.add(value) + return [value] + }) + }, + emptyTabs, + { equals: same }, + ) + const openedTabs = createMemo(() => panelTabs().filter((tab) => tab !== SESSION_OPEN_FILE_TAB), emptyTabs, { + equals: same, + }) + const activeTab = createMemo(() => { + const active = input.tabs().active() + if (active === "context") return active + if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active + if (active === "review" && review()) return active + if (active && input.pathFromTab(active)) return input.normalizeTab(active) + + const first = openedTabs()[0] + if (first) return first + if (contextOpen()) return "context" + if (review() && hasReview()) return "review" + return "empty" + }) + const activeFileTab = createMemo(() => { + const active = activeTab() + if (!openedTabs().includes(active)) return + return active + }) + const closableTab = createMemo(() => { + const active = activeTab() + if (active === "context") return active + if (active === SESSION_OPEN_FILE_TAB && openFileOpen()) return active + if (!openedTabs().includes(active)) return + return active + }) + + return { + contextOpen, + openFileOpen, + panelTabs, + openedTabs, + activeTab, + activeFileTab, + closableTab, + } +} + +export const focusTerminalById = (id: string) => { + const wrapper = document.getElementById(`terminal-wrapper-${id}`) + const terminal = wrapper?.querySelector('[data-component="terminal"]') + if (!(terminal instanceof HTMLElement)) return false + + const textarea = terminal.querySelector("textarea") + if (textarea instanceof HTMLTextAreaElement) { + textarea.focus() + return true + } + + terminal.focus() + terminal.dispatchEvent( + typeof PointerEvent === "function" + ? new PointerEvent("pointerdown", { bubbles: true, cancelable: true }) + : new MouseEvent("pointerdown", { bubbles: true, cancelable: true }), + ) + return true +} + +export const createOpenReviewFile = (input: { + showAllFiles: () => void + tabForPath: (path: string) => string + openTab: (tab: string) => void + setActive: (tab: string) => void + loadFile: (path: string) => any | Promise +}) => { + return (path: string) => { + batch(() => { + input.showAllFiles() + const maybePromise = input.loadFile(path) + const open = () => { + const tab = input.tabForPath(path) + input.openTab(tab) + input.setActive(tab) + } + if (maybePromise instanceof Promise) void maybePromise.then(open) + else open() + }) + } +} + +export const createOpenSessionFileTab = (input: { + normalizeTab: (tab: string) => string + openTab: (tab: string) => void + pathFromTab: (tab: string) => string | undefined + loadFile: (path: string) => void + openReviewPanel: () => void + setActive: (tab: string) => void +}) => { + return (value: string) => { + const next = input.normalizeTab(value) + input.openTab(next) + + const path = input.pathFromTab(next) + if (!path) return + + input.loadFile(path) + input.openReviewPanel() + input.setActive(next) + } +} + +export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => { + const fromIndex = tabs.indexOf(from) + const toIndex = tabs.indexOf(to) + if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined + return toIndex +} + +export const createSizing = () => { + const [state, setState] = createStore({ active: false }) + let t: number | undefined + + const stop = () => { + if (t !== undefined) { + clearTimeout(t) + t = undefined + } + setState("active", false) + } + + const start = () => { + if (t !== undefined) { + clearTimeout(t) + t = undefined + } + setState("active", true) + } + + onMount(() => { + makeEventListener(window, "pointerup", stop) + makeEventListener(window, "pointercancel", stop) + makeEventListener(window, "blur", stop) + }) + + onCleanup(() => { + if (t !== undefined) clearTimeout(t) + }) + + return { + active: () => state.active, + start, + touch() { + start() + t = window.setTimeout(stop, 120) + }, + } +} + +export type Sizing = ReturnType diff --git a/packages/app/src/pages/session/message-gesture.test.ts b/packages/app/src/pages/session/message-gesture.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b2af4bb8342387cba09afe2105bc43f338a570d1 --- /dev/null +++ b/packages/app/src/pages/session/message-gesture.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from "bun:test" +import { normalizeWheelDelta, shouldMarkBoundaryGesture } from "./message-gesture" + +describe("normalizeWheelDelta", () => { + test("converts line mode to px", () => { + expect(normalizeWheelDelta({ deltaY: 3, deltaMode: 1, rootHeight: 500 })).toBe(120) + }) + + test("converts page mode to container height", () => { + expect(normalizeWheelDelta({ deltaY: -1, deltaMode: 2, rootHeight: 600 })).toBe(-600) + }) + + test("keeps pixel mode unchanged", () => { + expect(normalizeWheelDelta({ deltaY: 16, deltaMode: 0, rootHeight: 600 })).toBe(16) + }) +}) + +describe("shouldMarkBoundaryGesture", () => { + test("marks when nested scroller cannot scroll", () => { + expect( + shouldMarkBoundaryGesture({ + delta: 20, + scrollTop: 0, + scrollHeight: 300, + clientHeight: 300, + }), + ).toBe(true) + }) + + test("marks when scrolling beyond top boundary", () => { + expect( + shouldMarkBoundaryGesture({ + delta: -40, + scrollTop: 10, + scrollHeight: 1000, + clientHeight: 400, + }), + ).toBe(true) + }) + + test("marks when scrolling beyond bottom boundary", () => { + expect( + shouldMarkBoundaryGesture({ + delta: 50, + scrollTop: 580, + scrollHeight: 1000, + clientHeight: 400, + }), + ).toBe(true) + }) + + test("does not mark when nested scroller can consume movement", () => { + expect( + shouldMarkBoundaryGesture({ + delta: 20, + scrollTop: 200, + scrollHeight: 1000, + clientHeight: 400, + }), + ).toBe(false) + }) +}) diff --git a/packages/app/src/pages/session/message-gesture.ts b/packages/app/src/pages/session/message-gesture.ts new file mode 100644 index 0000000000000000000000000000000000000000..731cb1bdeb6ce0a9d66fa17f1f887f70f8eb9558 --- /dev/null +++ b/packages/app/src/pages/session/message-gesture.ts @@ -0,0 +1,21 @@ +export const normalizeWheelDelta = (input: { deltaY: number; deltaMode: number; rootHeight: number }) => { + if (input.deltaMode === 1) return input.deltaY * 40 + if (input.deltaMode === 2) return input.deltaY * input.rootHeight + return input.deltaY +} + +export const shouldMarkBoundaryGesture = (input: { + delta: number + scrollTop: number + scrollHeight: number + clientHeight: number +}) => { + const max = input.scrollHeight - input.clientHeight + if (max <= 1) return true + if (!input.delta) return false + + if (input.delta < 0) return input.scrollTop + input.delta <= 0 + + const remaining = max - input.scrollTop + return input.delta > remaining +} diff --git a/packages/app/src/pages/session/message-id-from-hash.ts b/packages/app/src/pages/session/message-id-from-hash.ts new file mode 100644 index 0000000000000000000000000000000000000000..2857f4b01d6bd10ff4f3362a71b284e1e7395948 --- /dev/null +++ b/packages/app/src/pages/session/message-id-from-hash.ts @@ -0,0 +1,6 @@ +export const messageIdFromHash = (hash: string) => { + const value = hash.startsWith("#") ? hash.slice(1) : hash + const match = value.match(/^message-(.+)$/) + if (!match) return + return match[1] +} diff --git a/packages/app/src/pages/session/new-session-layout.ts b/packages/app/src/pages/session/new-session-layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..edd953ed2d3e0fc011242fa38c8a67e93e44f76e --- /dev/null +++ b/packages/app/src/pages/session/new-session-layout.ts @@ -0,0 +1,2 @@ +/** Inline new-session content width — keep in sync with session composer `placement === "inline"`. */ +export const NEW_SESSION_CONTENT_WIDTH = "w-full max-w-[720px] px-0" diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1b65af7121ac870d6fcaac70a8cd9a18c747e2d1 --- /dev/null +++ b/packages/app/src/pages/session/review-tab.tsx @@ -0,0 +1,172 @@ +import { createEffect, onCleanup, type JSX } from "solid-js" +import { makeEventListener } from "@solid-primitives/event-listener" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" +import { SessionReview } from "@opencode-ai/session-ui/session-review" +import type { + SessionReviewCommentActions, + SessionReviewCommentDelete, + SessionReviewCommentUpdate, +} from "@opencode-ai/session-ui/session-review" +import type { SelectedLineRange } from "@/context/file" +import { useSDK } from "@/context/sdk" +import { useLayout } from "@/context/layout" +import type { LineComment } from "@/context/comments" + +export type DiffStyle = "unified" | "split" + +type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff + +export interface SessionReviewTabProps { + title?: JSX.Element + empty?: JSX.Element + diffs: () => ReviewDiff[] + view: () => ReturnType["view"]> + diffStyle: DiffStyle + onDiffStyleChange?: (style: DiffStyle) => void + onViewFile?: (file: string) => void + onLineComment?: (comment: { file: string; selection: SelectedLineRange; comment: string; preview?: string }) => void + onLineCommentUpdate?: (comment: SessionReviewCommentUpdate) => void + onLineCommentDelete?: (comment: SessionReviewCommentDelete) => void + lineCommentActions?: SessionReviewCommentActions + comments?: LineComment[] + focusedComment?: { file: string; id: string } | null + onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void + focusedFile?: string + onScrollRef?: (el: HTMLDivElement | undefined) => void + commentMentions?: { + items: (query: string) => string[] | Promise + } + classes?: { + root?: string + header?: string + container?: string + } +} + +export function SessionReviewTab(props: SessionReviewTabProps) { + let scroll: HTMLDivElement | undefined + let restoreFrame: number | undefined + let userInteracted = false + let restored: { x: number; y: number } | undefined + + const sdk = useSDK() + const layout = useLayout() + + const readFile = async (path: string) => { + return sdk() + .client.file.read({ path }) + .then((x) => x.data) + .catch((error) => { + console.debug("[session-review] failed to read file", { path, error }) + return undefined + }) + } + + const handleInteraction = () => { + userInteracted = true + + if (restoreFrame !== undefined) { + cancelAnimationFrame(restoreFrame) + restoreFrame = undefined + } + } + + const doRestore = () => { + restoreFrame = undefined + const el = scroll + if (!el || !layout.ready() || userInteracted) return + if (el.clientHeight === 0 || el.clientWidth === 0) return + + const s = props.view().scroll("review") + if (!s || (s.x === 0 && s.y === 0)) return + + const maxY = Math.max(0, el.scrollHeight - el.clientHeight) + const maxX = Math.max(0, el.scrollWidth - el.clientWidth) + + const targetY = Math.min(s.y, maxY) + const targetX = Math.min(s.x, maxX) + + if (el.scrollTop === targetY && el.scrollLeft === targetX) return + + if (el.scrollTop !== targetY) el.scrollTop = targetY + if (el.scrollLeft !== targetX) el.scrollLeft = targetX + restored = { x: el.scrollLeft, y: el.scrollTop } + } + + const queueRestore = () => { + if (userInteracted || restoreFrame !== undefined) return + restoreFrame = requestAnimationFrame(doRestore) + } + + const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => { + const el = event.currentTarget + const prev = restored + if (prev && el.scrollTop === prev.y && el.scrollLeft === prev.x) { + restored = undefined + return + } + + restored = undefined + handleInteraction() + if (!layout.ready()) return + if (el.clientHeight === 0 || el.clientWidth === 0) return + + props.view().setScroll("review", { + x: el.scrollLeft, + y: el.scrollTop, + }) + } + + createEffect(() => { + props.diffs().length + props.diffStyle + if (!layout.ready()) return + queueRestore() + }) + + onCleanup(() => { + if (restoreFrame !== undefined) cancelAnimationFrame(restoreFrame) + props.onScrollRef?.(undefined) + }) + + return ( + { + scroll = el + makeEventListener(el, "wheel", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "mousewheel", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "pointerdown", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "touchstart", handleInteraction, { passive: true, capture: true }) + makeEventListener(el, "keydown", handleInteraction, { capture: true }) + props.onScrollRef?.(el) + queueRestore() + }} + onScroll={handleScroll} + onDiffRendered={queueRestore} + open={props.view().review.open()} + onOpenChange={props.view().review.setOpen} + classes={{ + root: props.classes?.root ?? "pr-3", + header: props.classes?.header ?? "px-3", + container: props.classes?.container ?? "pl-3", + }} + diffs={props.diffs()} + diffStyle={props.diffStyle} + onDiffStyleChange={props.onDiffStyleChange} + onViewFile={props.onViewFile} + focusedFile={props.focusedFile} + readFile={readFile} + onLineComment={props.onLineComment} + onLineCommentUpdate={props.onLineCommentUpdate} + onLineCommentDelete={props.onLineCommentDelete} + lineCommentActions={props.lineCommentActions} + lineCommentMention={props.commentMentions} + comments={props.comments} + focusedComment={props.focusedComment} + onFocusedCommentChange={props.onFocusedCommentChange} + /> + ) +} diff --git a/packages/app/src/pages/session/session-archive.ts b/packages/app/src/pages/session/session-archive.ts new file mode 100644 index 0000000000000000000000000000000000000000..396886953954888b797c154333f20f6e8f5317df --- /dev/null +++ b/packages/app/src/pages/session/session-archive.ts @@ -0,0 +1,74 @@ +import { useNavigate } from "@solidjs/router" +import { produce } from "solid-js/store" +import { notifySessionTabsRemoved } from "@/components/titlebar-session-events" +import { useLanguage } from "@/context/language" +import { useSDK } from "@/context/sdk" +import { useServerSync } from "@/context/server-sync" +import { useSync } from "@/context/sync" +import { useTabs } from "@/context/tabs" +import { errorMessage } from "@/pages/layout/helpers" +import { useSessionKey } from "@/pages/session/session-layout" +import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" +import { showToast } from "@/utils/toast" + +export function useSessionArchive() { + const language = useLanguage() + const navigate = useNavigate() + const sdk = useSDK() + const sync = useSync() + const serverSync = useServerSync() + const tabs = useTabs() + const { params } = useSessionKey() + + const navigateAfterRemoval = (sessionID: string, parentID?: string, nextSessionID?: string) => { + if (params.id !== sessionID) return + const href = (id: string) => + params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id) + if (parentID) { + navigate(href(parentID)) + return + } + if (nextSessionID) { + navigate(href(nextSessionID)) + return + } + if (params.serverKey) { + tabs.newDraft({ server: requireServerKey(params.serverKey), directory: sdk().directory }) + return + } + navigate(`/${params.dir}/session`) + } + + const archive = async (sessionID: string) => { + const session = sync().session.get(sessionID) + if (!session) return + if ((await sdk().protocol) !== "v1") return + + const sessions = sync().data.session ?? [] + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + await sdk() + .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) + .then(() => { + sync().set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === sessionID) + if (index !== -1) draft.session.splice(index, 1) + }), + ) + sync().session.evict(sessionID) + serverSync().homeSessions.remove(sessionID) + navigateAfterRemoval(sessionID, session.parentID, nextSession?.id) + notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] }) + }) + .catch((err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err, language.t("common.requestFailed")), + }) + }) + } + + return { archive, navigateAfterRemoval } +} diff --git a/packages/app/src/pages/session/session-layout.ts b/packages/app/src/pages/session/session-layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..1adf6c1a96740edbc57bf9052d672225feec7918 --- /dev/null +++ b/packages/app/src/pages/session/session-layout.ts @@ -0,0 +1,30 @@ +import { useParams } from "@solidjs/router" +import { createMemo } from "solid-js" +import { useLayout } from "@/context/layout" +import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope" +import { useSDK } from "@/context/sdk" +import { useServerSDK } from "@/context/server-sdk" +import { base64Encode } from "@opencode-ai/core/util/encode" + +export const useSessionKey = () => { + const params = useParams() + const sdk = useSDK() + const serverSDK = useServerSDK() + const scope = createMemo(() => serverSDK().scope) + const directory = createMemo(() => base64Encode(sdk().directory)) + const workspaceKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(directory()))) + const sessionKey = createMemo(() => SessionStateKey.from(scope(), SessionRouteKey.fromRoute(directory(), params.id))) + return { params, sessionKey, workspaceKey } +} + +export const useSessionLayout = () => { + const layout = useLayout() + const { params, sessionKey, workspaceKey } = useSessionKey() + return { + params, + sessionKey, + workspaceKey, + tabs: createMemo(() => layout.tabs(sessionKey)), + view: createMemo(() => layout.view(sessionKey)), + } +} diff --git a/packages/app/src/pages/session/session-lineage.ts b/packages/app/src/pages/session/session-lineage.ts new file mode 100644 index 0000000000000000000000000000000000000000..d86c7b657fa8be0e6526c035cdf356bc8afc82e4 --- /dev/null +++ b/packages/app/src/pages/session/session-lineage.ts @@ -0,0 +1,69 @@ +import { createEffect, createMemo, createSignal, on, onCleanup } from "solid-js" +import { sessionNotFoundError } from "@/utils/server-errors" + +type LineageStore = { peek: (id: string) => T | undefined; resolve: (id: string) => Promise } + +type Resolution = { id: string; store: LineageStore } & ( + | { state: "pending" } + | { state: "settled" } + | { state: "failed"; failure: unknown } +) + +// Reactive session lineage for the target session route, read from the sync store. +// All session tabs on a server share one route instance, so the target session ID +// changes in place; the effect is only a trigger that starts resolution for the +// current target, and each run cancels the previous one through onCleanup so a +// late result from an abandoned target is dropped. Resolution is imperative rather +// than a resource on purpose: a resource created here would be created inside the +// router's navigation transition, and suspending that transition deadlocks the URL +// commit and double-mounts the session header portals from the transition's shadow +// render. +// +// The returned accessor is a pure derivation. The sync cache is authoritative, and +// status only applies while it matches the current target (store + session ID): on +// navigation or store replacement the memo re-evaluates before the trigger runs, +// so trusting a previous target's settlement would fabricate a not-found for a +// session that simply has not resolved yet. Resolve failures rethrow on read so +// the enclosing SessionRouteErrorBoundary renders the scoped session error. +export function createSessionLineage(sessionID: () => string, lineage: () => LineageStore) { + const cached = createMemo(() => lineage().peek(sessionID())) + const [status, setStatus] = createSignal>() + + createEffect( + on([sessionID, lineage] as const, ([id, store]) => { + let stale = false + onCleanup(() => { + stale = true + }) + if (cached()) { + setStatus({ id, store, state: "settled" }) + return + } + setStatus({ id, store, state: "pending" }) + store + .resolve(id) + .then(() => { + if (!stale) setStatus({ id, store, state: "settled" }) + }) + .catch((failure) => { + if (!stale) setStatus({ id, store, state: "failed", failure }) + }) + }), + ) + + return createMemo(() => { + const id = sessionID() + const value = cached() + if (value) return value + const state = status() + if (state?.id !== id || state.store !== lineage()) return undefined + if (state.state === "failed") throw state.failure + // The viewed session is pinned (DirectoryDataProvider, directory-layout.tsx) + // and pinned lineages are exempt from cache pruning, so a lineage missing + // after settlement means the session (or an ancestor) was deleted, possibly + // by another client. Match the resolve error so the boundary shows the + // session not found fallback. + if (state.state === "settled") throw sessionNotFoundError(id) + return undefined + }) +} diff --git a/packages/app/src/pages/session/session-model-helpers.test.ts b/packages/app/src/pages/session/session-model-helpers.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..201627b3691e8887f2fab6e6c39bde428bd7c11e --- /dev/null +++ b/packages/app/src/pages/session/session-model-helpers.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, test } from "bun:test" +import type { UserMessage } from "@opencode-ai/sdk/v2" +import { resetSessionModel, restorePromptModel, syncPromptModel, syncSessionModel } from "./session-model-helpers" + +const message = (input?: { agent?: string; model?: UserMessage["model"] }) => + ({ + id: "msg", + sessionID: "session", + role: "user", + time: { created: 1 }, + agent: input?.agent ?? "build", + model: input?.model ?? { providerID: "anthropic", modelID: "claude-sonnet-4" }, + }) as UserMessage + +describe("syncSessionModel", () => { + test("restores the last message through session state", () => { + const calls: unknown[] = [] + + syncSessionModel( + { + session: { + restore(value) { + calls.push(value) + }, + reset() {}, + }, + }, + message({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }), + ) + + expect(calls).toEqual([ + message({ model: { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } }), + ]) + }) +}) + +describe("resetSessionModel", () => { + test("clears draft session state", () => { + const calls: string[] = [] + + resetSessionModel({ + session: { + reset() { + calls.push("reset") + }, + restore() {}, + }, + }) + + expect(calls).toEqual(["reset"]) + }) +}) + +describe("syncPromptModel", () => { + test("stores the effective session model in prompt state", () => { + const calls: unknown[] = [] + + syncPromptModel( + { + model: { + current: () => ({ id: "claude-sonnet-4", provider: { id: "anthropic" } }), + set() {}, + variant: { current: () => "high", set() {} }, + }, + }, + { + model: { + current: () => undefined, + set: (model) => calls.push(model), + }, + }, + ) + + expect(calls).toEqual([{ providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" }]) + }) + + test("does not rewrite an unchanged prompt model", () => { + const calls: unknown[] = [] + const model = { providerID: "anthropic", modelID: "claude-sonnet-4", variant: "high" } + + syncPromptModel( + { + model: { + current: () => ({ id: model.modelID, provider: { id: model.providerID } }), + set() {}, + variant: { current: () => model.variant, set() {} }, + }, + }, + { + model: { + current: () => model, + set: (value) => calls.push(value), + }, + }, + ) + + expect(calls).toEqual([]) + }) +}) + +describe("restorePromptModel", () => { + test("restores the persisted prompt model into session selection", () => { + const calls: unknown[] = [] + const restored = restorePromptModel( + { + model: { + current: () => ({ id: "gpt", provider: { id: "openai" } }), + set: (model) => calls.push(model), + variant: { + current: () => undefined, + set: (variant) => calls.push(variant), + }, + }, + }, + { + model: { + current: () => ({ providerID: "anthropic", modelID: "claude", variant: "high" }), + set() {}, + }, + }, + ) + + expect(restored).toBe(true) + expect(calls).toEqual([{ providerID: "anthropic", modelID: "claude" }, "high"]) + }) + + test("does nothing without a persisted prompt model", () => { + const calls: unknown[] = [] + const restored = restorePromptModel( + { + model: { + current: () => ({ id: "gpt", provider: { id: "openai" } }), + set: (model) => calls.push(model), + variant: { + current: () => undefined, + set: (variant) => calls.push(variant), + }, + }, + }, + { + model: { + current: () => undefined, + set() {}, + }, + }, + ) + + expect(restored).toBe(false) + expect(calls).toEqual([]) + }) +}) diff --git a/packages/app/src/pages/session/session-model-helpers.ts b/packages/app/src/pages/session/session-model-helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..f2aadff27d84cacba916f0bf4dba7592ca3e1930 --- /dev/null +++ b/packages/app/src/pages/session/session-model-helpers.ts @@ -0,0 +1,63 @@ +import type { UserMessage } from "@opencode-ai/sdk/v2" + +type Local = { + session: { + reset(): void + restore(msg: UserMessage): void + } +} + +type ModelSelection = { + model: { + current(): { id: string; provider: { id: string } } | undefined + set(model: { providerID: string; modelID: string }): void + variant: { + current(): string | undefined + set(variant: string | undefined): void + } + } +} + +type PromptState = { + model: { + current(): { providerID: string; modelID: string; variant?: string | null } | undefined + set(model: { providerID: string; modelID: string; variant?: string | null }): void + } +} + +export const resetSessionModel = (local: Local) => { + local.session.reset() +} + +export const syncSessionModel = (local: Local, msg: UserMessage) => { + local.session.restore(msg) +} + +export const syncPromptModel = (local: ModelSelection, prompt: PromptState) => { + const model = local.model.current() + if (!model) return + const next = { + providerID: model.provider.id, + modelID: model.id, + variant: local.model.variant.current(), + } + const current = prompt.model.current() + if (current?.providerID === next.providerID && current.modelID === next.modelID && current.variant === next.variant) + return + prompt.model.set(next) +} + +export const restorePromptModel = (local: ModelSelection, prompt: PromptState) => { + const model = prompt.model.current() + if (!model) return false + const current = local.model.current() + if ( + current?.provider.id === model.providerID && + current.id === model.modelID && + local.model.variant.current() === (model.variant ?? undefined) + ) + return true + local.model.set({ providerID: model.providerID, modelID: model.modelID }) + local.model.variant.set(model.variant ?? undefined) + return true +} diff --git a/packages/app/src/pages/session/session-ownership.ts b/packages/app/src/pages/session/session-ownership.ts new file mode 100644 index 0000000000000000000000000000000000000000..831b5faaf01141dfe5184c469fe82e1f1601799f --- /dev/null +++ b/packages/app/src/pages/session/session-ownership.ts @@ -0,0 +1,37 @@ +import { createComputed, onCleanup } from "solid-js" + +export function createSessionOwnership(sessionKey: () => string) { + let current = sessionKey() + let generation = 0 + const transition = () => { + const next = sessionKey() + if (next === current) return + current = next + generation++ + } + createComputed(transition) + onCleanup(() => generation++) + + return { + key: () => { + transition() + return `${generation}:${current}` + }, + capture() { + transition() + const captured = generation + return { + key: `${captured}:${current}`, + current: () => { + transition() + return generation === captured + }, + run(action: () => T) { + transition() + if (generation !== captured) return + return action() + }, + } + }, + } +} diff --git a/packages/app/src/pages/session/session-panel-layout.test.ts b/packages/app/src/pages/session/session-panel-layout.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..499dd7411a0bacb3a22caecf256fa12b4cab6910 --- /dev/null +++ b/packages/app/src/pages/session/session-panel-layout.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "bun:test" +import { sessionPanelLayout } from "./session-panel-layout" + +describe("sessionPanelLayout", () => { + test("keeps one V2 owner while changing panel geometry", () => { + expect(sessionPanelLayout({ review: false, terminal: false, files: false })).toEqual({ + visible: false, + stacked: false, + }) + expect(sessionPanelLayout({ review: false, terminal: true, files: false })).toEqual({ + visible: true, + stacked: false, + }) + expect(sessionPanelLayout({ review: true, terminal: true, files: false })).toEqual({ + visible: true, + stacked: true, + }) + }) +}) diff --git a/packages/app/src/pages/session/session-panel-layout.ts b/packages/app/src/pages/session/session-panel-layout.ts new file mode 100644 index 0000000000000000000000000000000000000000..49feaebedeefc25d72df022d08f1ee555972705f --- /dev/null +++ b/packages/app/src/pages/session/session-panel-layout.ts @@ -0,0 +1,6 @@ +export function sessionPanelLayout(input: { review: boolean; terminal: boolean; files: boolean }) { + return { + visible: input.review || input.terminal || input.files, + stacked: input.review && input.terminal, + } +} diff --git a/packages/app/src/pages/session/session-panel-width.test.ts b/packages/app/src/pages/session/session-panel-width.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e2623775fe9017d66009da0194bd97fe398c550 --- /dev/null +++ b/packages/app/src/pages/session/session-panel-width.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test" +import { + clampSessionPanelWidth, + REVIEW_PANE_WIDTH_MIN, + REVIEW_PANE_WIDTH_MIN_SPLIT, + SESSION_PANEL_WIDTH_MIN, + sessionPanelWidthMax, +} from "./session-panel-width" + +describe("sessionPanelWidthMax", () => { + test("reserves the unified review pane minimum", () => { + expect(sessionPanelWidthMax({ available: 1700, split: false })).toBe(1700 - REVIEW_PANE_WIDTH_MIN) + }) + + test("reserves a larger minimum for split diffs", () => { + expect(sessionPanelWidthMax({ available: 1700, split: true })).toBe(1700 - REVIEW_PANE_WIDTH_MIN_SPLIT) + expect(REVIEW_PANE_WIDTH_MIN_SPLIT).toBeGreaterThan(REVIEW_PANE_WIDTH_MIN) + }) + + test("lets the chat panel take everything beyond the review pane minimum", () => { + // Regression: the old cap was 45% of the window, forcing the review pane + // to at least 55% of the window regardless of content. + const available = 3440 + expect(sessionPanelWidthMax({ available, split: false })).toBeGreaterThan(available * 0.45) + }) + + test("never drops below the chat panel minimum on small windows", () => { + expect(sessionPanelWidthMax({ available: 600, split: true })).toBe(SESSION_PANEL_WIDTH_MIN) + expect(sessionPanelWidthMax({ available: 0, split: false })).toBe(SESSION_PANEL_WIDTH_MIN) + }) +}) + +describe("clampSessionPanelWidth", () => { + test("keeps widths already within the limit", () => { + expect(clampSessionPanelWidth({ width: 800, available: 1700, split: false })).toBe(800) + }) + + test("forces the width down when the window shrinks", () => { + expect(clampSessionPanelWidth({ width: 1600, available: 1700, split: false })).toBe(1700 - REVIEW_PANE_WIDTH_MIN) + expect(clampSessionPanelWidth({ width: 1600, available: 1700, split: true })).toBe( + 1700 - REVIEW_PANE_WIDTH_MIN_SPLIT, + ) + }) + + test("holds the chat panel minimum when there is no room for both", () => { + expect(clampSessionPanelWidth({ width: 1600, available: 700, split: true })).toBe(SESSION_PANEL_WIDTH_MIN) + }) + + test("skips clamping before the layout is measured", () => { + expect(clampSessionPanelWidth({ width: 1600, available: undefined, split: false })).toBe(1600) + }) +}) diff --git a/packages/app/src/pages/session/session-panel-width.ts b/packages/app/src/pages/session/session-panel-width.ts new file mode 100644 index 0000000000000000000000000000000000000000..d04646b483996927fb7c9691dc98fcecb5232a83 --- /dev/null +++ b/packages/app/src/pages/session/session-panel-width.ts @@ -0,0 +1,19 @@ +// The review pane has no width of its own: it takes whatever the chat panel +// leaves behind. Instead of capping the chat panel at a fraction of the window +// (which forces the review pane to grow with the monitor), reserve a fixed +// minimum for the review pane and let the chat panel take everything else. +export const SESSION_PANEL_WIDTH_MIN = 450 +export const REVIEW_PANE_WIDTH_MIN = 480 +export const REVIEW_PANE_WIDTH_MIN_SPLIT = 800 + +export function sessionPanelWidthMax(input: { available: number; split: boolean }) { + const pane = input.split ? REVIEW_PANE_WIDTH_MIN_SPLIT : REVIEW_PANE_WIDTH_MIN + return Math.max(SESSION_PANEL_WIDTH_MIN, input.available - pane) +} + +// `available` is undefined until the layout row is first measured; render the +// stored width untouched until then to avoid a first-frame snap. +export function clampSessionPanelWidth(input: { width: number; available: number | undefined; split: boolean }) { + if (input.available === undefined) return input.width + return Math.min(input.width, sessionPanelWidthMax({ available: input.available, split: input.split })) +} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6f074fa20304a46b023afbac68c934bd16b8a04c --- /dev/null +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -0,0 +1,867 @@ +import { For, Match, Show, Switch, createEffect, createMemo, onCleanup, type JSX } from "solid-js" +import { createStore } from "solid-js/store" +import { createMediaQuery } from "@solid-primitives/media" +import { DragDropProvider as DndKitProvider, PointerSensor } from "@dnd-kit/solid" +import { isSortable } from "@dnd-kit/solid/sortable" +import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" +import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers" +import { RestrictToElement } from "@dnd-kit/dom/modifiers" +import { + DragDropProvider, + DragDropSensors, + DragOverlay, + SortableProvider, + closestCenter, + type DragEvent, +} from "@thisbeyond/solid-dnd" +import { Tabs } from "@opencode-ai/ui/tabs" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Icon } from "@opencode-ai/ui/icon" +import { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { Mark } from "@opencode-ai/ui/logo" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" +import type { FileDiffInfo } from "@opencode-ai/client/promise" +import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" +import { useDialog } from "@opencode-ai/ui/context/dialog" + +import FileTree from "@/components/file-tree" +import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" +import { SessionContextUsage } from "@/components/session-context-usage" + +const reviewTabID = "session-side-panel-review-tab" +const reviewTabPanelID = "session-side-panel-review-tabpanel" +const fileBrowserTabPanelID = "session-side-panel-file-browser-tabpanel" +import { SessionContextTab, SortableTab, SortableTabV2, FileVisual } from "@/components/session" +import { OpenInAppV2 } from "@/components/session/open-in-app-v2" +import { useCommand } from "@/context/command" +import { useFile, type SelectedLineRange } from "@/context/file" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useSDK } from "@/context/sdk" +import { useSettings } from "@/context/settings" +import { createFileTabListSync } from "@/pages/session/file-tab-scroll" +import { FileTabContent } from "@/pages/session/file-tabs" +import { + SESSION_OPEN_FILE_TAB, + createOpenSessionFileTab, + createSessionTabs, + getTabReorderIndex, + shouldShowFileTree, + type Sizing, +} from "@/pages/session/helpers" +import { setSessionHandoff } from "@/pages/session/handoff" +import { useSessionLayout } from "@/pages/session/session-layout" +import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab" + +type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff +const FILE_TREE_WIDTH_MIN = 240 + +function renderDiff(value: ReviewDiff): value is RenderDiff { + return typeof value.file === "string" +} + +export function SessionSidePanel(props: { + canReview: () => boolean + diffs: () => ReviewDiff[] + diffsReady: () => boolean + empty: () => string + hasReview: () => boolean + reviewHasFocusableContent: () => boolean + reviewCount: () => number + reviewPanel: () => JSX.Element + reviewSidebarToggle?: (disabled: boolean) => JSX.Element + fileBrowserState?: SessionFileBrowserState + activeDiff?: string + focusReviewDiff: (path: string) => void + reviewSnap: boolean + size: Sizing + stacked?: boolean +}) { + const layout = useLayout() + const settings = useSettings() + const file = useFile() + const language = useLanguage() + const command = useCommand() + const dialog = useDialog() + const sdk = useSDK() + const { sessionKey, tabs, view, params } = useSessionLayout() + const projectDirectory = createMemo(() => sdk().directory) + + const isDesktop = createMediaQuery("(min-width: 768px)") + const shown = settings.visibility.fileTree + + const reviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) + const fileOpen = createMemo( + () => + isDesktop() && + shouldShowFileTree({ + visible: shown(), + opened: layout.fileTree.opened(), + }), + ) + const open = createMemo(() => reviewOpen() || fileOpen()) + const fileTreeWidth = createMemo(() => Math.max(FILE_TREE_WIDTH_MIN, layout.fileTree.width())) + const reviewTab = createMemo(() => isDesktop()) + const panelWidth = createMemo(() => { + if (!open()) return "0px" + if (reviewOpen()) return "auto" + return `${fileTreeWidth()}px` + }) + const treeWidth = createMemo(() => (fileOpen() ? `${fileTreeWidth()}px` : "0px")) + + const diffs = createMemo(() => props.diffs().filter(renderDiff)) + const diffFiles = createMemo(() => diffs().map((d) => d.file)) + const kinds = createMemo(() => { + const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => { + if (!a) return b + if (a === b) return a + return "mix" as const + } + + const out = new Map() + for (const diff of diffs()) { + const file = normalizeFileTreeV2Path(diff.file) + const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix" + + out.set(file, kind) + + const parts = file.split("/") + for (const [idx] of parts.slice(0, -1).entries()) { + const dir = parts.slice(0, idx + 1).join("/") + if (!dir) continue + out.set(dir, merge(out.get(dir), kind)) + } + } + return out + }) + + const empty = (msg: string) => ( +
+
+
+
{msg}
+
+
+ ) + + const nofiles = createMemo(() => { + const state = file.tree.state("") + if (!state?.loaded) return false + return file.tree.children("").length === 0 + }) + + const normalizeTab = (tab: string) => { + if (!tab.startsWith("file://")) return tab + return file.tab(tab) + } + + const openReviewPanel = () => { + if (!view().reviewPanel.opened()) view().reviewPanel.open() + } + + const openTab = createOpenSessionFileTab({ + normalizeTab, + openTab: tabs().open, + pathFromTab: file.pathFromTab, + loadFile: file.load, + openReviewPanel, + setActive: tabs().setActive, + }) + + const tabState = createSessionTabs({ + tabs, + pathFromTab: file.pathFromTab, + normalizeTab, + review: reviewTab, + hasReview: props.canReview, + fileBrowser: () => !!props.fileBrowserState, + }) + const contextOpen = tabState.contextOpen + const openFileOpen = tabState.openFileOpen + const panelTabs = tabState.panelTabs + const openedTabs = tabState.openedTabs + const activeTab = tabState.activeTab + const activeFileTab = tabState.activeFileTab + + const fileTreeTab = () => layout.fileTree.tab() + + const setFileTreeTabValue = (value: string) => { + if (value !== "changes" && value !== "all") return + layout.fileTree.setTab(value) + } + + const showAllFiles = () => { + if (fileTreeTab() !== "changes") return + layout.fileTree.setTab("all") + } + + let fileFilter: HTMLInputElement | undefined + let tabList: HTMLDivElement | undefined + const temporaryTab = tabs().preview + const previewTab = (value: string) => { + const next = normalizeTab(value) + tabs().previewTab(next) + const path = file.pathFromTab(next) + if (path) void file.load(path) + openReviewPanel() + queueMicrotask(() => tabs().setActive(next)) + } + const openFileBrowser = () => { + previewTab(SESSION_OPEN_FILE_TAB) + queueMicrotask(() => fileFilter?.focus()) + } + const activateTab = (value: string) => { + const next = normalizeTab(value) + const path = file.pathFromTab(next) + if (path) void file.load(path) + openReviewPanel() + tabs().setActive(next) + } + const browserTab = createMemo(() => { + if (!props.fileBrowserState) return undefined + const active = activeTab() + if (active === SESSION_OPEN_FILE_TAB) return SESSION_OPEN_FILE_TAB + if (active && file.pathFromTab(active)) return active + return activeFileTab() + }) + // Keep the file-browser shell mounted while any file tab exists. Kobalte briefly + // selects Review while the tab For replaces a preview trigger, which would + // otherwise dispose the sidebar and reset scroll. + const fileBrowserMounted = createMemo(() => { + if (!props.fileBrowserState) return false + return openedTabs().length > 0 || openFileOpen() || !!browserTab() + }) + const fileBrowserVisible = createMemo(() => { + const active = activeTab() + return active !== "review" && active !== "context" && active !== "empty" + }) + const openFileKeybind = createMemo(() => command.keybindParts("file.open")) + const closeTabKeybind = createMemo(() => command.keybindParts("tab.close")) + const [store, setStore] = createStore({ + activeDraggable: undefined as string | undefined, + }) + + const handleDragStart = (event: unknown) => { + const id = getDraggableId(event) + if (!id) return + setStore("activeDraggable", id) + } + + const handleDragOver = (event: DragEvent) => { + const { draggable, droppable } = event + if (!draggable || !droppable) return + + const currentTabs = tabs().all() + const toIndex = getTabReorderIndex(currentTabs, draggable.id.toString(), droppable.id.toString()) + if (toIndex === undefined) return + tabs().move(draggable.id.toString(), toIndex) + } + + const handleDragEnd = () => { + setStore("activeDraggable", undefined) + } + + createEffect(() => { + if (!file.ready()) return + + setSessionHandoff(sessionKey(), { + files: tabs() + .all() + .reduce>((acc, tab) => { + const path = file.pathFromTab(tab) + if (!path) return acc + + const selected = file.selectedLines(path) + acc[path] = + selected && typeof selected === "object" && "start" in selected && "end" in selected + ? (selected as SelectedLineRange) + : null + + return acc + }, {}), + }) + }) + + return ( + +
+ + + + ) +} diff --git a/packages/app/src/pages/session/terminal-label.ts b/packages/app/src/pages/session/terminal-label.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a34712e4363b6ce15786f9d0cee58cefeae783f --- /dev/null +++ b/packages/app/src/pages/session/terminal-label.ts @@ -0,0 +1,16 @@ +import { isDefaultTitle as isDefaultTerminalTitle } from "@/context/terminal-title" + +export const terminalTabLabel = (input: { + title?: string + titleNumber?: number + t: (key: string, vars?: Record) => string +}) => { + const title = input.title ?? "" + const number = input.titleNumber ?? 0 + const isDefaultTitle = Number.isFinite(number) && number > 0 && isDefaultTerminalTitle(title, number) + + if (title && !isDefaultTitle) return title + if (number > 0) return input.t("terminal.title.numbered", { number }) + if (title) return title + return input.t("terminal.title") +} diff --git a/packages/app/src/pages/session/terminal-panel-v2.tsx b/packages/app/src/pages/session/terminal-panel-v2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6a92e24ea6142beaeb1260f16b6f6a2df4b13830 --- /dev/null +++ b/packages/app/src/pages/session/terminal-panel-v2.tsx @@ -0,0 +1,352 @@ +import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" +import { createMediaQuery } from "@solid-primitives/media" +import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" +import { isSortable } from "@dnd-kit/solid/sortable" +import { Accessibility, AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" +import { RestrictToHorizontalAxis } from "@dnd-kit/abstract/modifiers" +import { RestrictToElement } from "@dnd-kit/dom/modifiers" +import { Tabs } from "@opencode-ai/ui/tabs" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" +import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" + +import { SortableTerminalTabV2 } from "@/components/session/session-sortable-terminal-tab-v2" +import { Terminal } from "@/components/terminal" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useSettings } from "@/context/settings" +import { useTerminal } from "@/context/terminal" +import { useSDK } from "@/context/sdk" +import { terminalTabLabel } from "@/pages/session/terminal-label" +import { createSizing, focusTerminalById } from "@/pages/session/helpers" +import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" +import { useSessionLayout } from "@/pages/session/session-layout" + +export function TerminalPanelV2(props: { stacked?: boolean } = {}) { + const layout = useLayout() + const terminal = useTerminal() + const sdk = useSDK() + const language = useLanguage() + const command = useCommand() + const settings = useSettings() + const { workspaceKey, view } = useSessionLayout() + + const isDesktop = createMediaQuery("(min-width: 768px)") + const newLayout = createMemo(() => settings.general.newLayoutDesigns()) + const opened = createMemo(() => view().terminal.opened()) + const size = createSizing() + const height = createMemo(() => layout.terminal.height()) + const close = () => view().terminal.close() + let root: HTMLDivElement | undefined + let tabList: HTMLDivElement | undefined + + onCleanup(() => terminal.cancelFocus()) + + const [store, setStore] = createStore({ + autoCreated: false, + recovered: {} as Record, + view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight), + }) + + const max = () => store.view * 0.6 + const pane = () => Math.min(height(), max()) + const stacked = createMemo(() => isDesktop() && props.stacked) + const panelHeight = createMemo(() => + isDesktop() ? (stacked() ? `${pane()}px` : "100%") : opened() ? `${pane()}px` : "0px", + ) + const contentHeight = createMemo(() => (isDesktop() ? (stacked() ? `${pane()}px` : "100%") : `${pane()}px`)) + const newTerminalKeybind = createMemo(() => command.keybindParts("terminal.new")) + + onMount(() => { + if (typeof window === "undefined") return + + const sync = () => setStore("view", window.visualViewport?.height ?? window.innerHeight) + const port = window.visualViewport + + sync() + makeEventListener(window, "resize", sync) + if (port) makeEventListener(port, "resize", sync) + }) + + createEffect(() => { + if (!opened()) { + setStore("autoCreated", false) + return + } + + if (!terminal.ready() || terminal.all().length !== 0 || store.autoCreated) return + terminal.new() + setStore("autoCreated", true) + }) + + createEffect( + on( + () => terminal.all().length, + (count, prevCount) => { + if (prevCount === undefined || prevCount <= 0 || count !== 0) return + if (!opened()) return + close() + }, + ), + ) + + createEffect( + on( + () => [opened(), terminal.active(), terminal.focusRequested(terminal.active())] as const, + ([next, id, requested]) => { + if (!next || !id || !requested) return + focusTerminalById(id) + }, + ), + ) + + createEffect(() => { + if (opened()) return + const active = document.activeElement + if (!(active instanceof HTMLElement)) return + if (!root?.contains(active)) return + active.blur() + }) + + createEffect(() => { + const dir = sdk().directory + if (!dir) return + if (!terminal.ready()) return + language.locale() + + setTerminalHandoff( + workspaceKey(), + terminal.all().map((pty) => + terminalTabLabel({ + title: pty.title, + titleNumber: pty.titleNumber, + t: language.t as (key: string, vars?: Record) => string, + }), + ), + ) + }) + + const handoff = createMemo(() => { + const dir = sdk().directory + if (!dir) return [] + return getTerminalHandoff(workspaceKey()) ?? [] + }) + + const all = terminal.all + const ids = createMemo(() => all().map((pty) => pty.id)) + + const recoverTerminal = (key: string, id: string, clone: (id: string) => Promise) => { + if (store.recovered[key]) return + setStore("recovered", key, true) + void clone(id) + } + + const terminalRecoveryKey = (pty: { id: string; title: string; titleNumber: number }) => { + return String(pty.titleNumber || pty.title || pty.id) + } + + const markTerminalConnected = (key: string, id: string, trim: (id: string) => void) => { + setStore("recovered", key, false) + trim(id) + } + + const handleTerminalDragEnd = () => { + const activeId = terminal.active() + if (!activeId) return + requestAnimationFrame(() => { + if (terminal.active() !== activeId) return + focusTerminalById(activeId) + }) + } + + return ( + + ) +} diff --git a/packages/app/src/pages/session/terminal-panel.test.ts b/packages/app/src/pages/session/terminal-panel.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..43eeec32f214983b1a7d9552669357d25a0efca9 --- /dev/null +++ b/packages/app/src/pages/session/terminal-panel.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from "bun:test" +import { terminalTabLabel } from "./terminal-label" + +const t = (key: string, vars?: Record) => { + if (key === "terminal.title.numbered") return `Terminal ${vars?.number}` + if (key === "terminal.title") return "Terminal" + return key +} + +describe("terminalTabLabel", () => { + test("returns custom title unchanged", () => { + const label = terminalTabLabel({ title: "server", titleNumber: 3, t }) + expect(label).toBe("server") + }) + + test("normalizes default numbered title", () => { + const label = terminalTabLabel({ title: "Terminal 2", titleNumber: 2, t }) + expect(label).toBe("Terminal 2") + }) + + test("falls back to generic title", () => { + const label = terminalTabLabel({ title: "", titleNumber: 0, t }) + expect(label).toBe("Terminal") + }) +}) diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2596eaee83f014dcf02e3295140c898653ec17f4 --- /dev/null +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -0,0 +1,343 @@ +import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js" +import { createStore } from "solid-js/store" +import { makeEventListener } from "@solid-primitives/event-listener" +import { Tabs } from "@opencode-ai/ui/tabs" +import { ResizeHandle } from "@opencode-ai/ui/resize-handle" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" +import type { DragEvent } from "@thisbeyond/solid-dnd" +import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" + +import { SortableTerminalTab } from "@/components/session" +import { Terminal } from "@/components/terminal" +import { useCommand } from "@/context/command" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useSettings } from "@/context/settings" +import { useTerminal } from "@/context/terminal" +import { useSDK } from "@/context/sdk" +import { terminalTabLabel } from "@/pages/session/terminal-label" +import { createSizing, focusTerminalById } from "@/pages/session/helpers" +import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff" +import { useSessionLayout } from "@/pages/session/session-layout" + +export function TerminalPanel() { + const delays = [120, 240] + const layout = useLayout() + const terminal = useTerminal() + const sdk = useSDK() + const language = useLanguage() + const command = useCommand() + const settings = useSettings() + const { workspaceKey, view } = useSessionLayout() + + const opened = createMemo(() => view().terminal.opened()) + const size = createSizing() + const height = createMemo(() => layout.terminal.height()) + const close = () => view().terminal.close() + let root: HTMLDivElement | undefined + + onCleanup(() => terminal.cancelFocus()) + + const [store, setStore] = createStore({ + autoCreated: false, + activeDraggable: undefined as string | undefined, + recovered: {} as Record, + view: typeof window === "undefined" ? 1000 : (window.visualViewport?.height ?? window.innerHeight), + }) + + const max = () => store.view * 0.6 + const pane = () => Math.min(height(), max()) + + onMount(() => { + if (typeof window === "undefined") return + + const sync = () => setStore("view", window.visualViewport?.height ?? window.innerHeight) + const port = window.visualViewport + + sync() + makeEventListener(window, "resize", sync) + if (port) makeEventListener(port, "resize", sync) + }) + + createEffect(() => { + if (!opened()) { + setStore("autoCreated", false) + return + } + + if (!terminal.ready() || terminal.all().length !== 0 || store.autoCreated) return + terminal.new() + setStore("autoCreated", true) + }) + + createEffect( + on( + () => terminal.all().length, + (count, prevCount) => { + if (prevCount === undefined || prevCount <= 0 || count !== 0) return + if (!opened()) return + close() + }, + ), + ) + + const focus = (id: string) => { + focusTerminalById(id) + + const frame = requestAnimationFrame(() => { + if (!opened()) return + if (terminal.active() !== id) return + focusTerminalById(id) + }) + + const timers = delays.map((ms) => + window.setTimeout(() => { + if (!opened()) return + if (terminal.active() !== id) return + focusTerminalById(id) + }, ms), + ) + + return () => { + cancelAnimationFrame(frame) + for (const timer of timers) clearTimeout(timer) + } + } + + createEffect( + on( + () => [opened(), terminal.active()] as const, + ([next, id]) => { + if (!next || !id) return + const stop = focus(id) + onCleanup(stop) + }, + ), + ) + + createEffect(() => { + if (opened()) return + const active = document.activeElement + if (!(active instanceof HTMLElement)) return + if (!root?.contains(active)) return + active.blur() + }) + + createEffect(() => { + const dir = sdk().directory + if (!dir) return + if (!terminal.ready()) return + language.locale() + + setTerminalHandoff( + workspaceKey(), + terminal.all().map((pty) => + terminalTabLabel({ + title: pty.title, + titleNumber: pty.titleNumber, + t: language.t as (key: string, vars?: Record) => string, + }), + ), + ) + }) + + const handoff = createMemo(() => { + const dir = sdk().directory + if (!dir) return [] + return getTerminalHandoff(workspaceKey()) ?? [] + }) + + const all = terminal.all + const ids = createMemo(() => all().map((pty) => pty.id)) + + const recoverTerminal = (key: string, id: string, clone: (id: string) => Promise) => { + if (store.recovered[key]) return + setStore("recovered", key, true) + void clone(id) + } + + const terminalRecoveryKey = (pty: { id: string; title: string; titleNumber: number }) => { + return String(pty.titleNumber || pty.title || pty.id) + } + + const markTerminalConnected = (key: string, id: string, trim: (id: string) => void) => { + setStore("recovered", key, false) + trim(id) + } + + const handleTerminalDragStart = (event: unknown) => { + const id = getDraggableId(event) + if (!id) return + setStore("activeDraggable", id) + } + + const handleTerminalDragOver = (event: DragEvent) => { + const { draggable, droppable } = event + if (!draggable || !droppable) return + + const terminals = terminal.all() + const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString()) + const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString()) + if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) { + terminal.move(draggable.id.toString(), toIndex) + } + } + + const handleTerminalDragEnd = () => { + setStore("activeDraggable", undefined) + + const activeId = terminal.active() + if (!activeId) return + requestAnimationFrame(() => { + if (terminal.active() !== activeId) return + focusTerminalById(activeId) + }) + } + + return ( +
+ +
+ +
+ + {(title) => ( +
+ {title} +
+ )} +
+
+
+ {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+
+
{language.t("terminal.loading")}
+
+ } + > + + + +
+ terminal.open(id)} + class="!h-auto !flex-none" + > + + + {(pty) => } + +
+ + terminal.new()} + aria-label={language.t("command.terminal.new")} + /> + +
+
+
+
+ + {(id) => { + const ops = terminal.bind() + return ( + pty.id === id)}> + {(pty) => ( +
+ terminal.consumeFocus(id)} + onConnect={() => markTerminalConnected(terminalRecoveryKey(pty()), id, ops.trim)} + onCleanup={ops.update} + onConnectError={() => recoverTerminal(terminalRecoveryKey(pty()), id, ops.clone)} + /> +
+ )} +
+ ) + }} +
+
+
+ + + {(id) => ( + pty.id === id)}> + {(t) => ( +
+ {terminalTabLabel({ + title: t().title, + titleNumber: t().titleNumber, + t: language.t as (key: string, vars?: Record) => string, + })} +
+ )} +
+ )} +
+
+
+
+
+
+ ) +} diff --git a/packages/app/src/pages/session/timeline/measure.test.ts b/packages/app/src/pages/session/timeline/measure.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..b66f4e161f51c202df9d1db533dd6acb4f8d8fe5 --- /dev/null +++ b/packages/app/src/pages/session/timeline/measure.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "bun:test" +import { scheduleConnectedMeasure } from "./measure" + +test("does not measure an element detached before the frame", async () => { + const element = document.createElement("div") + document.body.append(element) + let calls = 0 + + scheduleConnectedMeasure(element, () => { + calls += 1 + }) + element.remove() + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + + expect(calls).toBe(0) +}) + +test("measures a connected element on the next frame", async () => { + const element = document.createElement("div") + document.body.append(element) + let calls = 0 + + scheduleConnectedMeasure(element, () => { + calls += 1 + }) + await new Promise((resolve) => requestAnimationFrame(() => resolve())) + + expect(calls).toBe(1) + element.remove() +}) diff --git a/packages/app/src/pages/session/timeline/measure.ts b/packages/app/src/pages/session/timeline/measure.ts new file mode 100644 index 0000000000000000000000000000000000000000..c53df5f14558374622335c5058e55bfd3b8d4eee --- /dev/null +++ b/packages/app/src/pages/session/timeline/measure.ts @@ -0,0 +1,5 @@ +export function scheduleConnectedMeasure(element: T, measure: (element: T) => void) { + return requestAnimationFrame(() => { + if (element.isConnected) measure(element) + }) +} diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e0838825570e0e137f2dc6a188e7c6abe6fe46fa --- /dev/null +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -0,0 +1,1847 @@ +import { + createEffect, + createMemo, + createSignal, + For, + Index, + on, + onCleanup, + onMount, + Show, + type Accessor, + type JSX, +} from "solid-js" +import { createStore, produce } from "solid-js/store" +import { Dynamic } from "solid-js/web" +import { useNavigate } from "@solidjs/router" +import { useMutation } from "@tanstack/solid-query" +import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualItem } from "@tanstack/solid-virtual" +import { Accordion } from "@opencode-ai/ui/accordion" +import { Button } from "@opencode-ai/ui/button" +import { Card } from "@opencode-ai/ui/card" +import { + ContextToolGroup, + Message, + MessageDivider, + Part as MessagePart, + partDefaultOpen, + type UserActions, +} from "@opencode-ai/session-ui/message-part" +import { DiffChanges } from "@opencode-ai/ui/diff-changes" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { Icon } from "@opencode-ai/ui/icon" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { Dialog } from "@opencode-ai/ui/dialog" +import { DialogFooter, DialogHeader, DialogTitleGroup, DialogV2 } from "@opencode-ai/ui/v2/dialog-v2" +import { InlineInput } from "@opencode-ai/ui/inline-input" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { SessionRetry } from "@opencode-ai/session-ui/session-retry" +import { isScrollKeyTarget, scrollKey, scrollKeyOwner, ScrollView } from "@opencode-ai/ui/scroll-view" +import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" +import { TextField } from "@opencode-ai/ui/text-field" +import { TextReveal } from "@opencode-ai/ui/text-reveal" +import { TextShimmer } from "@opencode-ai/ui/text-shimmer" +import type { + AssistantMessage, + Message as MessageType, + Part as PartType, + ToolPart, + 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" +import { useFileComponent } from "@opencode-ai/ui/context/file" +import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" +import { SessionContextUsage } from "@/components/session-context-usage" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { useLanguage } from "@/context/language" +import { useSessionKey } from "@/pages/session/session-layout" +import { useSessionArchive } from "@/pages/session/session-archive" +import { useServerSDK } from "@/context/server-sdk" +import { usePlatform } from "@/context/platform" +import { useSettings } from "@/context/settings" +import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { notifySessionTabsRemoved } from "@/components/titlebar-session-events" +import { sessionTitle } from "@/utils/session-title" +import { scheduleConnectedMeasure } from "./measure" +import { observeElementOffsetReconnectAware } from "./observe-element-offset" +import { createTimelineProjection } from "./projection" +import { MessageComment, SummaryDiff, TimelineRow, TimelineRowMap } from "./rows" +import { filterVirtualIndexes } from "./virtual-items" + +const emptyMessages: MessageType[] = [] +const emptyParts: PartType[] = [] +const emptyTools: ToolPart[] = [] +const emptyAssistantMessages: AssistantMessage[] = [] +const idle = { type: "idle" as const } + +type FramedTimelineRow = Exclude +type TimelineRowByTag = Extract + +const timelineFallbackItemSize = 60 +const timelineCache = new Map }>() + +const taskDescription = (part: PartType, sessionID: string) => { + if (part.type !== "tool" || part.tool !== "task") return + const metadata = "metadata" in part.state ? part.state.metadata : undefined + if (metadata?.sessionId !== sessionID) return + const value = part.state.input?.description + if (typeof value === "string" && value) return value +} + +const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => { + const current = target instanceof Element ? target : undefined + const nested = current?.closest("[data-scrollable]") + if (!nested || nested === root) return root + if (!(nested instanceof HTMLElement)) return root + return nested +} + +const markBoundaryGesture = (input: { + root: HTMLDivElement + target: EventTarget | null + delta: number + onMarkScrollGesture: (target?: EventTarget | null) => void +}) => { + const target = boundaryTarget(input.root, input.target) + if (target === input.root) { + input.onMarkScrollGesture(input.root) + return + } + if ( + shouldMarkBoundaryGesture({ + delta: input.delta, + scrollTop: target.scrollTop, + scrollHeight: target.scrollHeight, + clientHeight: target.clientHeight, + }) + ) { + input.onMarkScrollGesture(input.root) + } +} + +function TimelineThinkingRow(props: { reasoningHeading?: string; showReasoningSummaries: boolean }) { + const language = useLanguage() + + return ( +
+ + + + +
+ ) +} + +function TimelineDiffSummaryRow(props: { diffs: SummaryDiff[] }) { + const language = useLanguage() + const maxFiles = 10 + const [state, setState] = createStore({ + showAll: false, + expanded: [] as string[], + }) + const showAll = () => state.showAll + const expanded = () => state.expanded + const overflow = createMemo(() => Math.max(0, props.diffs.length - maxFiles)) + const visible = createMemo(() => (showAll() ? props.diffs : props.diffs.slice(0, maxFiles))) + + return ( +
+
+ + {language.plural("ui.sessionTurn.diffs.changed", props.diffs.length)} + + + 0}> + setState("showAll", !showAll())}> + {showAll() ? language.t("ui.sessionTurn.diffs.showLess") : language.t("ui.sessionTurn.diffs.showAll")} + + +
+
+ setState("expanded", Array.isArray(value) ? value : value ? [value] : [])} + > + + {(diff) => { + const opened = createMemo(() => expanded().includes(diff.file)) + + return ( + + + +
+ + + {`\u202A${getDirectory(diff.file)}\u202C`} + + {getFilename(diff.file)} + +
+ + + + + + +
+
+
+
+ + + + + +
+ ) + }} +
+
+ 0}> +
setState("showAll", true)}> + {language.t("ui.sessionTurn.diffs.more", { count: String(overflow()) })} +
+
+
+
+ ) +} + +function TimelineDiffView(props: { diff: SummaryDiff }) { + const fileComponent = useFileComponent() + const view = normalize(props.diff) + + return ( +
+ +
+ ) +} + +export function MessageTimeline(props: { + actions?: UserActions + scroll: { overflow: boolean; bottom: boolean; jump: boolean } + onResumeScroll: () => void + setScrollRef: (el: HTMLDivElement | undefined) => void + onScheduleScrollState: (el: HTMLDivElement) => void + onAutoScrollHandleScroll: () => void + onMarkScrollGesture: (target?: EventTarget | null) => void + hasScrollGesture: () => boolean + onUserScroll: () => void + onHistoryScroll: () => void + onAutoScrollInteraction: (event: MouseEvent) => void + shouldAnchorBottom: () => boolean + centered: boolean + setContentRef: (el: HTMLDivElement) => void + userMessages: UserMessage[] + anchor: (id: string) => string + setRevealMessage?: (fn: (id: string) => void) => void + setScrollToEnd?: (fn: () => void) => void + setHistoryAnchor?: (handlers: { capture: () => void; restore: (done: boolean) => void }) => void +}) { + let touchGesture: number | undefined + + const navigate = useNavigate() + const serverSDK = useServerSDK() + const sdk = useSDK() + const sync = useSync() + const settings = useSettings() + const dialog = useDialog() + const sessionArchive = useSessionArchive() + const language = useLanguage() + const { params, sessionKey } = useSessionKey() + const ownerSessionKey = sessionKey() + const cached = timelineCache.get(ownerSessionKey) + const initialMeasurements = cached?.measurements + const coldBottomMount = !initialMeasurements?.length && props.shouldAnchorBottom() + const platform = usePlatform() + + const [listRoot, setListRoot] = createSignal() + const sessionID = createMemo(() => params.id) + const sessionStatus = createMemo(() => { + const id = sessionID() + if (!id) return idle + return sync().data.session_status[id] ?? idle + }) + const sessionMessages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : [])) + const projectedMessages = createMemo(() => { + const id = sessionID() + if (!id) return [] + const visible = new Set(props.userMessages.map((message) => message.id)) + const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id + const messages = sync().data.session_message[id] ?? [] + if (!boundary) return messages + const index = messages.findIndex((message) => message.id === boundary) + return index < 0 ? messages : messages.slice(0, index) + }) + const info = createMemo(() => { + const id = sessionID() + if (!id) return + return sync().session.get(id) + }) + const titleValue = createMemo(() => info()?.title) + const titleLabel = createMemo(() => sessionTitle(titleValue())) + const shareUrl = createMemo(() => info()?.share?.url) + const shareEnabled = createMemo(() => sync().data.config.share !== "disabled") + const parentID = createMemo(() => info()?.parentID) + const parent = createMemo(() => { + const id = parentID() + if (!id) return + return sync().session.get(id) + }) + const parentMessages = createMemo(() => { + const id = parentID() + if (!id) return emptyMessages + return sync().data.message[id] ?? emptyMessages + }) + const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new")) + const getMsgParts = (msgId: string) => sync().data.part[msgId] ?? emptyParts + const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID) + const childTaskDescription = createMemo(() => { + const id = sessionID() + if (!id) return + return parentMessages() + .flatMap((message) => getMsgParts(message.id)) + .map((part) => taskDescription(part, id)) + .findLast((value): value is string => !!value) + }) + const childTitle = createMemo(() => { + if (!parentID()) return titleLabel() ?? "" + if (childTaskDescription()) return childTaskDescription() + const value = titleLabel()?.replace(/\s+\(@[^)]+ subagent\)$/, "") + if (value) return value + return language.t("command.session.new") + }) + const showHeader = createMemo(() => !!(titleValue() || parentID())) + const projection = createTimelineProjection({ + messages: sessionMessages, + userMessages: () => props.userMessages, + sessionMessages: projectedMessages, + parts: getMsgParts, + status: sessionStatus, + showReasoningSummaries: settings.general.showReasoningSummaries, + inlineComments: settings.general.newLayoutDesigns, + }) + const activeMessageID = projection.activeMessageID + const assistantMessagesByParent = projection.assistantMessagesByParent + const lastAssistantGroupKey = projection.lastAssistantGroupKey + const messageByID = projection.messageByID + const messageLastRowIndex = projection.messageLastRowIndex + const messageRowIndex = projection.messageRowIndex + const timelineRowByKey = projection.rowByKey + const timelineRows = projection.rows + + let prependAnchor: { key: string; offset: number } | undefined + let prependAnchorFrame: number | undefined + let prependLoading = false + const clearPrependAnchor = () => { + prependLoading = false + prependAnchor = undefined + if (prependAnchorFrame === undefined) return + cancelAnimationFrame(prependAnchorFrame) + prependAnchorFrame = undefined + } + const capturePrependAnchor = () => { + prependLoading = true + updatePrependAnchor() + } + const updatePrependAnchor = () => { + const root = listRoot() + if (!root) return + const view = root.getBoundingClientRect() + const anchor = [...root.querySelectorAll("[data-timeline-key]")] + .map((element) => ({ element, rect: element.getBoundingClientRect() })) + .filter((item) => item.rect.bottom > view.top && item.rect.top < view.bottom) + .sort((a, b) => a.rect.top - b.rect.top)[0] + if (!anchor) return + if (!anchor.element.dataset.timelineKey) return + prependAnchor = { key: anchor.element.dataset.timelineKey, offset: anchor.rect.top - view.top } + } + const restorePrependAnchor = (done: boolean) => { + if (done) prependLoading = false + applyPrependAnchor() + } + const applyPrependAnchor = () => { + const root = listRoot() + if (!root || !prependAnchor) return + if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame) + let frames = 0 + let stable = 0 + const apply = () => { + prependAnchorFrame = undefined + const anchor = prependAnchor + if (!anchor) return + const element = root.querySelector(`[data-timeline-key="${CSS.escape(anchor.key)}"]`) + const delta = element + ? element.getBoundingClientRect().top - root.getBoundingClientRect().top - anchor.offset + : undefined + if (delta !== undefined && Math.abs(delta) > 0.5) { + root.scrollTop += delta + stable = 0 + } else { + stable += 1 + } + frames += 1 + if (stable >= 30 || frames >= 180) { + if (!prependLoading) prependAnchor = undefined + return + } + prependAnchorFrame = requestAnimationFrame(apply) + } + prependAnchorFrame = requestAnimationFrame(apply) + } + + const [toolOpen, setToolOpen] = createStore>(cached?.toolOpen ?? {}) + const [renderOverscan, setRenderOverscan] = createSignal(initialMeasurements?.length || coldBottomMount ? 6 : 20) + let resizePinnedIndexes: number[] = [] + let resizePinFrame: number | undefined + let virtualContent: HTMLDivElement | undefined + const virtualizer = createVirtualizer({ + get count() { + return timelineRows().length + }, + getScrollElement: () => listRoot() ?? null, + observeElementOffset: observeElementOffsetReconnectAware, + initialOffset: () => (props.shouldAnchorBottom() ? Number.MAX_SAFE_INTEGER : 0), + initialMeasurementsCache: initialMeasurements, + estimateSize: () => timelineFallbackItemSize, + scrollToFn: (offset, options, instance) => { + // Expose the computed range before core writes an anchor correction so the browser does not clamp it to the old height. + if (virtualContent) virtualContent.style.height = `${instance.getTotalSize()}px` + elementScroll(offset, options, instance) + }, + get getItemKey() { + const rows = timelineRows() + return (index: number) => { + const row = rows[index] + // ResizeObserver can report a removed element after its row has left the projection. + if (!row) return `removed:${index}` + return TimelineRow.key(row) + } + }, + anchorTo: "end", + followOnAppend: true, + scrollEndThreshold: 80, + get scrollMargin() { + return showHeader() ? 64 : 0 + }, + overscan: 50, + paddingEnd: 64, + rangeExtractor: (range) => { + const id = activeMessageID() + const active = id ? (messageLastRowIndex().get(id) ?? -1) : -1 + const indexes = defaultRangeExtractor({ ...range, overscan: renderOverscan() }) + return filterVirtualIndexes( + [...new Set([...resizePinnedIndexes, ...indexes, ...(active < 0 ? [] : [active])])].sort((a, b) => a - b), + range.count, + ) + }, + }) + const resizeItem = virtualizer.resizeItem + let resizeAnchorScheduled = false + const anchorResizedBottom = () => { + if (resizeAnchorScheduled || props.hasScrollGesture()) return + resizeAnchorScheduled = true + queueMicrotask(() => { + resizeAnchorScheduled = false + if (!props.shouldAnchorBottom() || props.hasScrollGesture()) return + virtualizer.scrollToEnd() + }) + } + virtualizer.resizeItem = (index, size) => { + const item = virtualizer.measurementsCache[index] + const previous = item ? (virtualizer.itemSizeCache.get(item.key) ?? item.size) : undefined + const root = listRoot() + if (root && previous !== undefined && Math.abs(size - previous) > root.clientHeight) { + const view = root.getBoundingClientRect() + resizePinnedIndexes = [...root.querySelectorAll("[data-index]")] + .filter((element) => { + const rect = element.getBoundingClientRect() + return rect.bottom > view.top && rect.top < view.bottom + }) + .map((element) => Number(element.dataset.index)) + if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame) + resizePinFrame = requestAnimationFrame(() => { + resizePinFrame = requestAnimationFrame(() => { + resizePinFrame = undefined + resizePinnedIndexes = [] + }) + }) + } + resizeItem(index, size) + if (root && props.shouldAnchorBottom()) anchorResizedBottom() + } + virtualizer.shouldAdjustScrollPositionOnItemSizeChange = (item) => { + if (props.shouldAnchorBottom()) return false + const first = virtualizer.range?.startIndex + return first !== undefined && item.index < first + } + const virtualItemByKey = createMemo( + () => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)), + ) + const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key as string)) + createEffect(() => { + props.setRevealMessage?.((id) => { + const index = messageRowIndex().get(id) + if (index === undefined) return + virtualizer.scrollToIndex(index, { align: "center" }) + }) + props.setScrollToEnd?.(() => virtualizer.scrollToEnd()) + props.setHistoryAnchor?.({ capture: capturePrependAnchor, restore: restorePrependAnchor }) + }) + + let overscanFrame: number | undefined + onMount(() => { + overscanFrame = requestAnimationFrame(() => { + if (props.shouldAnchorBottom()) virtualizer.scrollToEnd() + overscanFrame = requestAnimationFrame(() => { + overscanFrame = undefined + if (renderOverscan() < 20) setRenderOverscan(20) + if (props.shouldAnchorBottom()) virtualizer.scrollToEnd() + }) + }) + }) + + const maybeAnchorBottom = () => { + if (timelineRows().length === 0) return + if (!props.shouldAnchorBottom() || props.hasScrollGesture()) return + if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame) + clearPrependAnchor() + if (prependAnchorFrame !== undefined) cancelAnimationFrame(prependAnchorFrame) + virtualizer.scrollToEnd() + } + + let measuredSessionKey = sessionKey() + createEffect(() => { + const key = sessionKey() + timelineRows().length + if (measuredSessionKey !== key) { + measuredSessionKey = key + virtualizer.measure() + } + maybeAnchorBottom() + }) + + onCleanup(() => { + clearPrependAnchor() + timelineCache.delete(ownerSessionKey) + timelineCache.set(ownerSessionKey, { measurements: virtualizer.takeSnapshot(), toolOpen: { ...toolOpen } }) + while (timelineCache.size > 16) timelineCache.delete(timelineCache.keys().next().value!) + if (resizePinFrame !== undefined) cancelAnimationFrame(resizePinFrame) + if (overscanFrame !== undefined) cancelAnimationFrame(overscanFrame) + props.setRevealMessage?.(() => {}) + props.setScrollToEnd?.(() => {}) + props.setHistoryAnchor?.({ capture: () => {}, restore: () => {} }) + }) + + const [title, setTitle] = createStore({ + draft: "", + editing: false, + menuOpen: false, + pendingRename: false, + pendingShare: false, + }) + let titleRef: HTMLInputElement | undefined + + const [share, setShare] = createStore({ + open: false, + dismiss: null as "escape" | "outside" | null, + }) + let more: HTMLButtonElement | undefined + + const bindListRoot = (root: HTMLDivElement) => { + if (root === listRoot()) return + setListRoot(root) + props.setScrollRef(root) + } + + const handleListWheel = (event: WheelEvent & { currentTarget: HTMLDivElement }) => { + if (!prependLoading) clearPrependAnchor() + const root = event.currentTarget + const delta = normalizeWheelDelta({ + deltaY: event.deltaY, + deltaMode: event.deltaMode, + rootHeight: root.clientHeight, + }) + if (!delta) return + markBoundaryGesture({ root, target: event.target, delta, onMarkScrollGesture: props.onMarkScrollGesture }) + } + + const handleListTouchStart = (event: TouchEvent) => { + if (!prependLoading) clearPrependAnchor() + touchGesture = event.touches[0]?.clientY + } + + const handleListTouchMove = (event: TouchEvent & { currentTarget: HTMLDivElement }) => { + const next = event.touches[0]?.clientY + const prev = touchGesture + touchGesture = next + if (next === undefined || prev === undefined) return + + const delta = prev - next + if (!delta) return + + markBoundaryGesture({ + root: event.currentTarget, + target: event.target, + delta, + onMarkScrollGesture: props.onMarkScrollGesture, + }) + } + + const handleListTouchEnd = () => { + touchGesture = undefined + } + + const handleListPointerDown = (event: PointerEvent & { currentTarget: HTMLDivElement }) => { + if (!prependLoading) clearPrependAnchor() + props.onMarkScrollGesture(event.target) + } + + const handleListPointerMove = (event: PointerEvent) => { + if (event.buttons !== 1) return + props.onMarkScrollGesture(event.target) + } + + const handleListKeyDown = (event: KeyboardEvent & { currentTarget: HTMLDivElement }) => { + const key = scrollKey(event) + if (!key) return + if (!isScrollKeyTarget(event.target, key)) return + if (scrollKeyOwner(event.currentTarget, event.target, key) !== event.currentTarget) return + if (!prependLoading) clearPrependAnchor() + props.onMarkScrollGesture(event.currentTarget) + } + + const handleListScroll = (event: Event & { currentTarget: HTMLDivElement }) => { + if (prependLoading) updatePrependAnchor() + props.onScheduleScrollState(event.currentTarget) + props.onHistoryScroll() + if (!props.hasScrollGesture()) return + props.onUserScroll() + props.onAutoScrollHandleScroll() + props.onMarkScrollGesture(event.currentTarget) + } + + onCleanup(() => { + props.setScrollRef(undefined) + }) + + const viewShare = () => { + const url = shareUrl() + if (!url) return + platform.openExternal(url) + } + + const errorMessage = (err: unknown) => { + if (err && typeof err === "object" && "data" in err) { + const data = (err as { data?: { message?: string } }).data + if (data?.message) return data.message + } + if (err instanceof Error) return err.message + return language.t("common.requestFailed") + } + + const shareMutation = useMutation(() => ({ + mutationFn: (id: string) => serverSDK().client.session.share({ sessionID: id }), + onError: (err) => { + console.error("Failed to share session", err) + }, + })) + + const unshareMutation = useMutation(() => ({ + mutationFn: (id: string) => serverSDK().client.session.unshare({ sessionID: id }), + onError: (err) => { + console.error("Failed to unshare session", err) + }, + })) + + const titleMutation = useMutation(() => ({ + mutationFn: (input: { id: string; title: string }) => + sdk().api.session.rename({ sessionID: input.id, title: input.title }), + onSuccess: (_, input) => { + sync().set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === input.id) + if (index !== -1) draft.session[index].title = input.title + }), + ) + setTitle("editing", false) + }, + onError: (err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }, + })) + + const shareSession = () => { + const id = sessionID() + if (!id || shareMutation.isPending) return + if (!shareEnabled()) return + shareMutation.mutate(id) + } + + const unshareSession = () => { + const id = sessionID() + if (!id || unshareMutation.isPending) return + if (!shareEnabled()) return + unshareMutation.mutate(id) + } + const copyShareUrl = () => { + const url = shareUrl() + if (!url) return + void navigator.clipboard + .writeText(url) + .then(() => + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("session.share.copy.copied"), + description: url, + }), + ) + .catch((err: unknown) => + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }), + ) + } + const selectShareUrlText: JSX.EventHandler = (event) => { + const selection = window.getSelection() + if (!selection) return + const range = document.createRange() + range.selectNodeContents(event.currentTarget) + selection.removeAllRanges() + selection.addRange(range) + } + + createEffect( + on( + sessionKey, + () => + setTitle({ + draft: "", + editing: false, + menuOpen: false, + pendingRename: false, + pendingShare: false, + }), + { defer: true }, + ), + ) + + createEffect( + on( + () => [parentID(), childTaskDescription()] as const, + ([id, description]) => { + if (!id || description) return + if (sync().data.message[id] !== undefined) return + void sync().session.sync(id) + }, + { defer: true }, + ), + ) + + const openTitleEditor = () => { + if (!sessionID() || parentID()) return + setTitle({ editing: true, draft: titleLabel() ?? "" }) + requestAnimationFrame(() => { + if (!titleRef) return + titleRef.focus() + titleRef.select() + }) + } + + const closeTitleEditor = () => { + if (titleMutation.isPending) return + setTitle("editing", false) + } + + const saveTitleEditor = () => { + if (!title.editing) return + const id = sessionID() + if (!id) return + if (titleMutation.isPending) return + + const next = title.draft.trim() + if (!next || next === (titleLabel() ?? "")) { + setTitle("editing", false) + return + } + + titleMutation.mutate({ id, title: next }) + } + + 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 deleteSession = async (sessionID: string) => { + const session = sync().session.get(sessionID) + if (!session) return false + + const sessions = (sync().data.session ?? []).filter((s) => !s.parentID && !s.time?.archived) + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + const result = await sdk() + .api.session.remove({ sessionID }) + .then(() => true) + .catch((err) => { + showToast({ + title: language.t("session.delete.failed.title"), + description: errorMessage(err), + }) + return false + }) + + if (!result) return false + + const removed = new Set([sessionID]) + const byParent = new Map() + for (const item of sync().data.session) { + const parentID = item.parentID + if (!parentID) continue + const existing = byParent.get(parentID) + if (existing) { + existing.push(item.id) + continue + } + byParent.set(parentID, [item.id]) + } + + const stack = [sessionID] + while (stack.length) { + const parentID = stack.pop() + if (!parentID) continue + + const children = byParent.get(parentID) + if (!children) continue + + for (const child of children) { + if (removed.has(child)) continue + removed.add(child) + stack.push(child) + } + } + + sessionArchive.navigateAfterRemoval(sessionID, session.parentID, nextSession?.id) + + sync().set( + produce((draft) => { + draft.session = draft.session.filter((s) => !removed.has(s.id)) + }), + ) + + for (const id of removed) { + sync().session.evict(id) + } + notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [...removed] }) + return true + } + + const navigateParent = () => { + const id = parentID() + if (!id) return + navigate( + params.serverKey ? sessionHref(requireServerKey(params.serverKey), id) : legacySessionHref(sdk().directory, id), + ) + } + + function DialogDeleteSession(props: { sessionID: string }) { + const name = createMemo( + () => sessionTitle(sync().session.get(props.sessionID)?.title) ?? language.t("command.session.new"), + ) + const handleDelete = async () => { + await deleteSession(props.sessionID) + dialog.close() + } + + if (settings.general.newLayoutDesigns()) + return ( + + + + + + dialog.close()}> + {language.t("common.cancel")} + + + {language.t("session.delete.button")} + + + + ) + + return ( + +
+
+ + {language.t("session.delete.confirm", { name: name() })} + +
+
+ + +
+
+
+ ) + } + + const workingTurn = (userMessageID: string) => sessionStatus().type !== "idle" && activeMessageID() === userMessageID + + const turnDurationMs = (userMessageID: string) => { + const message = messageByID().get(userMessageID) + if (!message || message.role !== "user") return + const end = (assistantMessagesByParent().get(userMessageID) ?? emptyAssistantMessages).reduce( + (max, item) => { + const completed = item.time.completed + if (typeof completed !== "number") return max + if (max === undefined) return completed + return Math.max(max, completed) + }, + undefined, + ) + if (typeof end !== "number") return + if (end < message.time.created) return + return end - message.time.created + } + + const assistantCopyPartID = (userMessageID: string) => { + if (workingTurn(userMessageID)) return null + const messages = assistantMessagesByParent().get(userMessageID) ?? emptyAssistantMessages + + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i] + if (!message) continue + + const parts = getMsgParts(message.id) + for (let j = parts.length - 1; j >= 0; j--) { + const part = parts[j] + if (!part || part.type !== "text" || !part.text?.trim()) continue + return part.id + } + } + } + + const renderAssistantPartGroup = (row: Accessor, onSizeChange?: () => void) => { + if (row().group.type === "context") { + const parts = createMemo(() => { + const group = row().group + if (group.type !== "context") return emptyTools + return group.refs + .map((ref) => getMsgPart(ref.messageID, ref.partID)) + .filter((part): part is ToolPart => part?.type === "tool") + }) + const contextOpenKey = () => `context:${row().group.key}` + const open = createMemo(() => { + return toolOpen[contextOpenKey()] === true + }) + + return ( + setToolOpen(contextOpenKey(), value)} + busy={ + workingTurn(row().userMessageID) && lastAssistantGroupKey().get(row().userMessageID) === row().group.key + } + onSizeChange={onSizeChange} + /> + ) + } + + const message = createMemo(() => { + const group = row().group + if (group.type !== "part") return + return messageByID().get(group.ref.messageID) + }) + const part = createMemo(() => { + const group = row().group + if (group.type !== "part") return + return getMsgPart(group.ref.messageID, group.ref.partID) + }) + const defaultOpen = createMemo(() => { + const item = part() + if (!item) return + return partDefaultOpen(item, settings.general.shellToolPartsExpanded(), settings.general.editToolPartsExpanded()) + }) + + return ( + + {(message) => ( + + {(part) => ( + setToolOpen(part().id, open)} + deferToolContent + virtualizeDiff={false} + onContentRendered={onSizeChange} + /> + )} + + )} + + ) + } + + function TimelineRowFrame(input: { row: Accessor; children: JSX.Element }) { + const anchor = () => { + const row = input.row() + return row._tag === "CommentStrip" || (row._tag === "UserMessage" && row.anchor) + } + const previousAssistantPart = () => { + const row = input.row() + return row._tag === "AssistantPart" && row.previousAssistantPart + } + + return ( +
+
+ {input.children} +
+
+ ) + } + + const renderTimelineRow = (row: Accessor, onSizeChange?: () => void) => { + switch (row()._tag) { + case "TurnGap": + return + } + > + props.onSelectFile(node.path)} + /> + } + > + 0} + fallback={
{language.t("palette.empty")}
} + > + { + setExplicitHighlight(path) + props.onSelectFile(path) + }} + /> +
+
+ + + ) +} diff --git a/packages/app/src/pages/session/v2/session-file-browser-tab.tsx b/packages/app/src/pages/session/v2/session-file-browser-tab.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c9e0d3816705b58780c6e8313cf5e4b0f35ab6c9 --- /dev/null +++ b/packages/app/src/pages/session/v2/session-file-browser-tab.tsx @@ -0,0 +1,182 @@ +import { createMemo, createSignal, createUniqueId, Show } from "solid-js" +import { createQuery, keepPreviousData } from "@tanstack/solid-query" +import { Icon } from "@opencode-ai/ui/icon" +import { SessionFilePanelV2, SessionFilePanelV2Empty } from "@opencode-ai/session-ui/v2/session-file-panel-v2" +import { SessionReviewV2Sidebar } from "@opencode-ai/session-ui/v2/session-review-v2" +import FileTreeV2, { type Kind } from "@/components/file-tree-v2" +import { useFile } from "@/context/file" +import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" +import { useSDK } from "@/context/sdk" +import { displayName } from "@/pages/layout/helpers" +import { useSessionLayout } from "@/pages/session/session-layout" +import { SessionFileView } from "@/pages/session/file-tabs" +import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2" +import { pathKey } from "@/utils/path-key" + +const emptyFiles: string[] = [] + +export type SessionFileBrowserState = { + sidebarOpened: () => boolean + sidebarWidth: () => number + sidebarTransition: () => boolean + resizeSidebar: (width: number) => void + toggleSidebar: () => void +} + +export function SessionFileBrowserTab(props: { + tab: string + placeholder: boolean + active?: string + kinds: ReadonlyMap + state: SessionFileBrowserState + onSelect: (path: string) => void + onSelectPermanent: (path: string) => void + filterRef?: (element: HTMLInputElement) => void +}) { + const file = useFile() + const language = useLanguage() + const layout = useLayout() + const sdk = useSDK() + const { workspaceKey } = useSessionLayout() + const resultsID = `session-file-browser-results-${createUniqueId()}` + const [filter, setFilter] = createSignal("") + const [explicitHighlight, setExplicitHighlight] = createSignal() + const sidebarOpened = () => props.placeholder || props.state.sidebarOpened() + const query = createMemo(() => filter().trim()) + const search = createQuery(() => { + const value = query() + return { + queryKey: ["session-open-file", workspaceKey(), value] as const, + enabled: value.length > 0, + queryFn: ({ signal }) => file.searchFiles(value, { limit: 200, signal }), + placeholderData: keepPreviousData, + } + }) + const files = createMemo(() => { + if (!query() || search.isPending) return emptyFiles + return [...new Set(search.data ?? emptyFiles)] + }) + const highlighted = createMemo(() => { + const values = files() + if (values.length === 0) return undefined + const explicit = explicitHighlight() + if (explicit && values.includes(explicit)) return explicit + return values[0] + }) + const loading = createMemo(() => query().length > 0 && search.isPending) + const project = createMemo(() => { + const directory = pathKey(sdk().directory) + return layout.projects + .list() + .find( + (item) => + pathKey(item.worktree) === directory || item.sandboxes?.some((sandbox) => pathKey(sandbox) === directory), + ) + }) + const title = createMemo(() => displayName(project() ?? { worktree: sdk().directory })) + const optionID = (path: string) => `${resultsID}-option-${files().indexOf(path)}` + + const onFilterKeyDown = (event: KeyboardEvent & { currentTarget: HTMLInputElement }) => { + if (event.key === "Escape" && query()) { + event.preventDefault() + setFilter("") + return + } + if (!query()) return + applyFileListKeyDown(event, files(), highlighted(), { + onHighlight: setExplicitHighlight, + onSelect: props.onSelectPermanent, + }) + } + + // Keep the sidebar outside Kobalte Tabs.Content: a morphing content value + // unmounts the whole panel on every file-tab switch and resets sidebar scroll. + return ( + {title()}} + filter={filter()} + onFilterChange={setFilter} + onFilterKeyDown={onFilterKeyDown} + filterAutofocus={props.placeholder} + filterRef={props.filterRef} + filterControls={resultsID} + filterActiveDescendant={highlighted() ? optionID(highlighted()!) : undefined} + filterExpanded={query().length > 0 && files().length > 0} + width={props.state.sidebarWidth()} + onWidthChange={props.state.resizeSidebar} + > + props.onSelect(node.path)} + onFileDoubleClick={(node) => props.onSelectPermanent(node.path)} + /> + } + > + + {language.t("common.loading")} + {language.t("common.loading.ellipsis")} +
+ } + > + 0} + fallback={ +
+ {language.t("palette.empty")} +
+ } + > + { + setExplicitHighlight(path) + props.onSelect(path) + }} + onFileDoubleClick={props.onSelectPermanent} + /> +
+
+ + + } + > + +
+ +
{language.t("command.file.open")}
+
{language.t("session.files.selectToOpen")}
+
+ + } + > +
+ + {(tab) => } + +
+
+ + ) +} diff --git a/packages/app/src/pages/session/v2/session-file-list-v2.tsx b/packages/app/src/pages/session/v2/session-file-list-v2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..16cac2a45d66624f3de4dda39aef99356f078907 --- /dev/null +++ b/packages/app/src/pages/session/v2/session-file-list-v2.tsx @@ -0,0 +1,166 @@ +import { FileIcon } from "@opencode-ai/ui/file-icon" +import "@opencode-ai/ui/v2/file-tree-v2.css" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { createEffect, createMemo, createSignal, For, Show } from "solid-js" +import { kindChange, kindLabel, type Kind } from "@/components/file-tree-v2" +import { normalizePath } from "@/pages/session/v2/review-diff-kinds" +import { createVirtualizer, defaultRangeExtractor } from "@tanstack/solid-virtual" +import { virtualScrollElement } from "@/components/virtual-scroll-element" + +// Drives the highlight/selection of the flat search-result list from the filter +// input's keyboard events. +export function applyFileListKeyDown( + event: KeyboardEvent, + files: readonly string[], + highlighted: string | undefined, + options: { onHighlight: (path: string) => void; onSelect: (path: string) => void }, +) { + if (files.length === 0) return + + if (event.key === "ArrowDown" || event.key === "ArrowUp") { + const currentIndex = highlighted ? files.indexOf(highlighted) : -1 + const delta = event.key === "ArrowDown" ? 1 : -1 + const start = currentIndex === -1 ? (delta > 0 ? 0 : files.length - 1) : currentIndex + delta + const index = Math.max(0, Math.min(files.length - 1, start)) + options.onHighlight(files[index]!) + event.preventDefault() + return + } + + if (event.key !== "Enter") return + const target = highlighted ?? files[0] + if (!target) return + options.onSelect(target) + event.preventDefault() +} + +// Flat variant of FileTreeV2 for filtered results: reuses its data-component and +// row data-slots on purpose so file-tree-v2.css styles both. data-highlighted has +// no CSS of its own — it folds into data-selected below and only exists as the +// scrollIntoView query hook. +export function SessionFileListV2(props: { + files: readonly string[] + active?: string + highlighted?: string + kinds?: ReadonlyMap + id?: string + role?: "listbox" + optionID?: (path: string) => string + onFileClick: (path: string) => void + onFileDoubleClick?: (path: string) => void +}) { + const active = () => normalizePath(props.active ?? "") + const highlighted = () => normalizePath(props.highlighted ?? "") + const normalized = createMemo(() => props.files.map(normalizePath)) + const [root, setRoot] = createSignal() + const [focused, setFocused] = createSignal() + const virtualizer = createVirtualizer({ + get count() { + return props.files.length + }, + getScrollElement: () => virtualScrollElement(root()), + initialRect: { width: 0, height: 600 }, + estimateSize: () => 28, + gap: 2, + overscan: 10, + get getItemKey() { + const files = props.files + return (index: number) => files[index] ?? index + }, + rangeExtractor: (range) => { + const indexes = defaultRangeExtractor(range) + const path = focused() + const index = path ? props.files.indexOf(path) : -1 + if (index < 0 || indexes.includes(index)) return indexes + return [...indexes, index].sort((a, b) => a - b) + }, + }) + + createEffect(() => { + const index = normalized().indexOf(highlighted()) + if (index < 0) return + queueMicrotask(() => { + if (virtualizer.range && index >= virtualizer.range.startIndex && index <= virtualizer.range.endIndex) return + virtualizer.scrollToIndex(index, { align: "auto" }) + }) + }) + const virtualItemByKey = createMemo( + () => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item] as const)), + ) + const virtualRowKeys = createMemo(() => virtualizer.getVirtualItems().map((item) => item.key)) + + return ( +
+ + {(key) => { + const path = key as string + const value = normalizePath(path) + const selected = () => (highlighted() ? highlighted() === value : active() === value) + const highlightedRow = () => highlighted() === value + const kind = () => props.kinds?.get(value) + const directory = () => (value.includes("/") ? getDirectory(value) : undefined) + const filename = () => getFilename(value) + return ( + + {(item) => ( +
+ +
+ )} +
+ ) + }} +
+
+ ) +} diff --git a/packages/app/src/sst-env.d.ts b/packages/app/src/sst-env.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..035e323c0402de6be84a5da16ed0080f56ad65cb --- /dev/null +++ b/packages/app/src/sst-env.d.ts @@ -0,0 +1,12 @@ +/* This file is auto-generated by SST. Do not edit. */ +/* tslint:disable */ +/* eslint-disable */ +/* biome-ignore-all lint: auto-generated */ + +/// +interface ImportMetaEnv { + +} +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file diff --git a/packages/app/src/theme-preload.test.ts b/packages/app/src/theme-preload.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..00d7da23948bcc19558732ac135cca624c67e287 --- /dev/null +++ b/packages/app/src/theme-preload.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, test } from "bun:test" + +const src = await Bun.file(new URL("../public/oc-theme-preload.js", import.meta.url)).text() + +const run = () => Function(src)() + +beforeEach(() => { + document.head.innerHTML = "" + document.documentElement.removeAttribute("data-theme") + document.documentElement.removeAttribute("data-color-scheme") + localStorage.clear() + Object.defineProperty(window, "matchMedia", { + value: () => + ({ + matches: false, + }) as MediaQueryList, + configurable: true, + }) +}) + +describe("theme preload", () => { + test("migrates legacy oc-1 to oc-2 before mount", () => { + localStorage.setItem("opencode-theme-id", "oc-1") + localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;") + localStorage.setItem("opencode-theme-css-dark", "--background-base:#000;") + + run() + + expect(document.documentElement.dataset.theme).toBe("oc-2") + expect(document.documentElement.dataset.colorScheme).toBe("light") + expect(localStorage.getItem("opencode-theme-id")).toBe("oc-2") + expect(localStorage.getItem("opencode-theme-css-light")).toBeNull() + expect(localStorage.getItem("opencode-theme-css-dark")).toBeNull() + expect(document.getElementById("oc-theme-preload")).toBeNull() + }) + + test("keeps cached css for non-default themes", () => { + localStorage.setItem("opencode-theme-id", "nightowl") + localStorage.setItem("opencode-theme-css-light", "--background-base:#fff;") + + run() + + expect(document.documentElement.dataset.theme).toBe("nightowl") + expect(document.getElementById("oc-theme-preload")?.textContent).toContain("--background-base:#fff;") + }) +}) diff --git a/packages/app/src/updater.ts b/packages/app/src/updater.ts new file mode 100644 index 0000000000000000000000000000000000000000..228743488cc3c172b90b9bac51724ae0c8950462 --- /dev/null +++ b/packages/app/src/updater.ts @@ -0,0 +1,17 @@ +import type { Accessor } from "solid-js" + +export type UpdaterState = + | { status: "disabled" } + | { status: "idle" } + | { status: "checking" } + | { status: "downloading"; version: string; percent?: number } + | { status: "ready"; version: string } + | { status: "up-to-date" } + | { status: "installing"; version: string } + | { status: "error"; message: string } + +export type UpdaterPlatform = { + state: Accessor + check(): Promise + install(): Promise +} diff --git a/packages/app/src/wsl/add-server-probes.ts b/packages/app/src/wsl/add-server-probes.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9e1fd85e85fa671f1a5b611500ec13a0d771667 --- /dev/null +++ b/packages/app/src/wsl/add-server-probes.ts @@ -0,0 +1,57 @@ +import { useMutation } from "@tanstack/solid-query" +import { createEffect } from "solid-js" +import type { Accessor } from "solid-js" +import { + addServerProbePlan, + createProbeFailureGate, + runAddableProbePlan, + type AddServerProbePlan, + type WslAddServerView, +} from "./settings-model" +import type { WslInstalledDistro, WslServersPlatform, WslServersState } from "./types" + +export function useWslAddServerProbes(input: { + state: Accessor + api: WslServersPlatform + view: Accessor + adding: Accessor + busy: Accessor + selectedDistro: Accessor + addableInstalledDistros: Accessor + onError: (error: unknown) => void +}) { + const gate = createProbeFailureGate() + const probe = useMutation(() => ({ + mutationFn: async (command: AddServerProbePlan) => { + if (command.kind === "addable") { + await runAddableProbePlan({ plan: command.plan, api: input.api }) + return + } + if (command.plan.action === "probeRuntime") await input.api.probeRuntime() + if (command.plan.action === "refreshDistros") await input.api.refreshDistros() + }, + onError: input.onError, + onSettled: (_result, error, command) => { + if (command) gate.settle(command.key, error) + }, + })) + + createEffect(() => { + if (probe.isPending) return + const command = addServerProbePlan({ + state: input.state(), + view: input.view(), + adding: input.adding(), + busy: input.busy(), + selectedDistro: input.selectedDistro(), + addableInstalledDistros: input.addableInstalledDistros(), + }) + if (!command || !gate.accepts(command.key)) return + probe.mutate(command) + }) + + return { + probingAddable: () => probe.isPending && probe.variables?.kind === "addable", + resetProbeFailure: () => gate.reset(), + } +} diff --git a/packages/app/src/wsl/context.tsx b/packages/app/src/wsl/context.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bba52b501ad5fbe215dcd9c68d8627ec5e4edcb0 --- /dev/null +++ b/packages/app/src/wsl/context.tsx @@ -0,0 +1,36 @@ +import { createSimpleContext } from "@opencode-ai/ui/context" +import { queryOptions, useQuery, useQueryClient } from "@tanstack/solid-query" +import { createEffect, onCleanup } from "solid-js" +import type { WslServersState } from "./types" +import { usePlatform } from "../context/platform" + +const wslServersQueryKey = ["platform", "wslServers"] as const + +export const { use: useWslServers, provider: WslServersProvider } = createSimpleContext({ + name: "WslServers", + init: () => { + const platform = usePlatform() + const queryClient = useQueryClient() + const query = useQuery(() => { + const api = platform.wslServers + return queryOptions({ + queryKey: wslServersQueryKey, + queryFn: () => api!.getState(), + enabled: !!api, + staleTime: Number.POSITIVE_INFINITY, + gcTime: Number.POSITIVE_INFINITY, + }) + }) + + createEffect(() => { + const api = platform.wslServers + if (!api) return + const off = api.subscribe((event) => { + queryClient.setQueryData(wslServersQueryKey, event.state) + }) + onCleanup(off) + }) + + return query as typeof query & { readonly data: WslServersState | undefined } + }, +}) diff --git a/packages/app/src/wsl/dialog-add-server.tsx b/packages/app/src/wsl/dialog-add-server.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3c0b7ee35b8d650993ccd82a568d1e2d02f2cfde --- /dev/null +++ b/packages/app/src/wsl/dialog-add-server.tsx @@ -0,0 +1,463 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { showToast } from "@opencode-ai/ui/toast" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode-ai/ui/v2/dialog-v2" +import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2" +import { LoaderV2 } from "@opencode-ai/ui/v2/loader-v2" +import { RadioGroupV2, RadioItemV2 } from "@opencode-ai/ui/v2/radio-v2" +import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2" +import { createMemo, For, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { useWslAddServerProbes } from "./add-server-probes" +import { useWslServers } from "./context" +import { addServerViewModel, type AddServerText } from "./settings-model" +import "./dialog-add-wsl-server.css" + +function isWslRuntimeMissing(error: string | null | undefined) { + if (!error) return true + return /WSL is not installed|not been installed|wsl(?:\.exe)? --install/i.test(error) +} + +function translate(language: ReturnType, value: AddServerText) { + if (value.params) return language.t(value.key, value.params) + return language.t(value.key) +} + +interface DialogWslServerProps { + onAdded?: (distro: string) => void | Promise +} + +export function DialogAddWslServer(props: DialogWslServerProps = {}) { + const language = useLanguage() + const controller = useWslAddServerController(props) + const model = controller.model + const primaryButton = () => model().primaryButton + const primaryButtonStyle = () => { + const width = primaryButton().width + if (!width) return undefined + return { width } + } + + return ( + + {controller.loadError()}
} + > +
+ +
+ + + } + > + + } + > + +
+ +
+
+
+ } + > + + + + {controller.view() === "main" ? language.t("wsl.server.add") : language.t("wsl.onboarding.installDistro")} + + + + + + controller.setCatalogSearch(event.currentTarget.value)} + /> +
+ + + {(item) => ( + {item.label}} + /> + )} + + +
+
+ + + {language.t("common.cancel")} + + + + + + + + + } + > + +
+ {language.t("wsl.onboarding.installedDistros")} + + {language.t("wsl.onboarding.checkAgain")} + +
+ + 0} + fallback={ +
+
+ {model().visibleInstalledDistros.length + ? language.t("wsl.onboarding.allDistrosAdded") + : language.t("wsl.onboarding.noDistros")} +
+
+ } + > +
+ + + {(item) => { + const status = () => model().distroStatuses[item.name] ?? null + return ( + {item.name}} + description={ + + {(value) => ( + + {translate(language, value().label)} + + )} + + } + /> + ) + }} + + +
+
+ + 0}> + + +
+ + + + {language.t("common.cancel")} + + + + + + + +
+
+ + + ) +} + +function useWslAddServerController(props: DialogWslServerProps) { + const language = useLanguage() + const platform = usePlatform() + const dialog = useDialog() + const wslServers = useWslServers() + const api = platform.wslServers! + const [store, setStore] = createStore({ + view: "main" as "main" | "catalog", + selectedDistro: null as string | null, + catalogSearch: "", + catalogTarget: null as string | null, + adding: false, + }) + const current = () => wslServers.data + const viewModel = (probingAddable: boolean) => + addServerViewModel({ + state: current(), + view: store.view, + selectedDistro: store.selectedDistro, + catalogSearch: store.catalogSearch, + catalogTarget: store.catalogTarget, + adding: store.adding, + probingAddable, + }) + const baseModel = createMemo(() => viewModel(false)) + const probes = useWslAddServerProbes({ + state: current, + api, + view: () => store.view, + adding: () => store.adding, + busy: () => baseModel().busy, + selectedDistro: () => baseModel().selectedDistro, + addableInstalledDistros: () => baseModel().addableInstalledDistros, + onError: (error) => requestError(language, error), + }) + const model = createMemo(() => viewModel(probes.probingAddable())) + + const openCatalog = () => { + const first = model().installableDistros[0] + setStore({ + view: "catalog", + catalogSearch: "", + catalogTarget: first?.name ?? null, + }) + } + + const run = async (action: () => Promise) => { + try { + await action() + } catch (err) { + requestError(language, err) + } + } + + const refreshDistros = () => { + void run(async () => { + probes.resetProbeFailure() + await api.refreshDistros() + }) + } + + const installDistro = (name: string) => { + void run(async () => { + probes.resetProbeFailure() + await api.installDistro(name) + setStore("view", "main") + }) + } + + const installCatalogDistro = () => { + if (model().installingCatalogDistro) return + const name = model().catalogTarget + if (!name) return + installDistro(name) + } + + const closeCatalog = () => { + probes.resetProbeFailure() + setStore({ view: "main", catalogSearch: "", catalogTarget: null }) + } + + const runPrimary = async () => { + const button = model().primaryButton + if (button.loading) return + const distro = model().selectedDistro + const action = button.action + if (!distro || !action) return + if (action === "install-opencode") { + await run(() => api.installOpencode(distro)) + return + } + setStore("adding", true) + try { + await api.addServer(distro) + if (props.onAdded) { + await props.onAdded(distro) + } else { + dialog.close() + } + } catch (err) { + requestError(language, err) + } finally { + setStore("adding", false) + } + } + + const loadError = () => { + const error = wslServers.error + if (!error) return language.t("wsl.onboarding.loadFailed") + return error instanceof Error ? error.message : String(error) + } + + return { + wslServers, + model, + loadError, + runtimeError: () => current()?.runtime?.error ?? null, + view: () => store.view, + catalogSearch: () => store.catalogSearch, + adding: () => store.adding, + setCatalogSearch: (value: string) => setStore("catalogSearch", value), + setCatalogTarget: (value: string) => setStore("catalogTarget", value), + setSelectedDistro: (value: string) => setStore("selectedDistro", value), + openCatalog, + closeCatalog, + refreshDistros, + installCatalogDistro, + installWsl: () => void run(() => api.installWsl()), + runPrimary: () => void runPrimary(), + close: () => dialog.close(), + } +} + +function DialogWslSetup(props: { + state: string + error: string | null + installable: boolean + busy: boolean + onInstall: () => void +}) { + const language = useLanguage() + const dialog = useDialog() + const title = () => + props.state === "pendingRestart" + ? language.t("wsl.onboarding.restartRequired") + : props.installable + ? language.t("wsl.onboarding.wslNotInstalled.title") + : language.t("wsl.onboarding.wslUnavailable.title") + const description = () => { + if (props.state === "pendingRestart") return language.t("wsl.onboarding.windowsRestartRequired") + if (!props.installable) return language.t("wsl.onboarding.wslUnavailable.description") + return language.t("wsl.onboarding.wslNotInstalled.description") + } + + return ( + +
+
+ +

{title()}

+

{description()}

+ +

{props.error}

+
+
+ + + {language.t("wsl.onboarding.installWsl")} + + + + dialog.close()}> + {language.t("common.close")} + + +
+
+ ) +} + +function requestError(language: ReturnType, err: unknown) { + console.error("WSL servers request failed", err instanceof Error ? (err.stack ?? err.message) : String(err)) + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }) +} diff --git a/packages/app/src/wsl/dialog-add-wsl-server.css b/packages/app/src/wsl/dialog-add-wsl-server.css new file mode 100644 index 0000000000000000000000000000000000000000..19cb42613aeddf593658e942d8fadcbffacee167 --- /dev/null +++ b/packages/app/src/wsl/dialog-add-wsl-server.css @@ -0,0 +1,392 @@ +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-container"] { + width: 480px; + max-width: calc(100vw - 32px); + height: auto; + align-items: stretch; +} + +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-content"] { + align-items: stretch; + width: 100%; + overflow: hidden; +} + +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-header"] { + align-items: center; + padding: 24px 16px 0; +} + +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-body"] { + display: flex; + width: 100%; + min-width: 0; + flex-direction: column; + align-items: stretch; + padding: 0; + gap: 0; +} + +.settings-v2-wsl-dialog-body { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + min-width: 0; + align-self: stretch; + box-sizing: border-box; + padding: 16px 16px 1px; +} + +[data-component="dialog-v2"].settings-v2-wsl-dialog [data-slot="dialog-footer"] { + padding: 0 16px 24px; + margin-top: 8px; +} + +[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-container"] { + width: 480px; + max-width: calc(100vw - 32px); + min-height: 264px; + height: auto; + background: var(--v2-background-bg-layer-01); + box-shadow: var(--v2-elevation-overlay); + border-radius: 10px; +} + +[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-content"] { + align-items: stretch; + min-height: 264px; + border-radius: 10px; + overflow: hidden; +} + +[data-component="dialog-v2"].settings-v2-wsl-not-installed-dialog [data-slot="dialog-body"] { + display: flex; + flex: 1; + flex-direction: column; + align-items: stretch; + justify-content: center; + padding: 0; +} + +.settings-v2-wsl-not-installed-content { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 56px 0; + gap: 24px; + text-align: center; +} + +.settings-v2-wsl-not-installed-message { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + gap: 8px; +} + +.settings-v2-wsl-not-installed-icon { + flex: none; + flex-shrink: 0; +} + +.settings-v2-wsl-not-installed-title { + display: flex; + flex: none; + align-items: center; + justify-content: center; + margin: 0; + font-style: normal; + font-weight: 530; + font-size: 15px; + line-height: 20px; + letter-spacing: -0.13px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-base); +} + +.settings-v2-wsl-not-installed-description { + display: flex; + flex: none; + align-items: center; + justify-content: center; + width: 100%; + max-width: 300px; + margin: 0; + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 20px; + text-align: center; + letter-spacing: -0.04px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-muted); +} + +.settings-v2-wsl-unavailable-error { + width: 100%; + max-width: 360px; + margin: 4px 0 0; + font-style: normal; + font-weight: 440; + font-size: 12px; + line-height: 18px; + text-align: center; + letter-spacing: -0.04px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-faint); + overflow-wrap: anywhere; +} + +.settings-v2-wsl-section-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + flex: none; + align-self: stretch; + gap: 12px; + height: 24px; + padding: 0 0 0 9px; +} + +.settings-v2-wsl-section-title { + flex: none; + user-select: none; + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.04px; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-base); +} + +.settings-v2-wsl-distro-list { + display: flex; + flex-direction: column; + align-items: flex-start; + flex: none; + align-self: stretch; + width: 100%; + height: fit-content; + max-height: 220px; + padding: 0; + border-radius: 8px; + background: var(--v2-background-bg-base); + box-shadow: var(--v2-elevation-raised); + overflow-x: hidden; + overflow-y: auto; +} + +.settings-v2-wsl-distro-group { + width: 100%; +} + +.settings-v2-wsl-distro-group [data-slot="radio-v2-items"] { + width: 100%; + gap: 0; +} + +.settings-v2-wsl-distro-row { + box-sizing: border-box; + width: 100%; + min-height: 62px; + padding: 14px 14px 14px 16px; + border-bottom: 0.5px solid var(--v2-border-border-base); +} + +.settings-v2-wsl-distro-row:last-child { + border-bottom: none; +} + +.settings-v2-wsl-distro-row [data-slot="radio-v2-item-label"] { + position: relative; + display: flex; + flex: 1; + width: 100%; + min-width: 0; + align-self: stretch; +} + +.settings-v2-wsl-distro-row [data-slot="radio-v2-item-label"]::before { + content: ""; + position: absolute; + inset: -14px -14px -14px -44px; +} + +.settings-v2-wsl-distro-row [data-slot="radio-v2-item-text"] { + width: 100%; +} + +.settings-v2-wsl-distro-label { + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.04px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; + color: inherit; +} + +.settings-v2-wsl-distro-status { + flex: none; + font-style: normal; + font-weight: 440; + font-size: 12px; + line-height: 12px; + letter-spacing: 0.05px; + font-variant-numeric: tabular-nums; + font-variation-settings: "slnt" 0; +} + +.settings-v2-wsl-distro-status[data-tone="success"] { + color: var(--v2-state-fg-success); +} + +.settings-v2-wsl-distro-status[data-tone="warning"] { + color: var(--v2-state-fg-warning); +} + +.settings-v2-wsl-distro-status[data-tone="muted"] { + color: var(--v2-text-text-muted); +} + +.settings-v2-wsl-distro-row:where([data-disabled]) .settings-v2-wsl-distro-status { + color: inherit; +} + +.settings-v2-wsl-distro-empty { + padding: 16px; + font-size: 13px; + font-weight: 440; + line-height: 1.4; + color: var(--v2-text-text-muted); +} + +.settings-v2-wsl-catalog-card { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: flex-start; + flex: none; + align-self: stretch; + gap: 12px; + width: 100%; + min-height: 64px; + padding: 14px 14px 16px 16px; + border: 0.5px solid var(--v2-border-border-base); + border-radius: 8px; + background: transparent; + cursor: pointer; + text-align: left; +} + +.settings-v2-wsl-catalog-card:focus-visible { + outline: 2px solid var(--v2-border-border-focus); + outline-offset: 2px; +} + +.settings-v2-wsl-catalog-card:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.settings-v2-wsl-catalog-icon { + display: flex; + flex: none; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: var(--v2-icon-icon-base); +} + +.settings-v2-wsl-catalog-copy { + display: flex; + min-width: 0; + flex: 1; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 6px; +} + +.settings-v2-wsl-catalog-title { + user-select: none; + font-style: normal; + font-weight: 440; + font-size: 13px; + line-height: 16px; + letter-spacing: -0.04px; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-base); +} + +.settings-v2-wsl-catalog-description { + user-select: none; + font-style: normal; + font-weight: 440; + font-size: 12px; + line-height: 12px; + letter-spacing: 0.05px; + font-variation-settings: "slnt" 0; + color: var(--v2-text-text-muted); +} + +.settings-v2-wsl-catalog-chevron { + display: flex; + flex: none; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: var(--v2-icon-icon-muted); +} + +.settings-v2-wsl-catalog-list { + display: flex; + flex-direction: column; + align-items: flex-start; + flex: none; + align-self: stretch; + width: 100%; + height: fit-content; + max-height: 220px; + padding: 0; + border-radius: 8px; + box-shadow: inset 0 0 0 0.5px var(--v2-border-border-strong); + overflow-x: hidden; + overflow-y: auto; +} + +.settings-v2-wsl-catalog-picker { + gap: 12px; +} + +.settings-v2-wsl-catalog-search { + width: 100%; +} + +.settings-v2-wsl-catalog-row { + min-height: 48px; +} + +.settings-v2-wsl-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 48px 24px; + font-size: 13px; + font-weight: 440; + line-height: 1; + color: var(--v2-text-text-muted); + text-align: center; +} diff --git a/packages/app/src/wsl/settings-model.test.ts b/packages/app/src/wsl/settings-model.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1bbe1db57f142e11368133a3ae483e47a73cfac --- /dev/null +++ b/packages/app/src/wsl/settings-model.test.ts @@ -0,0 +1,231 @@ +import { describe, expect, test } from "bun:test" +import { + addableProbePlan, + addServerProbePlan, + addServerViewModel, + autoProbePlan, + createProbeFailureGate, + runAddableProbePlan, + wslOpencodeAction, + wslRuntimeRetryable, +} from "./settings-model" +import type { WslServersState } from "./types" + +const readyWslState = readyState() + +function readyState(input: Partial = {}): WslServersState { + return { + runtime: { available: true, version: "2.4.13.0", error: null }, + installed: [], + online: [], + distroProbes: {}, + opencodeChecks: {}, + pendingRestart: false, + servers: [], + job: null, + ...input, + } +} + +describe("WSL server settings presentation", () => { + test("retries only settled unsuccessful runtimes", () => { + expect(wslRuntimeRetryable({ kind: "starting" })).toBe(false) + expect(wslRuntimeRetryable({ kind: "ready", url: "http://127.0.0.1:4096", username: null, password: null })).toBe( + false, + ) + expect(wslRuntimeRetryable({ kind: "failed", message: "boom" })).toBe(true) + expect(wslRuntimeRetryable({ kind: "stopped" })).toBe(true) + }) + + test("offers install and update only when OpenCode needs attention", () => { + expect(wslOpencodeAction(undefined)).toBeUndefined() + expect( + wslOpencodeAction({ + distro: "Debian", + resolvedPath: null, + version: null, + expectedVersion: "1.2.3", + matchesDesktop: null, + error: null, + }), + ).toBe("wsl.onboarding.installOpencode") + expect( + wslOpencodeAction({ + distro: "Debian", + resolvedPath: "/usr/local/bin/opencode", + version: "1.2.2", + expectedVersion: "1.2.3", + matchesDesktop: false, + error: null, + }), + ).toBe("wsl.onboarding.updateOpencode") + expect( + wslOpencodeAction({ + distro: "Debian", + resolvedPath: "/usr/local/bin/opencode", + version: "1.2.3", + expectedVersion: "1.2.3", + matchesDesktop: true, + error: null, + }), + ).toBeUndefined() + }) + + test("plans addable distro probes with the selected distro first", () => { + const plan = addableProbePlan({ + state: readyWslState, + view: "main", + adding: false, + selectedDistro: "Ubuntu", + addableInstalledDistros: [ + { name: "Debian", version: 2, isDefault: true }, + { name: "Ubuntu", version: 2, isDefault: false }, + ], + }) + + expect(plan?.key).toBe("distro:Ubuntu|distro:Debian") + expect(plan?.distros).toEqual(["Ubuntu", "Debian"]) + }) + + test("plans bootstrap probes for missing runtime and initial distro lists", () => { + expect(autoProbePlan({ state: undefined, busy: false })).toBeUndefined() + expect(autoProbePlan({ state: { ...readyWslState, runtime: null }, busy: false })).toEqual({ + key: "runtime", + action: "probeRuntime", + }) + expect(autoProbePlan({ state: readyWslState, busy: false })).toEqual({ + key: "distros", + action: "refreshDistros", + }) + }) + + test("uses one command plan for bootstrap before addable distro probing", () => { + expect( + addServerProbePlan({ + state: { ...readyWslState, runtime: null }, + view: "main", + adding: false, + busy: false, + selectedDistro: null, + addableInstalledDistros: [{ name: "Debian", version: 2, isDefault: true }], + }), + ).toEqual({ kind: "auto", key: "auto:runtime", plan: { key: "runtime", action: "probeRuntime" } }) + + expect( + addServerProbePlan({ + state: { + ...readyWslState, + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + }, + view: "main", + adding: false, + busy: false, + selectedDistro: "Debian", + addableInstalledDistros: [{ name: "Debian", version: 2, isDefault: true }], + }), + ).toEqual({ kind: "addable", key: "addable:distro:Debian", plan: { key: "distro:Debian", distros: ["Debian"] } }) + }) + + test("does not accept the same failed probe command until reset", () => { + const gate = createProbeFailureGate() + + expect(gate.accepts("addable:distro:Debian")).toBe(true) + gate.settle("addable:distro:Debian", new Error("wsl failed")) + expect(gate.accepts("addable:distro:Debian")).toBe(false) + expect(gate.accepts("addable:distro:Ubuntu")).toBe(true) + gate.reset() + expect(gate.accepts("addable:distro:Debian")).toBe(true) + }) + + test("keeps default distro selection stable while probes resolve", () => { + const model = addServerViewModel({ + state: { + ...readyWslState, + installed: [ + { name: "Debian", version: 2, isDefault: true }, + { name: "Ubuntu", version: 2, isDefault: false }, + ], + online: [{ name: "Alpine", label: "Alpine Linux" }], + distroProbes: { + Ubuntu: { name: "Ubuntu", canExecute: true, hasBash: true, hasCurl: true, error: null }, + }, + }, + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: false, + }) + + expect(model.selectedDistro).toBe("Debian") + }) + + test("keeps the dialog busy across serial addable probe job gaps", () => { + const model = addServerViewModel({ + state: readyState({ + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + }), + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: true, + }) + + expect(model.busy).toBe(true) + }) + + test("does not report ready when OpenCode is present but cannot run", () => { + const model = addServerViewModel({ + state: { + ...readyWslState, + installed: [{ name: "Debian", version: 2, isDefault: true }], + online: [{ name: "Ubuntu", label: "Ubuntu" }], + distroProbes: { + Debian: { name: "Debian", canExecute: true, hasBash: true, hasCurl: true, error: null }, + }, + opencodeChecks: { + Debian: { + distro: "Debian", + resolvedPath: "/home/me/.opencode/bin/opencode", + version: null, + expectedVersion: "1.2.3", + matchesDesktop: null, + error: "opencode is installed but could not run", + }, + }, + }, + view: "main", + selectedDistro: null, + catalogSearch: "", + catalogTarget: null, + adding: false, + probingAddable: false, + }) + + expect(model.distroStatuses.Debian).toEqual({ + label: { key: "wsl.onboarding.installOpencode" }, + tone: "warning", + }) + expect(model.primaryButton.action).toBe("install-opencode") + }) + + test("delegates addable probe plans to one batch command", async () => { + const calls: string[][] = [] + + await runAddableProbePlan({ + plan: { key: "distro:Debian|distro:Ubuntu", distros: ["Debian", "Ubuntu"] }, + api: { + probeAddable: async (distros) => { + calls.push(distros) + }, + }, + }) + + expect(calls).toEqual([["Debian", "Ubuntu"]]) + }) +}) diff --git a/packages/app/src/wsl/settings-model.ts b/packages/app/src/wsl/settings-model.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a653cddb23b12515ea6fe98a36acc2b65758018 --- /dev/null +++ b/packages/app/src/wsl/settings-model.ts @@ -0,0 +1,341 @@ +import fuzzysort from "fuzzysort" +import type { + WslInstalledDistro, + WslOnlineDistro, + WslOpencodeCheck, + WslServersPlatform, + WslServerRuntime, + WslServersState, +} from "./types" + +export type AddServerText = { + key: string + params?: Record +} + +export type DistroStatusTone = "success" | "warning" | "muted" + +export type DistroStatus = { + label: AddServerText + tone: DistroStatusTone +} + +export type AddServerPrimaryButton = { + variant: "neutral" | "contrast" + label: AddServerText + disabled: boolean + action: "install-opencode" | "add" | null + loading: boolean + width: string | null +} + +export type AddServerRuntimeState = "loading" | "pendingRestart" | "checking" | "unavailable" | "ready" + +export type AddableProbePlan = { + key: string + distros: string[] +} + +export type AutoProbePlan = { key: "runtime"; action: "probeRuntime" } | { key: "distros"; action: "refreshDistros" } + +export type AddServerProbePlan = + | { kind: "auto"; key: string; plan: AutoProbePlan } + | { kind: "addable"; key: string; plan: AddableProbePlan } + +export type WslAddServerView = "main" | "catalog" + +function isHiddenDistro(name: string) { + return /^docker-desktop(?:-data)?$/i.test(name) +} + +export const wslRuntimeRetryable = (runtime: WslServerRuntime) => + runtime.kind === "failed" || runtime.kind === "stopped" + +export function wslOpencodeAction(check?: WslOpencodeCheck) { + if (!check) return + if (!check.resolvedPath) return "wsl.onboarding.installOpencode" + if (check.matchesDesktop === false) return "wsl.onboarding.updateOpencode" +} + +export function wslDistroReady(state: WslServersState | undefined, name: string) { + const installed = state?.installed.find((item) => item.name === name) + const probe = state?.distroProbes[name] + if (!probe || !installed) return false + if (installed.version === 1) return false + return probe.canExecute && probe.hasBash && probe.hasCurl +} + +export function addServerViewModel(input: { + state: WslServersState | undefined + view: WslAddServerView + selectedDistro: string | null + catalogSearch: string + catalogTarget: string | null + adding: boolean + probingAddable: boolean +}) { + const state = input.state + const visibleInstalledDistros = (state?.installed ?? []).filter((item) => !isHiddenDistro(item.name)) + const visibleOnlineDistros = (state?.online ?? []).filter((item) => !isHiddenDistro(item.name)) + const existingServerDistros = new Set((state?.servers ?? []).map((item) => item.config.distro)) + const addableInstalledDistros = visibleInstalledDistros.filter((item) => !existingServerDistros.has(item.name)) + const selectedDistro = addServerSelectedDistro(input.selectedDistro, visibleInstalledDistros, addableInstalledDistros) + const opencodeCheck = selectedDistro ? (state?.opencodeChecks[selectedDistro] ?? null) : null + const installableDistros = addServerInstallableDistros(visibleInstalledDistros, visibleOnlineDistros) + const filteredInstallableDistros = addServerFilteredInstallableDistros(installableDistros, input.catalogSearch) + const catalogTarget = addServerCatalogTarget(input.catalogTarget, filteredInstallableDistros) + const busy = !!state?.job || input.adding || input.probingAddable + + return { + busy, + runtimeState: addServerRuntimeState(state), + visibleInstalledDistros, + visibleOnlineDistros, + addableInstalledDistros, + selectedDistro, + opencodeCheck, + wslReady: !!state?.runtime?.available && !state?.pendingRestart, + distroStatuses: Object.fromEntries( + addableInstalledDistros.flatMap((item) => { + const status = addServerDistroStatus({ state, name: item.name, probingAddable: input.probingAddable }) + if (!status) return [] + return [[item.name, status]] + }), + ) as Record, + primaryButton: addServerPrimaryButton({ + state, + selectedDistro, + opencodeCheck, + adding: input.adding, + probingAddable: input.probingAddable, + }), + installableDistros, + filteredInstallableDistros, + catalogTarget, + installingCatalogDistro: state?.job?.kind === "install-distro", + } +} + +function addServerSelectedDistro( + selected: string | null, + visibleInstalledDistros: WslInstalledDistro[], + addableInstalledDistros: WslInstalledDistro[], +) { + if (selected && addableInstalledDistros.some((item) => item.name === selected && item.version !== 1)) return selected + const defaultDistro = visibleInstalledDistros.find((item) => item.isDefault) + if ( + defaultDistro && + defaultDistro.version !== 1 && + addableInstalledDistros.some((item) => item.name === defaultDistro.name) + ) { + return defaultDistro.name + } + return addableInstalledDistros.find((item) => item.version !== 1)?.name ?? null +} + +function addServerRuntimeState(state: WslServersState | undefined): AddServerRuntimeState { + if (!state) return "loading" + if (state.pendingRestart) return "pendingRestart" + if (!state.runtime) return "checking" + if (!state.runtime.available) return "unavailable" + return "ready" +} + +function addServerDistroStatus(input: { + state: WslServersState | undefined + name: string + probingAddable: boolean +}): DistroStatus | undefined { + const installed = input.state?.installed.find((item) => item.name === input.name) + if (installed?.version === 1) return { label: { key: "wsl.onboarding.distroStatus.unsupported" }, tone: "muted" } + const job = input.state?.job + const probe = input.state?.distroProbes[input.name] + if (!probe) { + if (input.probingAddable || (job?.kind === "probe-addable" && job.distros.includes(input.name))) { + return checkingStatus() + } + return + } + if (!probe.canExecute) { + if (!installed) { + return { label: { key: "wsl.onboarding.distroNotInstalled", params: { distro: input.name } }, tone: "warning" } + } + return { label: { key: "wsl.onboarding.openDistroOnce", params: { distro: input.name } }, tone: "warning" } + } + if (!probe.hasBash || !probe.hasCurl) { + return { label: { key: "wsl.onboarding.distroStatus.missingTools" }, tone: "warning" } + } + const check = input.state?.opencodeChecks[input.name] + if (!check) { + if (input.probingAddable || (job?.kind === "probe-addable" && job.distros.includes(input.name))) { + return checkingStatus() + } + return + } + if (check.matchesDesktop === false) return { label: { key: "wsl.onboarding.updateOpencode" }, tone: "warning" } + if (!check.resolvedPath) return { label: { key: "wsl.onboarding.distroStatus.opencodeMissing" }, tone: "warning" } + if (check.error) return { label: { key: "wsl.onboarding.installOpencode" }, tone: "warning" } + return { label: { key: "wsl.onboarding.distroStatus.ready" }, tone: "success" } +} + +function checkingStatus(): DistroStatus { + return { label: { key: "wsl.onboarding.distroStatus.checking" }, tone: "muted" } +} + +function addServerPrimaryButton(input: { + state: WslServersState | undefined + selectedDistro: string | null + opencodeCheck: WslOpencodeCheck | null + adding: boolean + probingAddable: boolean +}): AddServerPrimaryButton { + const ready = !!input.selectedDistro && wslDistroReady(input.state, input.selectedDistro) + const probingSelected = input.probingAddable && !addServerSelectedDistroSettled(input.state, input.selectedDistro) + const probingOpencode = + probingSelected || + (ready && + (!input.opencodeCheck || + (!!input.selectedDistro && + input.state?.job?.kind === "probe-addable" && + input.state.job.distros.includes(input.selectedDistro)))) + const installingOpencode = + input.state?.job?.kind === "install-opencode" && input.state.job.distro === input.selectedDistro + if (!ready || probingOpencode) { + return { + variant: "contrast", + label: probingSelected ? { key: "wsl.onboarding.distroStatus.checking" } : { key: "wsl.server.add" }, + disabled: true, + action: null, + loading: probingSelected, + width: null, + } + } + if (!addServerOpencodeReady(input.opencodeCheck)) { + const update = !!input.opencodeCheck?.resolvedPath && input.opencodeCheck.matchesDesktop === false + return { + variant: "neutral", + label: installingOpencode + ? { key: "wsl.onboarding.updatingOpencode" } + : update + ? { key: "wsl.onboarding.updateOpencode" } + : { key: "wsl.onboarding.installOpencode" }, + disabled: !!input.state?.job || input.adding, + action: "install-opencode", + loading: installingOpencode, + width: update ? "138px" : "129px", + } + } + return { + variant: "contrast", + label: input.adding ? { key: "wsl.onboarding.adding" } : { key: "wsl.server.add" }, + disabled: input.adding || !!input.state?.job, + action: "add", + loading: input.adding, + width: null, + } +} + +function addServerOpencodeReady(check: WslOpencodeCheck | null) { + return !!check?.resolvedPath && check.matchesDesktop !== false && !check.error +} + +function addServerSelectedDistroSettled(state: WslServersState | undefined, selectedDistro: string | null) { + if (!selectedDistro) return false + const installed = state?.installed.find((item) => item.name === selectedDistro) + if (installed?.version === 1) return false + if (!state?.distroProbes[selectedDistro]) return false + if (!wslDistroReady(state, selectedDistro)) return true + return !!state.opencodeChecks[selectedDistro] +} + +function addServerInstallableDistros(installedDistros: WslInstalledDistro[], onlineDistros: WslOnlineDistro[]) { + const installed = new Set(installedDistros.map((item) => item.name)) + const hasVersionedUbuntu = onlineDistros.some((item) => /^Ubuntu-\d/.test(item.name)) + return onlineDistros + .filter((item) => !installed.has(item.name)) + .filter((item) => item.name !== "Ubuntu" || !hasVersionedUbuntu) +} + +function addServerFilteredInstallableDistros(installableDistros: WslOnlineDistro[], search: string) { + const query = search.trim() + if (!query) return installableDistros + return fuzzysort.go(query, installableDistros, { keys: ["label", "name"] }).map((item) => item.obj) +} + +function addServerCatalogTarget(target: string | null, distros: WslOnlineDistro[]) { + if (target && distros.some((item) => item.name === target)) return target + return distros[0]?.name ?? null +} + +export function addableProbePlan(input: { + state: WslServersState | undefined + view: WslAddServerView + adding: boolean + selectedDistro: string | null + addableInstalledDistros: WslInstalledDistro[] +}) { + const state = input.state + if (!state?.runtime?.available || state.pendingRestart || input.view !== "main" || input.adding) return + if (state.job) return + const ordered = input.selectedDistro + ? [ + ...input.addableInstalledDistros.filter((item) => item.name === input.selectedDistro), + ...input.addableInstalledDistros.filter((item) => item.name !== input.selectedDistro), + ] + : input.addableInstalledDistros + const pending = ordered.flatMap((item) => { + if (item.version === 1) return [] + if (!state.distroProbes[item.name]) return [`distro:${item.name}`] + if (wslDistroReady(state, item.name) && !state.opencodeChecks[item.name]) return [`opencode:${item.name}`] + return [] + }) + if (!pending.length) return + return { + key: pending.join("|"), + distros: ordered.filter((item) => item.version !== 1).map((item) => item.name), + } satisfies AddableProbePlan +} + +export function autoProbePlan(input: { state: WslServersState | undefined; busy: boolean }) { + if (!input.state || input.busy || input.state.pendingRestart) return + if (!input.state.runtime) return { key: "runtime", action: "probeRuntime" } satisfies AutoProbePlan + if (!input.state.runtime.available) return + if (input.state.installed.length || input.state.online.length) return + return { key: "distros", action: "refreshDistros" } satisfies AutoProbePlan +} + +export function addServerProbePlan(input: { + state: WslServersState | undefined + view: WslAddServerView + adding: boolean + busy: boolean + selectedDistro: string | null + addableInstalledDistros: WslInstalledDistro[] +}) { + const auto = autoProbePlan({ state: input.state, busy: input.busy }) + if (auto) return { kind: "auto", key: `auto:${auto.key}`, plan: auto } satisfies AddServerProbePlan + const addable = addableProbePlan(input) + if (addable) return { kind: "addable", key: `addable:${addable.key}`, plan: addable } satisfies AddServerProbePlan +} + +export function createProbeFailureGate() { + let failed: string | undefined + return { + accepts(key: string) { + return key !== failed + }, + settle(key: string, error?: unknown) { + if (error) failed = key + }, + reset() { + failed = undefined + }, + } +} + +export async function runAddableProbePlan(input: { + plan: AddableProbePlan + api: Pick +}) { + await input.api.probeAddable(input.plan.distros) +} diff --git a/packages/app/src/wsl/settings.tsx b/packages/app/src/wsl/settings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..901f2476686d9e9d340e6e16282ef65bd26b8e07 --- /dev/null +++ b/packages/app/src/wsl/settings.tsx @@ -0,0 +1,173 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Tag } from "@opencode-ai/ui/v2/badge-v2" +import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" +import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" +import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" +import { useMutation } from "@tanstack/solid-query" +import fuzzysort from "fuzzysort" +import { type Accessor, For, Show, createMemo } from "solid-js" +import type { useServerManagementController } from "@/components/dialog-select-server" +import { ServerHealthIndicator } from "@/components/server/server-row" +import { useLanguage } from "@/context/language" +import { usePlatform } from "@/context/platform" +import { ServerConnection } from "@/context/server" +import { showToast } from "@/utils/toast" +import { DialogAddWslServer } from "./dialog-add-server" +import { useWslServers } from "./context" +import { wslOpencodeAction, wslRuntimeRetryable } from "./settings-model" + +type Controller = ReturnType + +export function isWslServer(server: ServerConnection.Any) { + return server.type === "sidecar" && server.variant === "wsl" +} + +export function AddServerMenu(props: { onAddServer: () => void }) { + const platform = usePlatform() + const dialog = useDialog() + const language = useLanguage() + const openAddWsl = () => { + dialog.push(() => ) + } + return ( + + {language.t("dialog.server.add.button")} + + } + > + + + {language.t("dialog.server.add.button")} + + + + {language.t("dialog.server.add.button")} + {language.t("wsl.server.add")} + + + + + ) +} + +export function useFilteredWslServers(filter: Accessor) { + const wsl = useWslServers() + return createMemo(() => { + const servers = wsl.data?.servers ?? [] + const query = filter().trim() + if (!query) return servers + return fuzzysort + .go(query, servers, { keys: [(item) => item.config.distro, (item) => item.config.id] }) + .map((x) => x.obj) + }) +} + +export function WslServerSettings(props: { + controller: Controller + servers: ReturnType +}) { + const platform = usePlatform() + const language = useLanguage() + const wsl = useWslServers() + const api = platform.wslServers + + const request = useMutation(() => ({ + mutationFn: (action: () => Promise) => action(), + onError: (error) => + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: error instanceof Error ? error.message : String(error), + }), + })) + + const remove = (key: ServerConnection.Key) => { + request.mutate(() => props.controller.handleRemove(key)) + } + + return ( + + + {(item) => { + const key = ServerConnection.Key.make(item.config.id) + const check = () => wsl.data?.opencodeChecks[item.config.distro] + const opencodeAction = () => wslOpencodeAction(check()) + const busy = () => wsl.data?.job?.kind === "install-opencode" && wsl.data.job.distro === item.config.distro + return ( +
+
+ +
+ + {item.config.distro} + + {language.t("wsl.server.label")} + + + + {(version) => `v${version()}`} + +
+
+
+ + {language.t("dialog.server.status.default")} + + + {(label) => ( + api && request.mutate(() => api.installOpencode(item.config.distro))} + > + {busy() ? language.t("wsl.server.updating") : language.t(label())} + + )} + + + } + aria-label={language.t("common.moreOptions")} + /> + + + + {language.t("wsl.server.menu.label")} + + api && request.mutate(() => api.startServer(key))}> + {language.t("wsl.server.retryStart")} + + + + props.controller.setDefault(key)}> + {language.t("dialog.server.menu.default")} + + + + props.controller.setDefault(null)}> + {language.t("dialog.server.menu.defaultRemove")} + + + + remove(key)}> + {language.t("dialog.server.menu.delete")} + + + + + +
+
+ ) + }} +
+
+ ) +} diff --git a/packages/app/src/wsl/types.ts b/packages/app/src/wsl/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..0dd44a54d14666870e86ef4c6b0f94b662817385 --- /dev/null +++ b/packages/app/src/wsl/types.ts @@ -0,0 +1,85 @@ +export type WslRuntimeCheck = { + available: boolean + version: string | null + error: string | null +} + +export type WslInstalledDistro = { + name: string + version: number | null + isDefault: boolean +} + +export type WslOnlineDistro = { + name: string + label: string +} + +export type WslDistroProbe = { + name: string + canExecute: boolean + hasBash: boolean + hasCurl: boolean + error: string | null +} + +export type WslOpencodeCheck = { + distro: string + resolvedPath: string | null + version: string | null + expectedVersion: string | null + matchesDesktop: boolean | null + error: string | null +} + +export type WslServerConfig = { + id: string + distro: string +} + +export type WslServerRuntime = + | { kind: "starting" } + | { kind: "ready"; url: string; username: string | null; password: string | null } + | { kind: "failed"; message: string } + | { kind: "stopped" } + +export type WslServerItem = { + config: WslServerConfig + runtime: WslServerRuntime +} + +export type WslJob = + | { kind: "runtime"; startedAt: number } + | { kind: "distros"; startedAt: number } + | { kind: "install-wsl"; startedAt: number } + | { kind: "install-distro"; distro: string; startedAt: number } + | { kind: "probe-addable"; distros: string[]; startedAt: number } + | { kind: "install-opencode"; distro: string; startedAt: number } + +export type WslServersState = { + runtime: WslRuntimeCheck | null + installed: WslInstalledDistro[] + online: WslOnlineDistro[] + distroProbes: Record + opencodeChecks: Record + pendingRestart: boolean + servers: WslServerItem[] + job: WslJob | null +} + +export type WslServersEvent = { type: "state"; state: WslServersState } + +export type WslServersPlatform = { + getState(): Promise + subscribe(cb: (event: WslServersEvent) => void): () => void + probeRuntime(): Promise + refreshDistros(): Promise + installWsl(): Promise + installDistro(name: string): Promise + probeAddable(distros: string[]): Promise + installOpencode(name: string): Promise + openTerminal(name: string): Promise + addServer(distro: string): Promise + removeServer(id: string): Promise + startServer(id: string): Promise +} diff --git a/packages/app/test-browser/command-palette.test.ts b/packages/app/test-browser/command-palette.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a74834fd0762bd758b90014d1a1b624cd68371c --- /dev/null +++ b/packages/app/test-browser/command-palette.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, test } from "bun:test" +import type { Project } from "@opencode-ai/sdk/v2/client" +import type { SessionInfo } from "@opencode-ai/client/promise" +import { createRoot } from "solid-js" +import { createServerSessionEntries } from "@/components/command-palette" +import type { LocalProject } from "@/context/layout" +import { ServerConnection } from "@/context/server" +import { getProjectAvatarSource } from "@/pages/layout/helpers" + +const stored: Project = { + id: "project-1", + name: "Palette project", + worktree: "/workspace/project", + sandboxes: [], + time: { created: 1, updated: 1 }, +} + +const session: SessionInfo = { + id: "session-1", + projectID: stored.id, + agent: "build", + model: { id: "model-1", providerID: "provider-1" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + location: { directory: stored.worktree }, + title: "Palette session", + time: { created: 1, updated: 2 }, +} + +describe("command palette sessions", () => { + test("uses the home project avatar and cancels superseded searches", async () => { + const server = ServerConnection.Key.make("selected-server") + const opened: LocalProject = { + ...stored, + icon: { override: "home-project-avatar" }, + expanded: true, + } + const searches: string[] = [] + const result = await new Promise>>>( + (resolve, reject) => { + createRoot((dispose) => { + const search = createServerSessionEntries({ + server, + opened: () => [opened], + stored: () => [{ ...stored, icon: { url: "stored-project-avatar" } }], + load: async (text) => { + searches.push(text) + return { + data: [session, { ...session, id: "archived-session", time: { ...session.time, archived: 3 } }], + } + }, + untitled: () => "Untitled", + category: () => "Sessions", + }) + const first = search("palette") + const second = search("palette session") + Promise.all([first, second]) + .then(([cancelled, entries]) => { + expect(cancelled).toEqual([]) + resolve(entries) + }) + .catch(reject) + .finally(dispose) + }) + }, + ) + + expect(searches).toEqual(["palette session"]) + expect(result).toHaveLength(1) + expect(getProjectAvatarSource(result[0]?.project?.id, result[0]?.project?.icon)).toBe("home-project-avatar") + expect(result[0]).toMatchObject({ + server, + sessionID: session.id, + description: stored.name, + project: { id: stored.id, icon: opened.icon }, + }) + }) +}) diff --git a/packages/app/test-browser/motion-spring.test.ts b/packages/app/test-browser/motion-spring.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc251af0088bc66ea680ed411511b715730a7f64 --- /dev/null +++ b/packages/app/test-browser/motion-spring.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from "bun:test" +import { useSpring } from "@opencode-ai/ui/motion-spring" +import { createRoot, createSignal } from "solid-js" + +test("snaps spring progress when the session changes", async () => { + const state = createRoot((dispose) => { + const [target, setTarget] = createSignal(0) + const [session, setSession] = createSignal("session-a") + const progress = useSpring(target, { visualDuration: 0.3, bounce: 0 }, session) + return { dispose, progress, setTarget, setSession } + }) + + await new Promise(queueMicrotask) + state.setTarget(1) + expect(state.progress()).toBe(0) + + state.setSession("session-b") + expect(state.progress()).toBe(1) + state.dispose() +}) diff --git a/packages/app/test-browser/prompt-attachments.test.ts b/packages/app/test-browser/prompt-attachments.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..49dad279cdf20983d751288eb7005f684193e344 --- /dev/null +++ b/packages/app/test-browser/prompt-attachments.test.ts @@ -0,0 +1,175 @@ +import { describe, expect, test } from "bun:test" +import { createRoot } from "solid-js" +import { createStore } from "solid-js/store" +import { createPromptAttachmentsCore } from "@/components/prompt-input/attachments" +import { createPromptState } from "@/context/prompt" +import { createPromptInputV2Attachments } from "../../session-ui/src/v2/components/prompt-input/attachments" +import type { PromptInputV2Prompt } from "../../session-ui/src/v2/components/prompt-input/types" + +describe("prompt attachment session ownership", () => { + test("adds an asynchronously read image to the session where the read started", async () => { + await createRoot(async (dispose) => { + const sessions = { A: createPromptState(), B: createPromptState() } + let active: "A" | "B" = "A" + const attachments = createPromptAttachmentsCore({ + capture: () => sessions[active].capture(), + editor: () => document.createElement("div"), + }) + const pending = attachments.addAttachment(new File([new Uint8Array(1024 * 1024)], "a.png", { type: "image/png" })) + + active = "B" + await pending + + expect(images(sessions.A)).toHaveLength(1) + expect(images(sessions.B)).toHaveLength(0) + dispose() + }) + }) + + test("finishes the captured attachment after the active editor is removed", async () => { + await createRoot(async (dispose) => { + const prompt = createPromptState() + let editor: HTMLDivElement | undefined = document.createElement("div") + const attachments = createPromptAttachmentsCore({ + capture: prompt.capture, + editor: () => editor, + }) + const pending = attachments.addAttachment(new File([new Uint8Array(1024 * 1024)], "a.png", { type: "image/png" })) + + editor = undefined + await pending + + expect(images(prompt)).toHaveLength(1) + dispose() + }) + }) + + test("keeps every file in a batch on the session where the batch started", async () => { + await createRoot(async (dispose) => { + const sessions = { A: createPromptState(), B: createPromptState() } + let active: "A" | "B" = "A" + const attachments = createPromptAttachmentsCore({ + capture: () => sessions[active].capture(), + editor: () => document.createElement("div"), + }) + const pending = attachments.addAttachments([ + new File([new Uint8Array(1024 * 1024)], "first.png", { type: "image/png" }), + new File([new Uint8Array(1024 * 1024)], "second.png", { type: "image/png" }), + ]) + + active = "B" + await pending + + expect(images(sessions.A)).toHaveLength(2) + expect(images(sessions.B)).toHaveLength(0) + dispose() + }) + }) + + test("keeps a delayed native clipboard image on the session where paste started", async () => { + await createRoot(async (dispose) => { + const sessions = { A: createPromptState(), B: createPromptState() } + const read = Promise.withResolvers() + let active: "A" | "B" = "A" + const attachments = createPromptAttachmentsCore({ + capture: () => sessions[active].capture(), + editor: () => document.createElement("div"), + }) + const pending = attachments.addClipboardAttachment(read.promise) + + active = "B" + read.resolve(new File([new Uint8Array(1024 * 1024)], "clipboard.png", { type: "image/png" })) + await pending + + expect(images(sessions.A)).toHaveLength(1) + expect(images(sessions.B)).toHaveLength(0) + dispose() + }) + }) +}) + +test("rejects a duplicate native clipboard attachment in the V2 prompt store", async () => { + await createRoot(async (dispose) => { + const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) + const duplicate = Promise.withResolvers() + const files = [ + new File(["hello"], "clipboard-1.txt", { type: "text/plain" }), + new File(["hello"], "clipboard-2.txt", { type: "text/plain" }), + ] + const attachments = createPromptInputV2Attachments({ + capture: () => ({ + current: () => state.prompt, + cursor: () => 0, + set: (prompt) => setState("prompt", prompt), + }), + editor: () => document.createElement("div"), + focusEditor: () => undefined, + addPart: () => false, + setDraggingType: () => undefined, + directory: () => "/", + isDialogActive: () => false, + warn: () => undefined, + duplicate: duplicate.resolve, + onError: () => undefined, + readClipboardImage: async () => files.shift() ?? null, + }) + const event = { + clipboardData: { items: [], getData: () => "" }, + preventDefault: () => undefined, + stopPropagation: () => undefined, + } as unknown as ClipboardEvent + + await attachments.handlePaste(event) + await attachments.handlePaste(event) + await duplicate.promise + + expect(state.prompt).toHaveLength(1) + dispose() + }) +}) + +test("rejects desktop duplicates and keeps changed files in the V2 prompt store", async () => { + await createRoot(async (dispose) => { + const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) + const duplicates: string[] = [] + const attachments = createPromptInputV2Attachments({ + capture: () => ({ + current: () => state.prompt, + cursor: () => 0, + set: (prompt) => setState("prompt", prompt), + }), + editor: () => document.createElement("div"), + focusEditor: () => undefined, + addPart: () => false, + setDraggingType: () => undefined, + directory: () => "/", + isDialogActive: () => false, + warn: () => undefined, + duplicate: () => duplicates.push("duplicate"), + onError: () => undefined, + getPathForFile: (file) => (file.name === "browser.txt" ? "" : `/tmp/${file.name}`), + }) + const first = new File(["first"], "a.txt", { type: "text/plain" }) + const second = new File(["second"], "b.txt", { type: "text/plain" }) + + await attachments.addAttachments([first, second]) + await attachments.addAttachments([first, second]) + expect(state.prompt).toHaveLength(2) + expect(duplicates).toEqual(["duplicate", "duplicate"]) + + await attachments.addAttachments([new File(["edited"], "a.txt", { type: "text/plain" })]) + expect(state.prompt).toHaveLength(3) + + await attachments.addAttachments([ + new File(["same"], "browser.txt", { type: "text/plain" }), + new File(["same"], "browser.txt", { type: "text/plain" }), + ]) + expect(state.prompt).toHaveLength(4) + expect(duplicates).toHaveLength(3) + dispose() + }) +}) + +function images(prompt: ReturnType) { + return prompt.current().filter((part) => part.type === "image") +} diff --git a/packages/app/test-browser/prompt-persistence.test.ts b/packages/app/test-browser/prompt-persistence.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c3d28226f434014837bb5cab7757526552a7414 --- /dev/null +++ b/packages/app/test-browser/prompt-persistence.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, test } from "bun:test" +import type { AsyncStorage } from "@solid-primitives/storage" +import { createEffect, createRoot } from "solid-js" +import type { Platform } from "@/context/platform" +import { createPromptReady, createPromptSession } from "@/context/prompt-state" +import { ServerScope } from "@/utils/server-scope" +import { createDraftStore } from "@/utils/draft-store" + +let read: ((value: string | null) => void) | undefined + +const storage: AsyncStorage = { + getItem: () => new Promise((resolve) => (read = resolve)), + setItem: async () => undefined, + removeItem: async () => undefined, + clear: async () => undefined, + key: async () => null, + getLength: async () => 0, + length: Promise.resolve(0), +} + +const platform: Platform = { + platform: "web", + openExternal: () => undefined, + restart: async () => undefined, + notify: async () => undefined, + draftStore: { + ...storage, + putBlob: async () => { + throw new Error("putBlob is not used by this test") + }, + }, +} + +describe("prompt persistence", () => { + test("waits for an async draft to hydrate before reporting ready", async () => { + await new Promise((resolve, reject) => { + createRoot((dispose) => { + const session = createPromptSession(ServerScope.local, { draftID: "draft-async" }, undefined, platform) + const ready = createPromptReady(() => session) + + expect(ready()).toBe(false) + expect(session.current()[0]).toMatchObject({ type: "text", content: "" }) + + read?.( + JSON.stringify({ + prompt: [{ type: "text", content: "persisted draft", start: 0, end: 15 }], + cursor: 15, + context: { items: [] }, + }), + ) + + createEffect(() => { + if (!ready()) return + try { + expect(session.current()[0]).toMatchObject({ type: "text", content: "persisted draft" }) + dispose() + resolve() + } catch (error) { + dispose() + reject(error) + } + }) + }) + }) + }) +}) + +test("moves legacy image data URLs into blobs and hydrates object URLs", async () => { + const documents = new Map() + const blobs = new Map() + const store = createDraftStore({ + get: async (key) => documents.get(key) ?? null, + set: async (key, value) => void documents.set(key, value), + remove: async (key) => void documents.delete(key), + putBlob: async (blob) => { + const id = String(blob.size) + blobs.set(id, blob) + return id + }, + getBlob: async (id) => blobs.get(id) ?? null, + }) + + await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] })) + expect(documents.get("prompt")).not.toContain("dataUrl") + const value = JSON.parse((await store.getItem("prompt"))!) + expect(value.prompt[0].blob.id).toBe("1") + expect(value.prompt[0].blob.url).toStartWith("blob:") +}) + +test("does not let delayed blob migration overwrite a newer draft", async () => { + const documents = new Map() + const migration = Promise.withResolvers() + const store = createDraftStore({ + get: async () => null, + set: async (key, value) => void documents.set(key, value), + remove: async () => undefined, + putBlob: async () => { + await migration.promise + return "blob" + }, + getBlob: async () => null, + }) + const older = store.setItem( + "prompt", + JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] }), + ) + await Bun.sleep(0) + await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "text", content: "latest" }] })) + migration.resolve() + await older + + expect(documents.get("prompt")).toContain("latest") +}) diff --git a/packages/app/test-browser/prompt-scope.test.ts b/packages/app/test-browser/prompt-scope.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..fad19dcff77d80f3b67ed59748b9c27f34b3c84f --- /dev/null +++ b/packages/app/test-browser/prompt-scope.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from "bun:test" +import { ServerConnection } from "@/context/server" +import { selectPromptTab } from "@/context/prompt" +import type { Tab } from "@/context/tabs" + +test("selects the explicitly scoped session tab instead of the active tab", () => { + const server = ServerConnection.Key.make("local") + const tabs: Tab[] = [ + { type: "session", server, sessionId: "A" }, + { type: "session", server, sessionId: "B" }, + ] + + expect(selectPromptTab(tabs, { dir: "repo", id: "B" }, server)).toBe(tabs[1]) +}) diff --git a/packages/app/test-browser/prompt-submission-state.test.ts b/packages/app/test-browser/prompt-submission-state.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f7019d96be9e1adb0ea81ba31230955b97eb741 --- /dev/null +++ b/packages/app/test-browser/prompt-submission-state.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test" +import { createPromptState } from "@/context/prompt" +import { createPromptSubmissionState } from "@/components/prompt-input/submission-state" + +describe("prompt submission state", () => { + test("keeps failed submission restoration with the prompt where it started", () => { + const target = createPromptState() + const submission = createPromptSubmissionState({ + target, + prompt: [{ type: "text", content: "prompt-A", start: 0, end: 8 }], + context: [{ key: "file:src/index.ts:undefined:undefined", type: "file", path: "src/index.ts" }], + }) + + expect(submission.restore()).toEqual({ + target, + prompt: [{ type: "text", content: "prompt-A", start: 0, end: 8 }], + context: [{ key: "file:src/index.ts:undefined:undefined", type: "file", path: "src/index.ts" }], + }) + }) + + test("moves first-submit restoration and context to the promoted session", () => { + const draft = createPromptState() + const session = createPromptState() + const submission = createPromptSubmissionState({ + target: draft, + prompt: [{ type: "text", content: "first prompt", start: 0, end: 12 }], + context: [{ key: "file:src/index.ts:undefined:undefined", type: "file", path: "src/index.ts" }], + }) + + submission.retarget(session) + + expect(submission.restore()).toEqual({ + target: session, + prompt: [{ type: "text", content: "first prompt", start: 0, end: 12 }], + context: [{ key: "file:src/index.ts:undefined:undefined", type: "file", path: "src/index.ts" }], + }) + expect(session.context.items()).toHaveLength(1) + expect(session.context.items()[0]).toMatchObject({ type: "file", path: "src/index.ts" }) + }) + + test("clears the original first-submit prompt after retargeting", () => { + const workspace = createPromptState() + const session = createPromptState() + workspace.set([{ type: "text", content: "first prompt", start: 0, end: 12 }]) + const submission = createPromptSubmissionState({ + target: workspace, + prompt: workspace.current(), + context: [], + }) + + submission.retarget(session) + submission.clear() + + expect(workspace.current()[0]).toMatchObject({ type: "text", content: "" }) + expect(session.current()[0]).toMatchObject({ type: "text", content: "" }) + }) + + test("does not restore over a prompt edited after submission", () => { + const target = createPromptState() + target.set([{ type: "text", content: "submitted", start: 0, end: 9 }]) + const submission = createPromptSubmissionState({ + target, + prompt: target.current(), + context: [], + }) + + submission.clear() + target.set([{ type: "text", content: "new draft", start: 0, end: 9 }]) + + expect(submission.restore()).toBeUndefined() + expect(target.current()[0]).toMatchObject({ type: "text", content: "new draft" }) + }) +}) diff --git a/packages/app/test-browser/prompt-transient-state.test.ts b/packages/app/test-browser/prompt-transient-state.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5777c9c392a659273b4030fc76c5d78a9ab2c6bb --- /dev/null +++ b/packages/app/test-browser/prompt-transient-state.test.ts @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test" +import { createRoot, createSignal } from "solid-js" +import { createPromptInputTransientState } from "@/components/prompt-input/transient-state" + +test("resets transient prompt input state when the prompt session changes", () => { + createRoot((dispose) => { + const [identity, setIdentity] = createSignal("A") + const [state, setState] = createPromptInputTransientState(identity, 3) + setState({ + popover: "slash", + slashMenu: true, + slashMenuQuery: "compact", + historyIndex: 2, + savedPrompt: { + prompt: [{ type: "text", content: "draft-A", start: 0, end: 7 }], + comments: [], + }, + draggingType: "image", + mode: "shell", + applyingHistory: true, + }) + + setIdentity("B") + + expect(state).toMatchObject({ + popover: null, + slashMenu: false, + slashMenuQuery: "", + historyIndex: -1, + savedPrompt: null, + placeholder: 3, + draggingType: null, + mode: "normal", + applyingHistory: false, + }) + dispose() + }) +}) diff --git a/packages/app/test-browser/review-panel-v2-state.test.ts b/packages/app/test-browser/review-panel-v2-state.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a7862ce171a95c526a6a08d02c7b1b82ad7d43b --- /dev/null +++ b/packages/app/test-browser/review-panel-v2-state.test.ts @@ -0,0 +1,65 @@ +import { beforeAll, expect, mock, test } from "bun:test" +import type { AsyncStorage } from "@solid-primitives/storage" +import { createEffect, createRoot } from "solid-js" + +let createReviewPanelV2State: typeof import("@/pages/session/v2/review-panel-v2-state").createReviewPanelV2State +let read: ((value: string | null) => void) | undefined + +const storage: AsyncStorage = { + getItem: () => new Promise((resolve) => (read = resolve)), + setItem: async () => undefined, + removeItem: async () => undefined, + clear: async () => undefined, + key: async () => null, + getLength: async () => 0, + length: Promise.resolve(0), +} + +beforeAll(async () => { + mock.module("@opencode-ai/session-ui/v2/session-review-v2", () => ({ + SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT: 240, + SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN: 200, + SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX: 480, + })) + mock.module("@/context/platform", () => ({ + usePlatform: () => ({ platform: "desktop", storage: () => storage }), + })) + + createReviewPanelV2State = (await import("@/pages/session/v2/review-panel-v2-state")).createReviewPanelV2State +}) + +test("enables sidebar motion only after custom width hydration", async () => { + await new Promise((resolve, reject) => { + createRoot((dispose) => { + const state = createReviewPanelV2State() + const transition = + "sidebarTransition" in state && typeof state.sidebarTransition === "function" + ? (state.sidebarTransition as () => boolean) + : undefined + + try { + expect(transition).toBeFunction() + expect(transition?.()).toBeFalse() + expect(state.sidebarWidth()).toBe(240) + } catch (error) { + dispose() + reject(error) + return + } + + createEffect(() => { + if (!transition?.()) return + try { + expect(state.sidebarWidth()).toBe(360) + dispose() + resolve() + } catch (error) { + dispose() + reject(error) + } + }) + + read?.(JSON.stringify({ sidebarOpened: true, sidebarWidth: 360, expandMode: "collapse" })) + }) + }) +}) diff --git a/packages/app/test-browser/session-lineage.test.ts b/packages/app/test-browser/session-lineage.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c2154d190073445f61c7886e62e7af2493d8c05 --- /dev/null +++ b/packages/app/test-browser/session-lineage.test.ts @@ -0,0 +1,231 @@ +import { expect, test } from "bun:test" +import { createRoot, createSignal } from "solid-js" +import { createSessionLineage } from "@/pages/session/session-lineage" + +type Lineage = { session: { id: string; directory: string } } + +const lineageOf = (id: string): Lineage => ({ session: { id, directory: `/dir/${id}` } }) + +// Fake sync lineage store: peek reads a reactive cache, resolve returns a +// deferred promise the test settles or fails explicitly. The lineage memo is +// live (read below), so it recomputes eagerly on cache/status writes — throws +// surface at the write site, which is also where the enclosing ErrorBoundary +// would see them in the app. Assertions wrap write + read to cover both. +function createFixture(initial: Record = {}) { + const [cache, setCache] = createSignal(initial) + const deferred = new Map>() + const resolves: string[] = [] + return { + resolves, + lineage: { + peek: (id: string) => cache()[id], + resolve: (id: string) => { + resolves.push(id) + const entry = deferred.get(id) ?? Promise.withResolvers() + deferred.set(id, entry) + return entry.promise + }, + }, + settle(id: string) { + setCache({ ...cache(), [id]: lineageOf(id) }) + deferred.get(id)?.resolve(undefined) + }, + fail(id: string, error: unknown) { + deferred.get(id)?.reject(error) + // The real store does not cache failures: the inflight request entry is + // dropped on rejection so the next resolve retries (server-session.ts). + deferred.delete(id) + }, + remove(id: string) { + const next = { ...cache() } + delete next[id] + setCache(next) + }, + } +} + +// Two microtask ticks: one for the resolve promise handed back by the fixture, +// one for the .then/.catch chain inside createSessionLineage. +const flush = async () => { + await Promise.resolve() + await Promise.resolve() +} + +test("resolves an uncached session and exposes its lineage", async () => { + await createRoot(async (dispose) => { + const fixture = createFixture() + const current = createSessionLineage( + () => "ses_a", + () => fixture.lineage, + ) + + expect(current()).toBeUndefined() + await flush() + expect(fixture.resolves).toEqual(["ses_a"]) + + fixture.settle("ses_a") + await flush() + expect(current()?.session.id).toBe("ses_a") + + dispose() + }) +}) + +// Session tabs on the same server share one route instance, so navigating to +// another session changes the id in place; resolution must follow it instead +// of reporting the new session as missing. +test("re-resolves when navigating to an uncached session without a remount", async () => { + await createRoot(async (dispose) => { + const fixture = createFixture({ ses_a: lineageOf("ses_a") }) + const [id, setId] = createSignal("ses_a") + const current = createSessionLineage(id, () => fixture.lineage) + + await flush() + expect(current()?.session.id).toBe("ses_a") + + expect(() => { + setId("ses_b") + current() + }).not.toThrow() + expect(fixture.resolves).toEqual(["ses_b"]) + + fixture.settle("ses_b") + await flush() + expect(current()?.session.id).toBe("ses_b") + + dispose() + }) +}) + +// A late failure from a session the user already navigated away from must not +// poison the currently viewed session. +test("ignores a stale resolution failure after the target changes", async () => { + await createRoot(async (dispose) => { + const fixture = createFixture() + const [id, setId] = createSignal("ses_a") + const current = createSessionLineage(id, () => fixture.lineage) + + await flush() + setId("ses_b") + fixture.fail("ses_a", new Error("Session not found: ses_a")) + await flush() + + expect(() => current()).not.toThrow() + fixture.settle("ses_b") + await flush() + expect(current()?.session.id).toBe("ses_b") + + dispose() + }) +}) + +test("returning to a pruned session re-resolves instead of throwing not found", async () => { + await createRoot(async (dispose) => { + const fixture = createFixture() + const [id, setId] = createSignal("ses_a") + const current = createSessionLineage(id, () => fixture.lineage) + + await flush() + fixture.settle("ses_a") + await flush() + + setId("ses_b") + fixture.settle("ses_b") + await flush() + + fixture.remove("ses_a") + expect(() => { + setId("ses_a") + current() + }).not.toThrow() + expect(fixture.resolves).toEqual(["ses_a", "ses_b", "ses_a"]) + + fixture.settle("ses_a") + await flush() + expect(current()?.session.id).toBe("ses_a") + + dispose() + }) +}) + +// A resolution that fails while its session is unfocused must not leave a +// poisoned status behind: revisiting that session retries cleanly instead of +// rethrowing the stale failure before the retry can start. +test("revisiting a session whose resolution failed while unfocused retries cleanly", async () => { + await createRoot(async (dispose) => { + const fixture = createFixture() + const [id, setId] = createSignal("ses_a") + const current = createSessionLineage(id, () => fixture.lineage) + + await flush() + setId("ses_b") + fixture.fail("ses_a", new Error("resolve failed")) + await flush() + + expect(() => { + setId("ses_a") + current() + }).not.toThrow() + expect(fixture.resolves).toEqual(["ses_a", "ses_b", "ses_a"]) + + fixture.settle("ses_a") + await flush() + expect(current()?.session.id).toBe("ses_a") + + dispose() + }) +}) + +// The lineage accessor is reactive: replacing the sync store (for example after +// the server context is rebuilt) must gate out the old store's status and +// re-resolve against the new one instead of fabricating a not-found. +test("re-resolves against a replaced lineage store", async () => { + await createRoot(async (dispose) => { + const first = createFixture() + const second = createFixture() + const [store, setStore] = createSignal(first.lineage) + const current = createSessionLineage(() => "ses_a", store) + + await flush() + first.settle("ses_a") + await flush() + expect(current()?.session.id).toBe("ses_a") + + expect(() => { + setStore(second.lineage) + current() + }).not.toThrow() + await flush() + expect(second.resolves).toEqual(["ses_a"]) + + second.settle("ses_a") + await flush() + expect(current()?.session.id).toBe("ses_a") + + dispose() + }) +}) + +// The viewed session is pinned in the cache, so disappearing after settlement +// means it was deleted; the boundary must show the not found fallback. +test("throws not found when the settled session is deleted", async () => { + await createRoot(async (dispose) => { + const fixture = createFixture() + const current = createSessionLineage( + () => "ses_a", + () => fixture.lineage, + ) + + await flush() + fixture.settle("ses_a") + await flush() + expect(current()?.session.id).toBe("ses_a") + + expect(() => { + fixture.remove("ses_a") + current() + }).toThrow("Session not found: ses_a") + + dispose() + }) +}) diff --git a/packages/app/test-browser/session-ownership.test.ts b/packages/app/test-browser/session-ownership.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..2f81c7d59541f6a9cfd02784256deb5ba986bfae --- /dev/null +++ b/packages/app/test-browser/session-ownership.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test" +import { createRoot, createSignal } from "solid-js" +import { createSessionOwnership } from "@/pages/session/session-ownership" + +describe("createSessionOwnership", () => { + test("invalidates captured work when its Solid owner is disposed", () => { + let current = true + createRoot((dispose) => { + const owner = createSessionOwnership(() => "A").capture() + dispose() + current = owner.current() + }) + + expect(current).toBe(false) + }) + + test("does not run a continuation after navigation", () => { + createRoot((dispose) => { + const [session, setSession] = createSignal("A") + const owner = createSessionOwnership(session).capture() + let ran = false + + setSession("B") + owner.run(() => { + ran = true + }) + + expect(ran).toBe(false) + dispose() + }) + }) + + test("does not revive a continuation after A to B to A navigation", () => { + createRoot((dispose) => { + const [session, setSession] = createSignal("A") + const owner = createSessionOwnership(session).capture() + + setSession("B") + setSession("A") + + expect(owner.current()).toBe(false) + dispose() + }) + }) +}) diff --git a/packages/app/test-browser/settings-keybinds.test.ts b/packages/app/test-browser/settings-keybinds.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..75f0b755434164d5d318b4786a4cbb9e5386a9bc --- /dev/null +++ b/packages/app/test-browser/settings-keybinds.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from "bun:test" +import { createRoot } from "solid-js" +import { createKeybindSettingsController } from "../src/components/settings-keybinds" + +function setup(overrides: Record = {}) { + const changes: [string, string][] = [] + const suppression: boolean[] = [] + const notifications: { title: string; description?: string }[] = [] + let resets = 0 + let controller: ReturnType + + const dispose = createRoot((dispose) => { + controller = createKeybindSettingsController( + { + command: { + catalog: [ + { id: "session.alpha", title: "Alpha", keybind: "mod+a" }, + { id: "session.beta", title: "Beta", keybind: "mod+b" }, + ], + options: [], + keybinds: (enabled) => suppression.push(enabled), + }, + settings: { + current: { keybinds: overrides }, + keybinds: { + get: (id) => overrides[id], + set: (id, value) => { + overrides[id] = value + changes.push([id, value]) + }, + resetAll: () => { + resets++ + }, + }, + }, + notify: (toast) => notifications.push(toast), + }, + { + locale: () => "en", + t: (key, params) => { + if (params) return `${key}:${Object.values(params).join("|")}` + if (key === "common.key.alt") return "Alt" + return String(key) + }, + }, + ) + return dispose + }) + + return { + controller: controller!, + changes, + suppression, + notifications, + resets: () => resets, + dispose, + } +} + +function modKey(key: string) { + const mac = /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) + return new KeyboardEvent("keydown", { key, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true }) +} + +describe("keybind settings controller", () => { + test("derives the catalog, effective bindings, and filtered groups", () => { + const state = setup({ "session.beta": "alt+k" }) + + expect(state.controller.catalog.title("session.alpha")).toBe("Alpha") + expect(state.controller.catalog.keybind("session.beta")).toBe("Alt+K") + expect(state.controller.catalog.filtered("alt k").get("Session")).toEqual(["session.beta"]) + expect(state.controller.settings.hasOverrides()).toBe(true) + + state.dispose() + }) + + test("captures bindings, rejects conflicts, and restores command handling", () => { + const state = setup() + + state.controller.capture.toggle("session.beta") + document.dispatchEvent(modKey("a")) + expect(state.changes).toEqual([]) + expect(state.notifications).toHaveLength(1) + expect(state.controller.capture.active()).toBe("session.beta") + + document.dispatchEvent(modKey("x")) + expect(state.changes).toEqual([["session.beta", "mod+x"]]) + expect(state.suppression).toEqual([false, true]) + expect(state.controller.capture.active()).toBeNull() + + state.controller.capture.toggle("session.alpha") + state.dispose() + expect(state.suppression).toEqual([false, true, false, true]) + document.dispatchEvent(modKey("z")) + expect(state.changes).toEqual([["session.beta", "mod+x"]]) + }) + + test("resets persisted overrides and reports success", () => { + const state = setup({ "session.alpha": "none" }) + + state.controller.settings.reset() + expect(state.resets()).toBe(1) + expect(state.notifications[0]?.title).toBe("settings.shortcuts.reset.toast.title") + + state.dispose() + }) +}) diff --git a/packages/app/test-browser/solid-router-cleanup.test.ts b/packages/app/test-browser/solid-router-cleanup.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5795fa5648c3108108b1cbf9fff40d4579fc9616 --- /dev/null +++ b/packages/app/test-browser/solid-router-cleanup.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test" +import { MetaProvider, Title } from "@solidjs/meta" +import { MemoryRouter, Route, createMemoryHistory, useParams } from "@solidjs/router" +import { createMemo } from "solid-js" +import { createComponent, render } from "solid-js/web" + +test("route cleanup cannot invalidate an owner list being disposed", async () => { + const host = document.createElement("div") + document.body.append(host) + const history = createMemoryHistory() + + const RepoPage = () => { + const params = useParams<{ id?: string }>() + const title = createMemo(() => params.id ?? "") + const button = document.createElement("button") + button.textContent = "Back" + button.addEventListener("click", () => history.set({ value: "/", scroll: false, replace: false })) + return [ + createComponent(Title, { + get children() { + return title() + }, + }), + button, + ] + } + + const HomePage = () => { + const button = document.createElement("button") + button.textContent = "Go" + button.addEventListener("click", () => history.set({ value: "/project", scroll: false, replace: false })) + return button + } + + const App = () => + createComponent(MetaProvider, { + get children() { + return createComponent(MemoryRouter, { + history, + get children() { + return [ + createComponent(Route, { path: "/", component: HomePage }), + createComponent(Route, { path: "/:id", component: RepoPage }), + ] + }, + }) + }, + }) + + const dispose = render(() => createComponent(App, {}), host) + const go = host.querySelector("button") + expect(go?.textContent).toBe("Go") + go?.click() + await new Promise((resolve) => setTimeout(resolve, 0)) + + const back = host.querySelector("button") + expect(back?.textContent).toBe("Back") + back?.click() + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(host.querySelector("button")?.textContent).toBe("Go") + dispose() + host.remove() +}) diff --git a/packages/app/test-browser/solid-virtual.test.ts b/packages/app/test-browser/solid-virtual.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..9327eacf426db3b9a7e1df26f946b078dfb39037 --- /dev/null +++ b/packages/app/test-browser/solid-virtual.test.ts @@ -0,0 +1,112 @@ +import { expect, test } from "bun:test" +import { createVirtualizer, defaultRangeExtractor, Virtualizer } from "@tanstack/solid-virtual" +import { createRoot, createSignal } from "solid-js" +import { filterVirtualIndexes } from "@/pages/session/timeline/virtual-items" + +test("end anchoring survives consecutive resizes when the first scroll write is clamped", () => { + const writes: { offset: number; adjustments?: number }[] = [] + const virtualizer = new Virtualizer({ + count: 5, + estimateSize: () => 50, + initialOffset: 50, + initialRect: { width: 400, height: 200 }, + anchorTo: "end", + scrollEndThreshold: 1, + getScrollElement: () => null, + scrollToFn: (offset, options) => writes.push({ offset, adjustments: options.adjustments }), + observeElementRect: () => {}, + observeElementOffset: () => {}, + }) + + virtualizer.getTotalSize() + virtualizer.resizeItem(4, 120) + expect(writes).toEqual([{ offset: 50, adjustments: 70 }]) + writes.length = 0 + + virtualizer.resizeItem(4, 200) + expect(writes).toEqual([{ offset: 120, adjustments: 80 }]) +}) + +test("reactive count updates preserve measured row sizes", () => { + createRoot((dispose) => { + const [count, setCount] = createSignal(2) + const virtualizer = createVirtualizer({ + get count() { + return count() + }, + getScrollElement: () => null, + estimateSize: () => 60, + initialRect: { width: 800, height: 600 }, + }) + + expect(virtualizer.getTotalSize()).toBe(120) + virtualizer.resizeItem(0, 100) + expect(virtualizer.getTotalSize()).toBe(160) + + setCount(3) + + expect(virtualizer.itemSizeCache.get(0)).toBe(100) + expect(virtualizer.getTotalSize()).toBe(220) + dispose() + }) +}) + +test("initial rect projects rows before a scroll element connects", () => { + createRoot((dispose) => { + const virtualizer = createVirtualizer({ + count: 100, + getScrollElement: () => null, + estimateSize: () => 28, + initialRect: { width: 0, height: 600 }, + overscan: 10, + }) + + expect(virtualizer.getVirtualItems().length).toBeGreaterThan(0) + dispose() + }) +}) + +test("clamps oversized offsets with scroll margin and padding changes", () => { + const options = (paddingEnd: number) => ({ + count: 20, + estimateSize: () => 60, + initialOffset: Number.MAX_SAFE_INTEGER, + initialRect: { width: 800, height: 600 }, + scrollMargin: 64, + paddingEnd, + overscan: 1, + getScrollElement: () => null, + scrollToFn: () => {}, + observeElementRect: () => {}, + observeElementOffset: () => {}, + }) + const virtualizer = new Virtualizer(options(64)) + + expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) + + virtualizer.setOptions(options(600)) + expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([18, 19]) +}) + +test("stale pinned indexes do not produce missing virtual items after count shrinks", () => { + createRoot((dispose) => { + const [count, setCount] = createSignal(2) + const pinned = [1] + const virtualizer = createVirtualizer({ + get count() { + return count() + }, + getScrollElement: () => null, + estimateSize: () => 60, + initialRect: { width: 800, height: 600 }, + rangeExtractor: (range) => + filterVirtualIndexes([...new Set([...defaultRangeExtractor(range), ...pinned])], range.count), + }) + + expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([0, 1]) + setCount(1) + expect(virtualizer.getVirtualItems().map((item) => item.index)).toEqual([0]) + expect(() => new Map(virtualizer.getVirtualItems().map((item) => [item.key, item]))).not.toThrow() + dispose() + }) +}) diff --git a/packages/app/test-browser/toast-owner.test.ts b/packages/app/test-browser/toast-owner.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..25ba5c000e11c24cbd6f7a5f2e64bbb4312c0831 --- /dev/null +++ b/packages/app/test-browser/toast-owner.test.ts @@ -0,0 +1,64 @@ +import { beforeEach, describe, expect, test } from "bun:test" +import { createSignal, type JSX } from "solid-js" +import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" + +describe("showToastV2", () => { + // The toast registry is module state, so each test starts from an empty stack. + beforeEach(() => { + toasterV2.dismiss() + }) + + test("coalesces exact active content", () => { + const first = showToastV2({ title: "Repeated error", description: "Try again" }) + const second = showToastV2({ title: "Repeated error", description: "Try again" }) + const different = showToastV2({ title: "Repeated error", description: "A different error" }) + + expect(second).toBe(first) + expect(different).not.toBe(first) + + toasterV2.dismiss(first) + toasterV2.dismiss(different) + }) + + test("allows dismissed content to appear again", () => { + const first = showToastV2("Dismiss and retry") + toasterV2.dismiss(first) + + const second = showToastV2("Dismiss and retry") + expect(second).not.toBe(first) + + toasterV2.dismiss(second) + }) + + test("recreates matching content when it is not the topmost toast", () => { + const first = showToastV2("First toast") + const topmost = showToastV2("Topmost toast") + const repeated = showToastV2("First toast") + + expect(repeated).not.toBe(first) + + toasterV2.dismiss(topmost) + toasterV2.dismiss(repeated) + }) + + test("creates no reactive computations at call time", () => { + const [tick, setTick] = createSignal(0) + let reads = 0 + const icon = (() => { + reads++ + tick() + return undefined + }) as unknown as JSX.Element + + const id = showToastV2({ description: "test", icon }) + + // Resolving the icon at call time creates an ownerless computation that is + // never disposed and tracks its dependencies forever; it must only resolve + // once the toast component renders. + expect(reads).toBe(0) + setTick(1) + expect(reads).toBe(0) + + toasterV2.dismiss(id) + }) +}) diff --git a/packages/core/script/fix-node-pty.ts b/packages/core/script/fix-node-pty.ts new file mode 100644 index 0000000000000000000000000000000000000000..93641adbdf3c873a088ab145279f75acacf3e872 --- /dev/null +++ b/packages/core/script/fix-node-pty.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env bun + +import fs from "fs/promises" +import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const dir = path.resolve(__dirname, "..") + +if (process.platform !== "win32") { + const root = path.join(dir, "node_modules", "node-pty", "prebuilds") + const dirs = await fs.readdir(root, { withFileTypes: true }).catch(() => []) + const files = dirs.filter((x) => x.isDirectory()).map((x) => path.join(root, x.name, "spawn-helper")) + const result = await Promise.all( + files.map(async (file) => { + const stat = await fs.stat(file).catch(() => undefined) + if (!stat) return + if ((stat.mode & 0o111) === 0o111) return + await fs.chmod(file, stat.mode | 0o755) + return file + }), + ) + const fixed = result.filter(Boolean) + if (fixed.length) { + console.log(`fixed node-pty permissions for ${fixed.length} helper${fixed.length === 1 ? "" : "s"}`) + } +} diff --git a/packages/core/script/migration.ts b/packages/core/script/migration.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f383f5f8f6861a059f0048fba569eb6e1d3b3b2 --- /dev/null +++ b/packages/core/script/migration.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env bun + +import { $ } from "bun" +import fs from "fs/promises" +import os from "os" +import path from "path" +import { pathToFileURL } from "url" +import { parseArgs } from "util" + +const root = path.resolve(import.meta.dirname, "../../..") +const snapshot = path.join(root, "packages/core/schema.json") +const tsDir = path.join(root, "packages/core/src/database/migration") +const registry = path.join(root, "packages/core/src/database/migration.gen.ts") +const schema = path.join(root, "packages/core/src/database/schema.gen.ts") +const args = parseArgs({ + args: process.argv.slice(2), + options: { + check: { type: "boolean" }, + name: { type: "string" }, + }, +}) + +if (args.values.check) { + await check() + process.exit(0) +} + +await generate() + +async function generate() { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-")) + const incremental = path.join(temporary, "incremental") + const full = path.join(temporary, "full") + try { + await fs.mkdir(incremental) + await fs.mkdir(path.join(incremental, "baseline")) + await fs.copyFile(snapshot, path.join(incremental, "baseline/snapshot.json")) + await drizzle(temporary, incremental, args.values.name) + + const generated = await generatedMigrations(incremental) + if (generated.length > 1) throw new Error(`Expected one generated migration, found ${generated.length}.`) + const name = generated[0] + if (name) { + const target = path.join(tsDir, `${name}.ts`) + if (await Bun.file(target).exists()) throw new Error(`Database migration already exists: ${name}`) + await Bun.write( + target, + await formatTypescript( + renderMigration(name, await Bun.file(path.join(incremental, name, "migration.sql")).text()), + ), + ) + await fs.copyFile(path.join(incremental, name, "snapshot.json"), snapshot) + } + + await fs.mkdir(full) + await drizzle(temporary, full, "schema") + await Bun.write(schema, await formatTypescript(renderSchema(await generatedSql(full)))) + await Bun.write(registry, await formatTypescript(renderRegistry(await typescriptMigrations()))) + } finally { + await fs.rm(temporary, { recursive: true, force: true }) + } +} + +async function check() { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-")) + const incremental = path.join(temporary, "incremental") + const full = path.join(temporary, "full") + try { + await fs.mkdir(incremental) + await fs.mkdir(path.join(incremental, "baseline")) + await fs.copyFile(snapshot, path.join(incremental, "baseline/snapshot.json")) + await drizzle(temporary, incremental) + if ((await generatedMigrations(incremental)).length > 0) { + throw new Error( + "Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.", + ) + } + + await fs.mkdir(full) + await drizzle(temporary, full, "schema") + if ((await Bun.file(schema).text()) !== (await formatTypescript(renderSchema(await generatedSql(full))))) { + throw new Error("Current database schema is stale. Run `bun script/migration.ts` from packages/core.") + } + + const migrations = await typescriptMigrations() + if ((await Bun.file(registry).text()) !== (await formatTypescript(renderRegistry(migrations)))) { + throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.") + } + } finally { + await fs.rm(temporary, { recursive: true, force: true }) + } +} + +async function drizzle(temporary: string, output: string, name?: string) { + const config = path.join(temporary, `${path.basename(output)}.config.ts`) + await Bun.write( + config, + `import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)} + +export default { ...config, out: ${JSON.stringify(output)} } +`, + ) + await $`bun drizzle-kit generate --config ${config} ${name ? ["--name", name] : []}`.cwd( + path.join(root, "packages/core"), + ) +} + +async function generatedMigrations(directory: string) { + return (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: directory }))) + .map((file) => file.split("/")[0]) + .filter((name): name is string => name !== undefined) + .sort() +} + +async function generatedSql(directory: string) { + const generated = await generatedMigrations(directory) + if (generated.length !== 1) throw new Error(`Expected one full schema migration, found ${generated.length}.`) + return Bun.file(path.join(directory, generated[0]!, "migration.sql")).text() +} + +async function typescriptMigrations() { + return (await Array.fromAsync(new Bun.Glob("*.ts").scan({ cwd: tsDir }))) + .map((file) => path.basename(file, ".ts")) + .sort() +} + +function renderMigration(name: string, sql: string) { + return `import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: ${JSON.stringify(name)}, + up(tx) { + return Effect.gen(function* () { +${renderStatements(sql)} + }) + }, +} satisfies DatabaseMigration.Migration +` +} + +function renderSchema(sql: string) { + return `import { Effect } from "effect" +import type { DatabaseMigration } from "./migration" + +export default { + up(tx) { + return Effect.gen(function* () { +${renderStatements(sql)} + }) + }, +} satisfies Omit +` +} + +function renderStatements(sql: string) { + return sql + .split("--> statement-breakpoint") + .map((statement) => statement.trim()) + .filter((statement) => statement.length > 0) + .map(renderRun) + .join("\n") +} + +function renderRun(statement: string) { + const lines = statement.replaceAll("\t", " ").split("\n") + if (lines.length === 1) return ` yield* tx.run(\`${escapeTemplate(lines[0])}\`)` + return ` yield* tx.run(\`\n${lines.map((line) => ` ${escapeTemplate(line)}`).join("\n")}\n \`)` +} + +function escapeTemplate(line: string) { + return line.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${") +} + +async function formatTypescript(input: string) { + const prettier = await import("prettier") + const typescript = await import("prettier/plugins/typescript") + const estree = await import("prettier/plugins/estree") + return prettier.format(input, { + parser: "typescript", + plugins: [typescript.default, estree.default], + semi: false, + printWidth: 120, + }) +} + +function renderRegistry(names: string[]) { + return `import type { DatabaseMigration } from "./migration" + +export const migrations = ( + await Promise.all([ +${names.map((name) => ` import("./migration/${name}"),`).join("\n")} + ]) +).map((module) => module.default) satisfies DatabaseMigration.Migration[] +` +} diff --git a/packages/core/src/account.ts b/packages/core/src/account.ts new file mode 100644 index 0000000000000000000000000000000000000000..d364d6f344837735a7195b270dffcd03f80c8b83 --- /dev/null +++ b/packages/core/src/account.ts @@ -0,0 +1,101 @@ +export * as AccountV2 from "./account" + +import { Schema } from "effect" +import type * as HttpClientError from "effect/unstable/http/HttpClientError" + +export const ID = Schema.String.pipe(Schema.brand("AccountID")) +export type ID = Schema.Schema.Type + +export const OrgID = Schema.String.pipe(Schema.brand("OrgID")) +export type OrgID = Schema.Schema.Type + +export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken")) +export type AccessToken = Schema.Schema.Type + +export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken")) +export type RefreshToken = Schema.Schema.Type + +export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode")) +export type DeviceCode = Schema.Schema.Type + +export const UserCode = Schema.String.pipe(Schema.brand("UserCode")) +export type UserCode = Schema.Schema.Type + +export class Info extends Schema.Class("Account")({ + id: ID, + email: Schema.String, + url: Schema.String, + active_org_id: Schema.NullOr(OrgID), +}) {} + +export class Org extends Schema.Class("Org")({ + id: OrgID, + name: Schema.String, +}) {} + +export class AccountRepoError extends Schema.TaggedErrorClass()("AccountRepoError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} + +export class AccountServiceError extends Schema.TaggedErrorClass()("AccountServiceError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} + +export class AccountTransportError extends Schema.TaggedErrorClass()("AccountTransportError", { + method: Schema.String, + url: Schema.String, + description: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect()), +}) { + static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError { + return new AccountTransportError({ + method: error.request.method, + url: error.request.url, + description: error.description, + cause: error.cause, + }) + } + + override get message(): string { + return [ + `Could not reach ${this.method} ${this.url}.`, + `This failed before the server returned an HTTP response.`, + this.description, + `Check your network, proxy, or VPN configuration and try again.`, + ] + .filter(Boolean) + .join("\n") + } +} + +export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError + +export class Login extends Schema.Class("Login")({ + code: DeviceCode, + user: UserCode, + url: Schema.String, + server: Schema.String, + expiry: Schema.Duration, + interval: Schema.Duration, +}) {} + +export class PollSuccess extends Schema.TaggedClass()("PollSuccess", { + email: Schema.String, +}) {} + +export class PollPending extends Schema.TaggedClass()("PollPending", {}) {} + +export class PollSlow extends Schema.TaggedClass()("PollSlow", {}) {} + +export class PollExpired extends Schema.TaggedClass()("PollExpired", {}) {} + +export class PollDenied extends Schema.TaggedClass()("PollDenied", {}) {} + +export class PollError extends Schema.TaggedClass()("PollError", { + cause: Schema.Defect(), +}) {} + +export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError]) +export type PollResult = Schema.Schema.Type diff --git a/packages/core/src/account/sql.ts b/packages/core/src/account/sql.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f45651d78eca01a83b10b237609e5f9b98e27a9 --- /dev/null +++ b/packages/core/src/account/sql.ts @@ -0,0 +1,39 @@ +import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" + +import { AccountV2 } from "../account" +import { Timestamps } from "../database/schema.sql" + +export const AccountTable = sqliteTable("account", { + id: text().$type().primaryKey(), + email: text().notNull(), + url: text().notNull(), + access_token: text().$type().notNull(), + refresh_token: text().$type().notNull(), + token_expiry: integer(), + ...Timestamps, +}) + +export const AccountStateTable = sqliteTable("account_state", { + id: integer().primaryKey(), + active_account_id: text() + .$type() + .references(() => AccountTable.id, { onDelete: "set null" }), + active_org_id: text().$type(), +}) + +// LEGACY +export const ControlAccountTable = sqliteTable( + "control_account", + { + email: text().notNull(), + url: text().notNull(), + access_token: text().$type().notNull(), + refresh_token: text().$type().notNull(), + token_expiry: integer(), + active: integer({ mode: "boolean" }) + .notNull() + .$default(() => false), + ...Timestamps, + }, + (table) => [primaryKey({ columns: [table.email, table.url] })], +) diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts new file mode 100644 index 0000000000000000000000000000000000000000..da86cf809b90a8117cfa7bb315362d3e61230fa2 --- /dev/null +++ b/packages/core/src/agent.ts @@ -0,0 +1,111 @@ +export * as AgentV2 from "./agent" + +import { makeLocationNode } from "./effect/app-node" +import { Array, Context, Effect, Layer, Types } from "effect" +import { Agent } from "@opencode-ai/schema/agent" +import { State } from "./state" + +export const ID = Agent.ID +export type ID = typeof ID.Type +export const defaultID = ID.make("build") + +export const Color = Agent.Color + +export const Info = Agent.Info +export type Info = Agent.Info + +export interface Selection { + readonly id: ID + readonly info: Info | undefined +} + +type Data = { + agents: Map> + default?: ID +} + +export type Draft = { + list: () => readonly Info[] + get: (id: ID) => Info | undefined + default: (id: ID | undefined) => void + update: (id: ID, fn: (agent: Types.DeepMutable) => void) => void + remove: (id: ID) => void +} + +export interface Interface extends State.Transformable { + readonly get: (id: ID) => Effect.Effect + readonly default: () => Effect.Effect + readonly resolve: (id?: ID | string) => Effect.Effect + readonly select: (id?: ID | string) => Effect.Effect + readonly all: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Agent") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const state = State.create({ + initial: () => ({ agents: new Map() }), + draft: (draft) => ({ + list: () => Array.fromIterable(draft.agents.values()) as Info[], + get: (id) => draft.agents.get(id), + default: (id) => { + draft.default = id + }, + update: (id, fn) => { + const current = draft.agents.get(id) ?? (Info.empty(id) as Types.DeepMutable) + if (!draft.agents.has(id)) draft.agents.set(id, current) + fn(current) + current.id = id + }, + remove: (id) => { + draft.agents.delete(id) + }, + }), + }) + const selectable = (agent: Info | undefined) => + agent && agent.mode !== "subagent" && !agent.hidden ? agent : undefined + const selectedDefault = () => { + const data = state.get() + const configured = data.default ? selectable(data.agents.get(data.default)) : undefined + if (configured) return configured + const build = selectable(data.agents.get(ID.make("build"))) + if (build) return build + for (const agent of data.agents.values()) { + const fallback = selectable(agent) + if (fallback) return fallback + } + } + + return Service.of({ + transform: state.transform, + reload: state.reload, + get: Effect.fn("AgentV2.get")(function* (id) { + return state.get().agents.get(id) + }), + default: Effect.fn("AgentV2.default")(function* () { + return selectedDefault() + }), + resolve: Effect.fn("AgentV2.resolve")(function* (id) { + if (id !== undefined) return state.get().agents.get(ID.make(id)) + return selectedDefault() + }), + select: Effect.fn("AgentV2.select")(function* (id) { + if (id !== undefined) { + const selected = ID.make(id) + return { id: selected, info: state.get().agents.get(selected) } + } + const info = selectedDefault() + return { id: info?.id ?? defaultID, info } + }), + all: Effect.fn("AgentV2.all")(function* () { + return Array.fromIterable(state.get().agents.values()) + }), + }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1a973b821973b9c1e0fe9c2c9305eb8b527c623 --- /dev/null +++ b/packages/core/src/aisdk.ts @@ -0,0 +1,235 @@ +export * as AISDK from "./aisdk" + +import { makeLocationNode } from "./effect/app-node" +import type { LanguageModelV3 } from "@ai-sdk/provider" +import { Cause, Context, Effect, Layer, Schema, Scope } from "effect" +import { ModelV2 } from "./model" +import { ProviderV2 } from "./provider" +import { State } from "./state" + +type SDK = any + +export interface SDKEvent { + readonly model: ModelV2.Info + readonly package: string + readonly options: Record + sdk?: SDK +} + +export interface LanguageEvent { + readonly model: ModelV2.Info + readonly sdk: SDK + readonly options: Record + language?: LanguageModelV3 +} + +function wrapSSE(res: Response, ms: number, ctl: AbortController) { + if (typeof ms !== "number" || ms <= 0) return res + if (!res.body) return res + if (!res.headers.get("content-type")?.includes("text/event-stream")) return res + + const reader = res.body.getReader() + const body = new ReadableStream({ + async pull(ctrl) { + const part = await new Promise>>((resolve, reject) => { + const id = setTimeout(() => { + const err = new Error("SSE read timed out") + ctl.abort(err) + reader.cancel(err).catch(() => {}) + reject(err) + }, ms) + + reader.read().then( + (part) => { + clearTimeout(id) + resolve(part) + }, + (err) => { + clearTimeout(id) + reject(err) + }, + ) + }) + + if (part.done) { + ctrl.close() + return + } + + ctrl.enqueue(part.value) + }, + async cancel(reason) { + ctl.abort(reason) + await reader.cancel(reason) + }, + }) + + return new Response(body, { + headers: new Headers(res.headers), + status: res.status, + statusText: res.statusText, + }) +} + +function prepareOptions(model: ModelV2.Info, pkg: string) { + const options: Record = { + name: model.providerID, + ...(model.api.type === "aisdk" ? (model.api.settings ?? {}) : {}), + ...model.request.body, + } + if (model.api.type === "aisdk" && model.api.url) options.baseURL = model.api.url + + const customFetch = options.fetch + const chunkTimeout = options.chunkTimeout + delete options.chunkTimeout + options.fetch = async (input: Parameters[0], init?: RequestInit) => { + const opts = { ...(init ?? {}) } + const signals = [ + opts.signal, + typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined, + options.timeout !== undefined && options.timeout !== null && options.timeout !== false + ? AbortSignal.timeout(options.timeout) + : undefined, + ].filter((item): item is AbortSignal | AbortController => Boolean(item)) + const chunkAbortCtl = signals.find((item): item is AbortController => item instanceof AbortController) + const abortSignals = signals.map((item) => (item instanceof AbortController ? item.signal : item)) + if (abortSignals.length === 1) opts.signal = abortSignals[0] + if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals) + + if ( + (pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure" || pkg === "@ai-sdk/amazon-bedrock/mantle") && + opts.body && + opts.method === "POST" + ) { + const body = JSON.parse(opts.body as string) + if (body.store !== true && Array.isArray(body.input)) { + for (const item of body.input) { + if ("id" in item) delete item.id + } + opts.body = JSON.stringify(body) + } + } + + const res = await (typeof customFetch === "function" ? customFetch : fetch)(input, { + ...opts, + timeout: false, + }) + if (!chunkAbortCtl || typeof chunkTimeout !== "number") return res + return wrapSSE(res, chunkTimeout, chunkAbortCtl) + } + + return options +} + +export class InitError extends Schema.TaggedErrorClass()("AISDK.InitError", { + providerID: ProviderV2.ID, + cause: Schema.Defect(), +}) {} + +function initError(providerID: ProviderV2.ID) { + return Effect.catchCause((cause) => Effect.fail(new InitError({ providerID, cause: Cause.squash(cause) }))) +} + +export interface Interface { + readonly hook: { + readonly sdk: ( + callback: (event: SDKEvent) => Effect.Effect | void, + ) => Effect.Effect + readonly language: ( + callback: (event: LanguageEvent) => Effect.Effect | void, + ) => Effect.Effect + } + readonly runSDK: (event: SDKEvent) => Effect.Effect + readonly runLanguage: (event: LanguageEvent) => Effect.Effect + readonly language: (model: ModelV2.Info) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/AISDK") {} + +export const locationLayer = Layer.effect( + Service, + Effect.gen(function* () { + let sdkHooks: ((event: SDKEvent) => Effect.Effect | void)[] = [] + let languageHooks: ((event: LanguageEvent) => Effect.Effect | void)[] = [] + const languages = new Map() + const sdks = new Map() + + const register = ( + hooks: () => ((event: Event) => Effect.Effect | void)[], + update: (hooks: ((event: Event) => Effect.Effect | void)[]) => void, + ) => + Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect | void) { + const scope = yield* Scope.Scope + let active = true + update([...hooks(), callback]) + const dispose = Effect.sync(() => { + if (!active) return + active = false + update(hooks().filter((item) => item !== callback)) + }) + yield* Scope.addFinalizer(scope, dispose) + return { dispose } + }) + + const run = Effect.fnUntraced(function* ( + hooks: readonly ((event: Event) => Effect.Effect | void)[], + event: Event, + ) { + for (const hook of hooks) { + const result = hook(event) + if (Effect.isEffect(result)) yield* result + } + return event + }) + + const service = Service.of({ + hook: { + sdk: register( + () => sdkHooks, + (next) => (sdkHooks = next), + ), + language: register( + () => languageHooks, + (next) => (languageHooks = next), + ), + }, + runSDK: (event) => run(sdkHooks, event), + runLanguage: (event) => run(languageHooks, event), + language: Effect.fn("AISDK.language")(function* (model) { + const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}` + const existing = languages.get(key) + if (existing) return existing + if (model.api.type !== "aisdk") + return yield* new InitError({ + providerID: model.providerID, + cause: new Error(`Unsupported api ${model.api.type}`), + }) + + const options = prepareOptions(model, model.api.package) + const sdkKey = JSON.stringify({ + providerID: model.providerID, + api: model.api, + options, + }) + const sdk = + sdks.get(sdkKey) ?? + (yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk + if (!sdk) + return yield* new InitError({ + providerID: model.providerID, + cause: new Error("No AISDK provider plugin returned an SDK"), + }) + sdks.set(sdkKey, sdk) + const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID)) + const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe( + initError(model.providerID), + ) + languages.set(key, language) + return language + }), + }) + return service + }), +) + +export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [] }) diff --git a/packages/core/src/background-job.ts b/packages/core/src/background-job.ts new file mode 100644 index 0000000000000000000000000000000000000000..cdffd212bc4e9b4fe922ccbe4f7c4b9fd6cafe90 --- /dev/null +++ b/packages/core/src/background-job.ts @@ -0,0 +1,365 @@ +export * as BackgroundJob from "./background-job" + +import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Scope, SynchronizedRef } from "effect" +import { Identifier } from "./id/id" +import { makeGlobalNode } from "./effect/app-node" + +export type Status = "running" | "completed" | "error" | "cancelled" + +export type Info = { + id: string + type: string + title?: string + status: Status + started_at: number + completed_at?: number + output?: string + error?: string + metadata?: Record +} + +type Active = { + info: Info + done: Deferred.Deferred + scope: Scope.Closeable + token: object + pending: number + next: number + output?: { sequence: number; text: string } + tail: Deferred.Deferred + promoted: Deferred.Deferred + onPromote?: Effect.Effect +} + +type State = { + jobs: SynchronizedRef.SynchronizedRef> + scope: Scope.Scope +} + +type FinishResult = { + info?: Info + done?: Deferred.Deferred + scope?: Scope.Closeable +} + +type PromoteResult = { + info?: Info + promoted?: Deferred.Deferred + onPromote?: Effect.Effect +} + +type StartResult = { info: Info } | { info: Info; scope: Scope.Closeable; token: object } + +type ExtendResult = + | { extended: false } + | { + extended: true + previous: Deferred.Deferred + scope: Scope.Closeable + tail: Deferred.Deferred + token: object + sequence: number + } + +export type StartInput = { + id?: string + type: string + title?: string + metadata?: Record + onPromote?: Effect.Effect + run: Effect.Effect +} + +export type ExtendInput = { + id: string + run: Effect.Effect +} + +export type WaitInput = { + id: string + timeout?: number +} + +export type WaitResult = { + info?: Info + timedOut: boolean +} + +export interface Interface { + readonly list: () => Effect.Effect + readonly get: (id: string) => Effect.Effect + readonly start: (input: StartInput) => Effect.Effect + readonly extend: (input: ExtendInput) => Effect.Effect + readonly wait: (input: WaitInput) => Effect.Effect + readonly waitForPromotion: (id: string) => Effect.Effect + readonly promote: (id: string) => Effect.Effect + readonly cancel: (id: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/BackgroundJob") {} + +function snapshot(job: Active): Info { + return { + ...job.info, + ...(job.info.metadata ? { metadata: { ...job.info.metadata } } : {}), + } +} + +function errorText(error: unknown) { + if (error instanceof Error) return error.message + return String(error) +} + +/** + * Makes one scoped, process-local registry. Entries are intentionally not + * durable: process restart or owner-scope closure loses status and interrupts + * live work. Persisted observation, restart recovery, and remote workers need a + * separate durable ownership slice rather than pretending this registry has + * those semantics. + */ +export const make = Effect.gen(function* () { + const state: State = { + jobs: yield* SynchronizedRef.make(new Map()), + scope: yield* Scope.Scope, + } + + const settle = Effect.fn("BackgroundJob.settle")(function* ( + id: string, + token: object, + sequence: number, + exit: Exit.Exit, + ) { + const completed_at = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(state.jobs, (jobs): readonly [FinishResult, Map] => { + const job = jobs.get(id) + if (!job) return [{}, jobs] + if (job.token !== token) return [{}, jobs] + if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs] + const pending = job.pending - 1 + const output = + Exit.isSuccess(exit) && (!job.output || sequence > job.output.sequence) + ? { sequence, text: exit.value } + : job.output + if (Exit.isSuccess(exit) && pending > 0) { + return [{}, new Map(jobs).set(id, { ...job, pending, output })] + } + const status: Exclude = Exit.isSuccess(exit) + ? "completed" + : Cause.hasInterruptsOnly(exit.cause) + ? "cancelled" + : "error" + const next = { + ...job, + onPromote: undefined, + pending: 0, + output, + info: { + ...job.info, + status, + completed_at, + ...(output ? { output: output.text } : {}), + ...(Exit.isFailure(exit) ? { error: errorText(Cause.squash(exit.cause)) } : {}), + }, + } + return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)] + }) + if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore) + if (result.scope) { + yield* Scope.close(result.scope, Exit.void).pipe(Effect.forkIn(state.scope, { startImmediately: true })) + } + return result.info + }) + + const fork = Effect.fn("BackgroundJob.fork")(function* ( + scope: Scope.Scope, + id: string, + token: object, + sequence: number, + run: Effect.Effect, + ) { + return yield* run.pipe( + Effect.matchCauseEffect({ + onSuccess: (output) => settle(id, token, sequence, Exit.succeed(output)), + onFailure: (cause) => settle(id, token, sequence, Exit.failCause(cause)), + }), + Effect.asVoid, + Effect.forkIn(scope, { startImmediately: true }), + ) + }) + + const list: Interface["list"] = Effect.fn("BackgroundJob.list")(function* () { + return Array.from((yield* SynchronizedRef.get(state.jobs)).values()) + .map(snapshot) + .toSorted((a, b) => a.started_at - b.started_at) + }) + + const get: Interface["get"] = Effect.fn("BackgroundJob.get")(function* (id) { + const job = (yield* SynchronizedRef.get(state.jobs)).get(id) + if (!job) return + return snapshot(job) + }) + + const start: Interface["start"] = Effect.fn("BackgroundJob.start")(function* (input) { + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const id = input.id ?? Identifier.ascending("job") + const started_at = yield* Clock.currentTimeMillis + const done = yield* Deferred.make() + const promoted = yield* Deferred.make() + const tail = yield* Deferred.make() + const result = yield* SynchronizedRef.modifyEffect( + state.jobs, + Effect.fnUntraced(function* (jobs) { + const existing = jobs.get(id) + if (existing?.info.status === "running") { + return [{ info: snapshot(existing) }, jobs] as readonly [StartResult, Map] + } + const scope = yield* Scope.fork(state.scope, "parallel") + const token = {} + const job = { + info: { + id, + type: input.type, + title: input.title, + status: "running" as const, + started_at, + metadata: input.metadata, + }, + done, + scope, + token, + pending: 1, + next: 1, + tail, + promoted, + onPromote: input.onPromote, + } + return [{ info: snapshot(job), scope, token }, new Map(jobs).set(id, job)] as readonly [ + StartResult, + Map, + ] + }), + ) + if ("scope" in result) + yield* fork( + result.scope, + id, + result.token, + 0, + restore(input.run).pipe(Effect.ensuring(Deferred.succeed(tail, undefined))), + ) + return result.info + }), + ) + }) + + const extend: Interface["extend"] = Effect.fn("BackgroundJob.extend")(function* (input) { + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const tail = yield* Deferred.make() + const result = yield* SynchronizedRef.modify( + state.jobs, + (jobs): readonly [ExtendResult, Map] => { + const job = jobs.get(input.id) + if (!job || job.info.status !== "running") return [{ extended: false }, jobs] + return [ + { extended: true, previous: job.tail, scope: job.scope, tail, token: job.token, sequence: job.next }, + new Map(jobs).set(input.id, { + ...job, + pending: job.pending + 1, + next: job.next + 1, + tail, + }), + ] + }, + ) + if (!result.extended) return false + yield* fork( + result.scope, + input.id, + result.token, + result.sequence, + Deferred.await(result.previous).pipe( + Effect.andThen(restore(input.run)), + Effect.ensuring(Deferred.succeed(result.tail, undefined)), + ), + ) + return true + }), + ) + }) + + const wait: Interface["wait"] = Effect.fn("BackgroundJob.wait")(function* (input) { + const job = (yield* SynchronizedRef.get(state.jobs)).get(input.id) + if (!job) return { timedOut: false } + if (job.info.status !== "running") return { info: snapshot(job), timedOut: false } + if (input.timeout === undefined) return { info: yield* Deferred.await(job.done), timedOut: false } + if (input.timeout <= 0) return { info: snapshot(job), timedOut: true } + const info = yield* Deferred.await(job.done).pipe(Effect.timeoutOption(input.timeout)) + if (info._tag === "Some") return { info: info.value, timedOut: false } + return { info: snapshot(job), timedOut: true } + }) + + const waitForPromotion: Interface["waitForPromotion"] = Effect.fn("BackgroundJob.waitForPromotion")(function* (id) { + const job = (yield* SynchronizedRef.get(state.jobs)).get(id) + if (!job || job.info.status !== "running") return yield* Effect.never + if (job.info.metadata?.background === true) return snapshot(job) + return yield* Deferred.await(job.promoted) + }) + + const promote: Interface["promote"] = Effect.fn("BackgroundJob.promote")(function* (id) { + const result = yield* SynchronizedRef.modifyEffect( + state.jobs, + Effect.fnUntraced(function* (jobs) { + const job = jobs.get(id) + if (!job || job.info.status !== "running") return [{}, jobs] as readonly [PromoteResult, Map] + if (job.info.metadata?.background === true) + return [{ info: snapshot(job) }, jobs] as readonly [PromoteResult, Map] + const next = { + ...job, + onPromote: undefined, + info: { + ...job.info, + metadata: { ...job.info.metadata, background: true }, + }, + } + return [ + { info: snapshot(next), onPromote: job.onPromote, promoted: job.promoted }, + new Map(jobs).set(id, next), + ] as readonly [PromoteResult, Map] + }), + ) + if (result.info && result.promoted) yield* Deferred.succeed(result.promoted, result.info).pipe(Effect.ignore) + if (result.onPromote) yield* result.onPromote.pipe(Effect.ignore) + return result.info + }) + + const cancel: Interface["cancel"] = Effect.fn("BackgroundJob.cancel")(function* (id) { + const completed_at = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(state.jobs, (jobs): readonly [FinishResult, Map] => { + const job = jobs.get(id) + if (!job) return [{}, jobs] + if (job.info.status !== "running") return [{ info: snapshot(job) }, jobs] + const next = { + ...job, + onPromote: undefined, + pending: 0, + info: { + ...job.info, + status: "cancelled" as const, + completed_at, + }, + } + return [{ info: snapshot(next), done: job.done, scope: job.scope }, new Map(jobs).set(id, next)] + }) + if (result.info && result.done) yield* Deferred.succeed(result.done, result.info).pipe(Effect.ignore) + if (result.scope) yield* Scope.close(result.scope, Exit.void) + return result.info + }) + + return Service.of({ list, get, start, extend, wait, waitForPromotion, promote, cancel }) +}) + +const layer = Layer.effect(Service, make) + +export const node = makeGlobalNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts new file mode 100644 index 0000000000000000000000000000000000000000..1945024b54e8c755a4f110d4bf4444ae10a10844 --- /dev/null +++ b/packages/core/src/catalog.ts @@ -0,0 +1,301 @@ +export * as Catalog from "./catalog" + +import { makeLocationNode } from "./effect/app-node" +import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect" +import { Catalog } from "@opencode-ai/schema/catalog" +import { ModelV2 } from "./model" +import { ProviderV2 } from "./provider" +import { EventV2 } from "./event" +import { Policy } from "./policy" +import { State } from "./state" +import { Integration } from "./integration" + +export type ProviderRecord = { + provider: ProviderV2.MutableInfo + models: Map +} + +export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID } + +export const PolicyActions = Schema.Literals(["provider.use"]) + +export const Event = Catalog.Event + +type Data = { + providers: Map + defaultModel?: DefaultModel +} + +export type Draft = { + provider: { + list: () => readonly ProviderRecord[] + get: (providerID: ProviderV2.ID) => ProviderRecord | undefined + update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void + remove: (providerID: ProviderV2.ID) => void + } + model: { + get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined + update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void + remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void + default: { + get: () => DefaultModel | undefined + set: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void + } + } +} + +export interface Interface extends State.Transformable { + readonly provider: { + readonly get: (providerID: ProviderV2.ID) => Effect.Effect + readonly all: () => Effect.Effect + readonly available: () => Effect.Effect + } + readonly model: { + readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect + readonly all: () => Effect.Effect + readonly available: () => Effect.Effect + readonly default: () => Effect.Effect + readonly small: (providerID: ProviderV2.ID) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/v2/Catalog") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const policy = yield* Policy.Service + const integrations = yield* Integration.Service + + const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => { + if (provider.disabled) return false + if (typeof provider.request.body.apiKey === "string") return true + if (integration?.connections.length) return true + return provider.integrationID === undefined && !integration + } + + const projectModel = (model: ModelV2.Info, provider: ProviderV2.Info) => { + const api = + model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0 + ? { ...provider.api, id: model.api.id } + : model.api.type === "aisdk" && provider.api.type === "aisdk" && !model.api.url + ? { ...model.api, url: provider.api.url, settings: { ...provider.api.settings, ...model.api.settings } } + : model.api.type === "aisdk" && provider.api.type === "aisdk" + ? { ...model.api, settings: { ...provider.api.settings, ...model.api.settings } } + : model.api + const request = { + headers: { ...provider.request.headers, ...model.request.headers }, + body: { ...provider.request.body, ...model.request.body }, + variant: model.request.variant, + } + return ModelV2.Info.make({ + ...model, + api, + request, + }) + } + + const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => { + if (typeof item.request.body.baseURL !== "string") return + item.api.url = item.request.body.baseURL + delete item.request.body.baseURL + } + + const state = State.create({ + initial: () => ({ providers: new Map() }), + draft: (draft) => { + const result: Draft = { + provider: { + list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[], + get: (providerID) => draft.providers.get(providerID), + update: (providerID, fn) => { + let current = draft.providers.get(providerID) + if (!current) { + current = { + provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, + models: new Map(), + } + draft.providers.set(providerID, current) + } + fn(current.provider) + normalizeApi(current.provider) + }, + remove: (providerID) => { + draft.providers.delete(providerID) + }, + }, + model: { + get: (providerID, modelID) => draft.providers.get(providerID)?.models.get(modelID), + update: (providerID, modelID, fn) => { + let record = draft.providers.get(providerID) + if (!record) { + record = { + provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo, + models: new Map(), + } + draft.providers.set(providerID, record) + } + const model = + record.models.get(modelID) ?? (ModelV2.Info.empty(providerID, modelID) as ModelV2.MutableInfo) + if (!record.models.has(modelID)) record.models.set(modelID, model) + fn(model) + model.id = modelID + model.providerID = providerID + normalizeApi(model) + }, + remove: (providerID, modelID) => { + draft.providers.get(providerID)?.models.delete(modelID) + }, + default: { + get: () => draft.defaultModel, + set: (providerID, modelID) => { + draft.defaultModel = { providerID, modelID } + }, + }, + }, + } + return result + }, + finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) { + if (policy.hasStatements()) { + for (const record of [...catalog.provider.list()]) { + if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") { + catalog.provider.remove(record.provider.id) + } + } + } + yield* events.publish(Event.Updated, {}) + }), + }) + const result: Interface = { + transform: state.transform, + reload: state.reload, + + provider: { + get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { + return state.get().providers.get(providerID)?.provider + }), + + all: Effect.fn("CatalogV2.provider.all")(function* () { + return Array.fromIterable(state.get().providers.values()).map((record) => record.provider) + }), + + available: Effect.fn("CatalogV2.provider.available")(function* () { + const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration])) + return (yield* result.provider.all()).filter((provider) => + available(provider, active.get(provider.integrationID ?? Integration.ID.make(provider.id))), + ) + }), + }, + + model: { + get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) { + const record = state.get().providers.get(providerID) + if (!record) return + const model = record.models.get(modelID) + return model && projectModel(model, record.provider) + }), + + all: Effect.fn("CatalogV2.model.all")(function* () { + return pipe( + Array.fromIterable(state.get().providers.values()), + Array.flatMap((record) => { + return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider)) + }), + Array.sortWith((item) => item.time.released, Order.flip(Order.Number)), + ) + }), + + available: Effect.fn("CatalogV2.model.available")(function* () { + const providers = new Set((yield* result.provider.available()).map((provider) => provider.id)) + return (yield* result.model.all()).filter((model) => providers.has(model.providerID) && model.enabled) + }), + + default: Effect.fn("CatalogV2.model.default")(function* () { + const defaultModel = state.get().defaultModel + if (defaultModel) { + const provider = yield* result.provider.get(defaultModel.providerID) + if (provider && (yield* result.provider.available()).some((item) => item.id === provider.id)) { + const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID) + if (model?.enabled) return model + } + } + + return Option.getOrUndefined( + pipe( + yield* result.model.available(), + Array.sortWith((item) => item.time.released, Order.flip(Order.Number)), + Array.head, + ), + ) + }), + + small: Effect.fn("CatalogV2.model.small")(function* (providerID) { + const record = state.get().providers.get(providerID) + if (!record) return + const provider = record.provider + + // TODO: Remove these provider-specific assumptions once model syncing reliably reports available deployments. + if (providerID === ProviderV2.ID.azure || providerID === ProviderV2.ID.make("azure-cognitive-services")) { + return + } + + if (providerID === ProviderV2.ID.opencode) { + const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) + if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider) + } + + const candidates = pipe( + Array.fromIterable(record.models.values()), + Array.filter( + (model) => + model.providerID === providerID && + model.enabled && + model.status === "active" && + model.capabilities.input.some((item) => item.startsWith("text")) && + model.capabilities.output.some((item) => item.startsWith("text")), + ), + Array.map((model) => ({ + model, + cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999, + age: (Date.now() - model.time.released) / (1000 * 60 * 60 * 24 * 30), + small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()), + })), + Array.filter((item) => item.cost > 0 && item.age <= 18), + ) + + const pick = (items: typeof candidates) => { + const maxCost = Math.max(...items.map((item) => item.cost), 0.01) + const maxAge = Math.max(...items.map((item) => item.age), 0.01) + return pipe( + items, + Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number), + Array.map((item) => projectModel(item.model, provider)), + Array.head, + ) + } + + return Option.getOrUndefined( + pipe( + candidates, + Array.filter((item) => item.small), + (items) => (items.length > 0 ? pick(items) : pick(candidates)), + ), + ) + }), + }, + } + + return Service.of(result) + }), +) + +const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/ + +export const locationLayer = layer.pipe( + Layer.provideMerge(Integration.locationLayer), + Layer.provideMerge(Policy.locationLayer), +) + +export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Policy.node, Integration.node] }) diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb082a9f47ebb81623c13e9ced26400bcfc2c605 --- /dev/null +++ b/packages/core/src/command.ts @@ -0,0 +1,64 @@ +export * as CommandV2 from "./command" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect, Layer, Types } from "effect" +import { Command } from "@opencode-ai/schema/command" +import { State } from "./state" + +export const Info = Command.Info +export type Info = Command.Info + +export type Data = { + commands: Map> +} + +export type Draft = { + list: () => readonly Info[] + get: (name: string) => Info | undefined + update: (name: string, update: (command: Types.DeepMutable) => void) => void + remove: (name: string) => void +} + +export interface Interface extends State.Transformable { + readonly get: (name: string) => Effect.Effect + readonly list: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Command") {} + +const layer = Layer.effect( + Service, + Effect.sync(() => { + const state = State.create({ + initial: () => ({ commands: new Map() }), + draft: (draft) => ({ + list: () => Array.from(draft.commands.values()) as Info[], + get: (name) => draft.commands.get(name), + update: (name, update) => { + const current = draft.commands.get(name) ?? ({ name, template: "" } as Types.DeepMutable) + if (!draft.commands.has(name)) draft.commands.set(name, current) + update(current) + current.name = name + }, + remove: (name) => { + draft.commands.delete(name) + }, + }), + }) + + return Service.of({ + reload: state.reload, + transform: state.transform, + get: Effect.fn("CommandV2.get")(function* (name) { + return state.get().commands.get(name) + }), + list: Effect.fn("CommandV2.list")(function* () { + return Array.from(state.get().commands.values()) + }), + }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [] }) diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..c76486968b0784d57dab102e2d51a7e18f57b7f5 --- /dev/null +++ b/packages/core/src/config.ts @@ -0,0 +1,227 @@ +export * as Config from "./config" + +import { makeLocationNode } from "./effect/app-node" +import path from "path" +import { type ParseError, parse } from "jsonc-parser" +import { Context, Effect, Layer, Option, Schema } from "effect" +import { Permission } from "@opencode-ai/schema/permission" +import { FSUtil } from "./fs-util" +import { Global } from "./global" +import { Location } from "./location" +import { Policy } from "./policy" +import { AbsolutePath } from "./schema" +import { ConfigAgent } from "./config/agent" +import { ConfigAttachments } from "./config/attachments" +import { ConfigCompaction } from "./config/compaction" +import { ConfigCommand } from "./config/command" +import { ConfigExperimental } from "./config/experimental" +import { ConfigFormatter } from "./config/formatter" +import { ConfigLSP } from "./config/lsp" +import { ConfigMCP } from "./config/mcp" +import { ConfigPlugin } from "./config/plugin" +import { ConfigProvider } from "./config/provider" +import { ConfigReference } from "./config/reference" +import { ConfigToolOutput } from "./config/tool-output" +import { ConfigWatcher } from "./config/watcher" +import { ConfigV1 } from "./v1/config/config" +import { ConfigMigrateV1 } from "./v1/config/migrate" + +export class Info extends Schema.Class("Config.Info")({ + $schema: Schema.optional(Schema.String).annotate({ + description: "JSON schema reference for configuration validation", + }), + shell: Schema.String.pipe(Schema.optional).annotate({ + description: "Default shell to use for terminal and shell tool execution", + }), + model: Schema.String.pipe(Schema.optional).annotate({ + description: "Default model to use when no session or agent model is selected", + }), + default_agent: Schema.String.pipe(Schema.optional).annotate({ + description: "Default primary agent to use when no session agent is selected", + }), + autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]) + .pipe(Schema.optional) + .annotate({ + description: "Automatically update or notify when a new version is available", + }), + share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({ + description: "Control whether sessions may be shared manually, automatically, or not at all", + }), + enterprise: Schema.Struct({ + url: Schema.String.pipe(Schema.optional), + }) + .pipe(Schema.optional) + .annotate({ + description: "Enterprise sharing service configuration", + }), + username: Schema.String.pipe(Schema.optional).annotate({ + description: "Username displayed in conversations and used for telemetry identity", + }), + permissions: Permission.Ruleset.pipe(Schema.optional).annotate({ + description: "Ordered tool permission rules applied to agent tool use", + }), + agents: Schema.Record(Schema.String, ConfigAgent.Info).pipe(Schema.optional).annotate({ + description: "Named built-in agent overrides and custom agent definitions", + }), + snapshots: Schema.Boolean.pipe(Schema.optional).annotate({ + description: "Enable snapshots used for undo and revert behavior", + }), + watcher: ConfigWatcher.Info.pipe(Schema.optional).annotate({ + description: "Filesystem watcher configuration", + }), + formatter: ConfigFormatter.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in formatters or configure formatter overrides", + }), + lsp: ConfigLSP.Info.pipe(Schema.optional).annotate({ + description: "Enable built-in language servers or configure server overrides", + }), + attachments: ConfigAttachments.Info.pipe(Schema.optional).annotate({ + description: "Attachment processing configuration", + }), + tool_output: ConfigToolOutput.Info.pipe(Schema.optional).annotate({ + description: "Tool output truncation thresholds", + }), + mcp: ConfigMCP.Info.pipe(Schema.optional).annotate({ + description: "MCP server configuration", + }), + compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({ + description: "Conversation compaction behavior", + }), + skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ + description: "Additional paths or URLs to discover skills from", + }), + commands: Schema.Record(Schema.String, ConfigCommand.Info).pipe(Schema.optional).annotate({ + description: "Named slash command definitions", + }), + instructions: Schema.String.pipe(Schema.Array, Schema.optional).annotate({ + description: "Additional paths or URLs supplying ambient instructions", + }), + references: ConfigReference.Info.pipe(Schema.optional).annotate({ + description: "Named local directories or Git repositories available as external context", + }), + plugins: ConfigPlugin.Plugins.pipe(Schema.optional).annotate({ + description: "Ordered external plugin packages to load", + }), + experimental: ConfigExperimental.Experimental.pipe(Schema.optional), + providers: Schema.Record(Schema.String, ConfigProvider.Info).pipe(Schema.optional), +}) {} + +export class Document extends Schema.Class("Config.Document")({ + type: Schema.Literal("document"), + path: Schema.String.pipe(Schema.optional), + info: Info, +}) {} + +export class Directory extends Schema.Class("Config.Directory")({ + type: Schema.Literal("directory"), + path: AbsolutePath, +}) {} + +export type Entry = Document | Directory + +export function latest(entries: readonly Entry[], key: K): Info[K] | undefined { + return entries + .filter((entry): entry is Document => entry.type === "document") + .findLast((entry) => entry.info[key] !== undefined)?.info[key] +} + +export interface Interface { + /** Returns location config documents and supplemental directories from lowest to highest priority. */ + readonly entries: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Config") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const global = yield* Global.Service + const location = yield* Location.Service + const policy = yield* Policy.Service + const names = ["opencode.json", "opencode.jsonc"] + const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const + const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions) + const decodeV1Info = Schema.decodeUnknownOption(ConfigV1.Info, decodeOptions) + + const loadFile = Effect.fnUntraced(function* (filepath: string) { + const text = yield* fs.readFileStringSafe(filepath) + if (!text) return + + const errors: ParseError[] = [] + const input: unknown = parse(text, errors, { allowTrailingComma: true }) + if (errors.length) return + + const info = Option.getOrUndefined( + ConfigMigrateV1.isV1(input) + ? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo)) + : decodeInfo(input), + ) + if (!info) return + return new Document({ type: "document", path: filepath, info }) + }) + + const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) { + return [ + ...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe( + Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)), + )), + new Directory({ type: "directory", path: directory }), + ] + }) + + const globalDirectory = AbsolutePath.make(global.config) + const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config) + // Read configuration once when this location opens. Later calls reuse these + // values until the location is reopened. + const discovered = locationIsGlobal + ? [] + : yield* fs + .up({ + targets: [".opencode", ...names.toReversed()], + start: location.directory, + stop: location.project.directory, + }) + .pipe(Effect.orDie) + const directories = [ + globalDirectory, + ...discovered + .filter((item) => path.basename(item) === ".opencode") + .toReversed() + .map((directory) => AbsolutePath.make(directory)), + ] + // A config closer to the opened directory should win over one higher up. + // Search starts nearby, so reverse the results before applying them. + const directPaths = discovered.filter((item) => path.basename(item) !== ".opencode").toReversed() + const direct = yield* Effect.forEach(directPaths, loadFile).pipe( + Effect.orDie, + Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)), + ) + const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie) + // Apply general settings first and more specific settings last: + // global config, project files, then `.opencode` files. + const configs = [...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()] + // Rules use the opposite order so a user-global rule can override a + // repository rule. Statement order inside each file stays unchanged. + yield* policy.load( + configs + .filter((config): config is Document => config.type === "document") + .toReversed() + .flatMap((config) => config.info.experimental?.policies ?? []), + ) + + return Service.of({ + entries: Effect.fn("Config.entries")(function* () { + return configs + }), + }) + }), +) + +export const locationLayer = layer.pipe(Layer.provideMerge(Policy.locationLayer)) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [FSUtil.node, Global.node, Location.node, Policy.node], +}) diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts new file mode 100644 index 0000000000000000000000000000000000000000..63df995f859fb8c28422f35dbb8c511829737866 --- /dev/null +++ b/packages/core/src/config/agent.ts @@ -0,0 +1,25 @@ +export * as ConfigAgent from "./agent" + +import { Schema } from "effect" +import { Permission } from "@opencode-ai/schema/permission" +import { ConfigProvider } from "./provider" +import { PositiveInt } from "../schema" + +export const Color = Schema.Union([ + Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), + Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), +]) + +export class Info extends Schema.Class("ConfigV2.Agent")({ + model: Schema.String.pipe(Schema.optional), + variant: Schema.String.pipe(Schema.optional), + request: ConfigProvider.Request.pipe(Schema.optional), + system: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + mode: Schema.Literals(["subagent", "primary", "all"]).pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), + color: Color.pipe(Schema.optional), + steps: PositiveInt.pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + permissions: Permission.Ruleset.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/attachments.ts b/packages/core/src/config/attachments.ts new file mode 100644 index 0000000000000000000000000000000000000000..f14775ff3f7d24cab32662498d93d9ff19582e13 --- /dev/null +++ b/packages/core/src/config/attachments.ts @@ -0,0 +1,15 @@ +export * as ConfigAttachments from "./attachments" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Image extends Schema.Class("ConfigV2.Attachments.Image")({ + auto_resize: Schema.Boolean.pipe(Schema.optional), + max_width: PositiveInt.pipe(Schema.optional), + max_height: PositiveInt.pipe(Schema.optional), + max_base64_bytes: PositiveInt.pipe(Schema.optional), +}) {} + +export class Info extends Schema.Class("ConfigV2.Attachments")({ + image: Image.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/command.ts b/packages/core/src/config/command.ts new file mode 100644 index 0000000000000000000000000000000000000000..394079b1e983a33aa0ca7243b9672a22cc78c28c --- /dev/null +++ b/packages/core/src/config/command.ts @@ -0,0 +1,12 @@ +export * as ConfigCommand from "./command" + +import { Schema } from "effect" + +export class Info extends Schema.Class("ConfigV2.Command")({ + template: Schema.String, + description: Schema.String.pipe(Schema.optional), + agent: Schema.String.pipe(Schema.optional), + model: Schema.String.pipe(Schema.optional), + variant: Schema.String.pipe(Schema.optional), + subtask: Schema.Boolean.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/compaction.ts b/packages/core/src/config/compaction.ts new file mode 100644 index 0000000000000000000000000000000000000000..3c5960c835568c73ef4167b90b2869e8e1a3b378 --- /dev/null +++ b/packages/core/src/config/compaction.ts @@ -0,0 +1,15 @@ +export * as ConfigCompaction from "./compaction" + +import { Schema } from "effect" +import { NonNegativeInt } from "../schema" + +export class Keep extends Schema.Class("ConfigV2.Compaction.Keep")({ + tokens: NonNegativeInt.pipe(Schema.optional), +}) {} + +export class Info extends Schema.Class("ConfigV2.Compaction")({ + auto: Schema.Boolean.pipe(Schema.optional), + prune: Schema.Boolean.pipe(Schema.optional), + keep: Keep.pipe(Schema.optional), + buffer: NonNegativeInt.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/experimental.ts b/packages/core/src/config/experimental.ts new file mode 100644 index 0000000000000000000000000000000000000000..12a02635db658e0d42fb7cf72f2596fe755f7f97 --- /dev/null +++ b/packages/core/src/config/experimental.ts @@ -0,0 +1,18 @@ +export * as ConfigExperimental from "./experimental" + +import { Schema } from "effect" +import { Catalog } from "../catalog" +import { Policy as PolicyV2 } from "../policy" + +// Each core domain exports the policy actions it supports. Adding an action to +// this union makes it valid in authored config while keeping Policy generic. +export const PolicyAction = Schema.Union([Catalog.PolicyActions]) + +export class Policy extends Schema.Class("ConfigV2.Experimental.Policy")({ + ...PolicyV2.Info.fields, + action: PolicyAction, +}) {} + +export class Experimental extends Schema.Class("ConfigV2.Experimental")({ + policies: Policy.pipe(Schema.Array, Schema.optional), +}) {} diff --git a/packages/core/src/config/formatter.ts b/packages/core/src/config/formatter.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1f90302d1a99b5e06040725428f19f15b93bdc7 --- /dev/null +++ b/packages/core/src/config/formatter.ts @@ -0,0 +1,12 @@ +export * as ConfigFormatter from "./formatter" + +import { Schema } from "effect" + +export class Entry extends Schema.Class("ConfigV2.Formatter.Entry")({ + disabled: Schema.Boolean.pipe(Schema.optional), + command: Schema.String.pipe(Schema.Array, Schema.optional), + environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + extensions: Schema.String.pipe(Schema.Array, Schema.optional), +}) {} + +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]) diff --git a/packages/core/src/config/lsp.ts b/packages/core/src/config/lsp.ts new file mode 100644 index 0000000000000000000000000000000000000000..651597befddec760addf139c0edc82bb3e65910e --- /dev/null +++ b/packages/core/src/config/lsp.ts @@ -0,0 +1,18 @@ +export * as ConfigLSP from "./lsp" + +import { Schema } from "effect" + +export const Disabled = Schema.Struct({ + disabled: Schema.Literal(true), +}) + +export class Server extends Schema.Class("ConfigV2.LSP.Server")({ + command: Schema.String.pipe(Schema.Array), + extensions: Schema.String.pipe(Schema.Array, Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + env: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + initialization: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) {} + +export const Entry = Schema.Union([Disabled, Server]) +export const Info = Schema.Union([Schema.Boolean, Schema.Record(Schema.String, Entry)]) diff --git a/packages/core/src/config/markdown.ts b/packages/core/src/config/markdown.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1d74e649eb4363446d05d6ae65b369c68d93b29 --- /dev/null +++ b/packages/core/src/config/markdown.ts @@ -0,0 +1,36 @@ +export * as ConfigMarkdown from "./markdown" + +import matter from "gray-matter" +export function parse(content: string) { + try { + return matter(content) + } catch { + return matter(sanitize(content)) + } +} + +export function parseOption(content: string) { + try { + return parse(content) + } catch { + return undefined + } +} + +// Other coding agents accept unquoted colons in frontmatter values. Retry +// those values as YAML block scalars so existing config files keep working. +export function sanitize(content: string) { + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/) + if (!match) return content + const frontmatter = match[1] + const result = frontmatter.split(/\r?\n/).flatMap((line) => { + if (line.trim().startsWith("#") || line.trim() === "" || /^\s+/.test(line)) return [line] + const entry = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/) + if (!entry) return [line] + const value = entry[2].trim() + if (value === "" || value === ">" || value === "|" || value.startsWith('"') || value.startsWith("'")) return [line] + if (!value.includes(":")) return [line] + return [`${entry[1]}: |-`, ` ${value}`] + }) + return content.replace(frontmatter, () => result.join("\n")) +} diff --git a/packages/core/src/config/mcp.ts b/packages/core/src/config/mcp.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3a5ac9b256bd6b349b45c31a344dbf92b881990 --- /dev/null +++ b/packages/core/src/config/mcp.ts @@ -0,0 +1,48 @@ +export * as ConfigMCP from "./mcp" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Timeout extends Schema.Class("ConfigV2.MCP.Timeout")({ + startup: PositiveInt.pipe(Schema.optional).annotate({ + description: "Maximum time in milliseconds to establish and initialize the MCP server.", + }), + request: PositiveInt.pipe(Schema.optional).annotate({ + description: "Maximum time in milliseconds to wait for each MCP request after initialization.", + }), +}) {} + +export class Local extends Schema.Class("ConfigV2.MCP.Local")({ + type: Schema.Literal("local"), + command: Schema.String.pipe(Schema.Array), + cwd: Schema.String.pipe(Schema.optional).annotate({ + description: "Working directory for the MCP server process. Relative paths resolve from the workspace directory.", + }), + environment: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + timeout: Timeout.pipe(Schema.optional), +}) {} + +export class OAuth extends Schema.Class("ConfigV2.MCP.OAuth")({ + client_id: Schema.String.pipe(Schema.optional), + client_secret: Schema.String.pipe(Schema.optional), + scope: Schema.String.pipe(Schema.optional), + callback_port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })).pipe(Schema.optional), + redirect_uri: Schema.String.pipe(Schema.optional), +}) {} + +export class Remote extends Schema.Class("ConfigV2.MCP.Remote")({ + type: Schema.Literal("remote"), + url: Schema.String, + headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + oauth: Schema.Union([OAuth, Schema.Literal(false)]).pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + timeout: Timeout.pipe(Schema.optional), +}) {} + +export const Server = Schema.Union([Local, Remote]).pipe(Schema.toTaggedUnion("type")) + +export class Info extends Schema.Class("ConfigV2.MCP")({ + timeout: Timeout.pipe(Schema.optional), + servers: Schema.Record(Schema.String, Server).pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/plugin.ts b/packages/core/src/config/plugin.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5fd6661ff52d001b15c967d935c8e0ede2f73a3 --- /dev/null +++ b/packages/core/src/config/plugin.ts @@ -0,0 +1,13 @@ +export * as ConfigPlugin from "./plugin" + +import { Schema } from "effect" + +export class Entry extends Schema.Class("ConfigV2.Plugin.Entry")({ + package: Schema.String, + options: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) {} + +export const Plugin = Schema.Union([Schema.String, Entry]) +export type Plugin = typeof Plugin.Type + +export const Plugins = Plugin.pipe(Schema.Array) diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts new file mode 100644 index 0000000000000000000000000000000000000000..0635aa6291c140f210514b92ec115f38a66f494b --- /dev/null +++ b/packages/core/src/config/plugin/agent.ts @@ -0,0 +1,179 @@ +export * as ConfigAgentPlugin from "./agent" + +import { define } from "../../plugin/internal" +import path from "path" +import { Effect, Option, Schema } from "effect" +import { AgentV2 } from "../../agent" +import { Config } from "../../config" +import { ConfigAgent } from "../agent" +import { ConfigMarkdown } from "../markdown" +import { FSUtil } from "../../fs-util" +import { ModelV2 } from "../../model" +import { ConfigAgentV1 } from "../../v1/config/agent" +import { ConfigMigrateV1 } from "../../v1/config/migrate" +import { Global } from "../../global" +import { PermissionV2 } from "../../permission" +import type { LocationMutation } from "../../location-mutation" +import type { ReadTool } from "../../tool/read" +import type { EditTool } from "../../tool/edit" + +const legacySources = [ + { pattern: "{agent,agents}/**/*.md", primary: false }, + { pattern: "{mode,modes}/*.md", primary: true }, +] as const +const decodeAgent = Schema.decodeUnknownOption(ConfigAgent.Info) +const decodeLegacyAgent = Schema.decodeUnknownOption(ConfigAgentV1.Info) +const decodeConfig = Schema.decodeUnknownOption(Config.Info) +type PathAction = + | LocationMutation.ExternalDirectoryAuthorization["action"] + | typeof ReadTool.name + | typeof EditTool.name +const pathActions = ["external_directory", "read", "edit"] as const satisfies readonly PathAction[] +const agentKeys = new Set([ + "model", + "variant", + "request", + "system", + "description", + "mode", + "hidden", + "color", + "steps", + "disabled", + "permissions", +]) + +export const Plugin = define({ + id: "config-agent", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const fs = yield* FSUtil.Service + const global = yield* Global.Service + yield* ctx.agent.transform( + Effect.fn(function* (draft) { + const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { + if (entry.type === "document") return Effect.succeed([entry]) + return Effect.gen(function* () { + const files = yield* discover(fs, entry.path) + return yield* Effect.forEach(files, (file) => + fs.readFileStringSafe(file.filepath).pipe( + Effect.map((content) => content && decode(file, content)), + Effect.catch(() => Effect.succeed(undefined)), + ), + ).pipe( + Effect.map((documents) => + documents.filter((document): document is Config.Document => document !== undefined), + ), + ) + }) + }).pipe(Effect.map((documents) => documents.flat())) + const permissions = expandPermissions( + documents.flatMap((document) => document.info.permissions ?? []), + global.home, + ) + const configuredDefault = Config.latest(documents, "default_agent") + if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault)) + for (const current of draft.list()) { + draft.update(current.id, (agent) => agent.permissions.push(...permissions)) + } + + for (const document of documents) { + for (const [id, item] of Object.entries(document.info.agents ?? {})) { + const agentID = AgentV2.ID.make(id) + if (item.disabled) { + draft.remove(agentID) + continue + } + + const exists = draft.get(agentID) !== undefined + draft.update(agentID, (agent) => { + if (!exists) agent.permissions.push(...permissions) + if (item.model !== undefined) { + const model = ModelV2.parse(item.model) + agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant } + } + if (item.variant !== undefined && agent.model !== undefined) { + agent.model.variant = ModelV2.VariantID.make(item.variant) + } + if (item.request !== undefined) { + Object.assign(agent.request.headers, item.request.headers ?? {}) + Object.assign(agent.request.body, item.request.body ?? {}) + } + if (item.system !== undefined) agent.system = item.system + if (item.description !== undefined) agent.description = item.description + if (item.mode !== undefined) agent.mode = item.mode + if (item.hidden !== undefined) agent.hidden = item.hidden + if (item.color !== undefined) agent.color = item.color + if (item.steps !== undefined) agent.steps = item.steps + if (item.permissions !== undefined) { + agent.permissions.push(...expandPermissions(item.permissions, global.home)) + } + }) + } + } + }), + ) + }), +}) + +function expandPermissions(rules: PermissionV2.Ruleset, home: string): PermissionV2.Ruleset { + // Expand only resources tools resolve as filesystem paths. Bash resources are raw shell text: + // rewriting `$HOME/private/**` would miss `$HOME/private/key`, and safe expansion needs shell-aware parsing. + return rules.map((rule) => + isPathAction(rule.action) ? { ...rule, resource: expandHome(rule.resource, home) } : rule, + ) +} + +function isPathAction(action: string): action is PathAction { + return pathActions.some((item) => item === action) +} + +function expandHome(resource: string, home: string) { + if (resource.startsWith("~/")) return home + resource.slice(1) + if (resource === "~") return home + if (resource === "$HOME") return home + if (resource.startsWith("$HOME/")) return home + resource.slice(5) + if (resource.startsWith("$HOME\\")) return home + resource.slice(5) + return resource +} + +function discover(fs: FSUtil.Interface, directory: string) { + return Effect.forEach(legacySources, (source) => + fs + .glob(source.pattern, { cwd: directory, absolute: true, dot: true, symlink: true }) + .pipe( + Effect.map((files) => files.toSorted().map((filepath) => ({ directory, filepath, primary: source.primary }))), + ), + ).pipe( + Effect.map((files) => files.flat()), + Effect.catch(() => Effect.succeed([])), + ) +} + +function decode(file: { directory: string; filepath: string; primary: boolean }, content: string) { + const markdown = ConfigMarkdown.parseOption(content) + if (!markdown) return + const name = path + .relative(file.directory, file.filepath) + .replaceAll("\\", "/") + .replace(/^(agent|agents|mode|modes)\//, "") + .replace(/\.md$/, "") + const body = markdown.content.trim() + const legacy = Object.keys(markdown.data).some((key) => !agentKeys.has(key)) + const agent = Option.getOrUndefined( + legacy + ? Option.map( + decodeLegacyAgent({ name, ...markdown.data, prompt: body }, { errors: "all", propertyOrder: "original" }), + ConfigMigrateV1.migrateAgent, + ) + : decodeAgent({ ...markdown.data, system: body }, { errors: "all", propertyOrder: "original" }), + ) + if (!agent) return + const info = Option.getOrUndefined( + decodeConfig({ + agents: { [name]: file.primary ? { ...agent, mode: "primary" } : agent }, + }), + ) + if (!info) return + return new Config.Document({ type: "document", path: file.filepath, info }) +} diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9b31f8e45a1ad10c3543b4ba700ddb178ddd227 --- /dev/null +++ b/packages/core/src/config/plugin/command.ts @@ -0,0 +1,83 @@ +export * as ConfigCommandPlugin from "./command" + +import { define } from "../../plugin/internal" +import path from "path" +import { Effect, Option, Schema } from "effect" +import { CommandV2 } from "../../command" +import { Config } from "../../config" +import { FSUtil } from "../../fs-util" +import { ModelV2 } from "../../model" +import { ConfigCommand } from "../command" +import { ConfigMarkdown } from "../markdown" + +const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info) + +export const Plugin = define({ + id: "config-command", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const fs = yield* FSUtil.Service + yield* ctx.command.transform( + Effect.fn(function* (draft) { + const documents = yield* Effect.forEach(yield* config.entries(), (entry) => { + if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }]) + return loadDirectory(fs, entry.path).pipe( + Effect.map((commands) => [ + { commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) }, + ]), + ) + }).pipe(Effect.map((documents) => documents.flat())) + for (const document of documents) { + for (const [name, command] of Object.entries(document.commands ?? {})) { + draft.update(name, (item) => { + item.template = command.template + if (command.description !== undefined) item.description = command.description + if (command.agent !== undefined) item.agent = command.agent + if (command.model !== undefined) { + const model = ModelV2.parse(command.model) + item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant } + } + if (command.variant !== undefined && item.model !== undefined) { + item.model.variant = ModelV2.VariantID.make(command.variant) + } + if (command.subtask !== undefined) item.subtask = command.subtask + }) + } + } + }), + ) + }), +}) + +function loadDirectory(fs: FSUtil.Interface, directory: string) { + return Effect.gen(function* () { + const files = yield* fs + .glob("{command,commands}/**/*.md", { cwd: directory, absolute: true, dot: true, symlink: true }) + .pipe(Effect.catch(() => Effect.succeed([] as string[]))) + return yield* Effect.forEach(files.toSorted(), (filepath) => + fs.readFileStringSafe(filepath).pipe( + Effect.map((content) => (content === undefined ? undefined : decode(directory, filepath, content))), + Effect.catch(() => Effect.succeed(undefined)), + ), + ).pipe( + Effect.map((commands) => + commands.filter((command): command is { name: string; info: ConfigCommand.Info } => command !== undefined), + ), + ) + }) +} + +function decode(directory: string, filepath: string, content: string) { + const markdown = ConfigMarkdown.parseOption(content) + if (!markdown) return + const info = Option.getOrUndefined(decodeCommand({ ...markdown.data, template: markdown.content.trim() })) + if (!info) return + return { + name: path + .relative(directory, filepath) + .replaceAll("\\", "/") + .replace(/^(command|commands)\//, "") + .replace(/\.md$/, ""), + info, + } +} diff --git a/packages/core/src/config/plugin/external.ts b/packages/core/src/config/plugin/external.ts new file mode 100644 index 0000000000000000000000000000000000000000..d8ace63b3dd8a83419d6d5d1167189edc165c7a9 --- /dev/null +++ b/packages/core/src/config/plugin/external.ts @@ -0,0 +1,91 @@ +export * as ConfigExternalPlugin from "./external" + +import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect" +import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise" +import { Effect, Schema } from "effect" +import path from "path" +import { fileURLToPath, pathToFileURL } from "url" +import { Config } from "../../config" +import { FSUtil } from "../../fs-util" +import { Location } from "../../location" +import { Npm } from "../../npm" +import { define } from "../../plugin/internal" +import { PluginPromise } from "../../plugin/promise" + +const PluginModule = Schema.Struct({ + default: Schema.Union([ + Schema.Struct({ + id: Schema.String, + effect: Schema.declare( + (input): input is EffectPlugin["effect"] => typeof input === "function", + ), + }), + Schema.Struct({ + id: Schema.String, + setup: Schema.declare( + (input): input is PromisePlugin["setup"] => typeof input === "function", + ), + }), + ]), +}) + +export const Plugin = define({ + id: "config-plugin", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const npm = yield* Npm.Service + yield* Effect.gen(function* () { + const configured: { package: string; options?: Record }[] = [] + + for (const entry of yield* config.entries()) { + if (entry.type === "document") { + const directory = entry.path ? path.dirname(entry.path) : location.directory + for (const item of entry.info.plugins ?? []) { + const ref = typeof item === "string" ? { package: item } : item + const packageName = (() => { + if (ref.package.startsWith("file://")) return fileURLToPath(ref.package) + if (ref.package.startsWith("./") || ref.package.startsWith("../")) { + return path.resolve(directory, ref.package) + } + return ref.package + })() + configured.push({ package: packageName, options: ref.options }) + } + } + + if (entry.type === "directory") { + const files = yield* fs + .glob("{plugin,plugins}/*.{ts,js}", { + cwd: entry.path, + absolute: true, + include: "file", + dot: true, + symlink: true, + }) + .pipe(Effect.orElseSucceed(() => [])) + files.sort() + for (const file of files) configured.push({ package: file }) + } + } + + for (const ref of configured) { + yield* Effect.gen(function* () { + const entrypoint = path.isAbsolute(ref.package) + ? pathToFileURL(ref.package).href + : (yield* npm.add(ref.package)).entrypoint + if (!entrypoint) return + + const mod = yield* Effect.promise(() => import(entrypoint)) + const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default + const plugin = "effect" in value ? value : PluginPromise.fromPromise(value) + yield* ctx.plugin.add({ + id: plugin.id, + effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }), + }) + }).pipe(Effect.ignoreCause) + } + }).pipe(Effect.forkScoped({ startImmediately: true })) + }), +}) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f6e0528da7334228da7b5238ea9319c3b2247d4 --- /dev/null +++ b/packages/core/src/config/plugin/provider.ts @@ -0,0 +1,113 @@ +export * as ConfigProviderPlugin from "./provider" + +import { define } from "../../plugin/internal" +import { Effect } from "effect" +import { Config } from "../../config" +import { ModelV2 } from "../../model" +import { ProviderV2 } from "../../provider" + +export const Plugin = define({ + id: "config-provider", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + yield* ctx.integration.transform( + Effect.fn(function* (integrations) { + const files = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document") + const configuredIntegrations = new Set( + files.flatMap((file) => + Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => + provider.env === undefined ? [] : [id], + ), + ), + ) + for (const file of files) { + for (const [id, item] of Object.entries(file.info.providers ?? {})) { + const integrationID = id + if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue + integrations.update(integrationID, (integration) => { + integration.name = item.name ?? integration.name + }) + if (item.env !== undefined) { + integrations.method.update({ + integrationID, + method: { type: "env", names: [...item.env] }, + }) + } + } + } + }), + ) + + yield* ctx.catalog.transform( + Effect.fn(function* (catalog) { + const entries = yield* config.entries() + const files = entries.filter((entry): entry is Config.Document => entry.type === "document") + const configuredDefault = Config.latest(entries, "model") + if (configuredDefault !== undefined) { + const model = ModelV2.parse(configuredDefault) + catalog.model.default.set(model.providerID, model.modelID) + } + for (const file of files) { + for (const [id, item] of Object.entries(file.info.providers ?? {})) { + const providerID = id + catalog.provider.update(providerID, (provider) => { + if (item.name !== undefined) provider.name = item.name + if (item.api !== undefined) provider.api = { ...item.api } + if (item.request !== undefined) { + Object.assign(provider.request.headers, item.request.headers) + Object.assign(provider.request.body, item.request.body) + } + }) + for (const [id, config] of Object.entries(item.models ?? {})) { + catalog.model.update(providerID, id, (model) => { + if (config.family !== undefined) model.family = config.family + if (config.name !== undefined) model.name = config.name + if (config.api !== undefined) model.api = { ...model.api, ...config.api } + if (config.capabilities !== undefined) { + model.capabilities = { + tools: config.capabilities.tools, + input: [...config.capabilities.input], + output: [...config.capabilities.output], + } + } + if (config.request !== undefined) { + Object.assign(model.request.headers, config.request.headers) + Object.assign(model.request.body, config.request.body) + if (config.request.variant !== undefined) model.request.variant = config.request.variant + } + if (config.variants !== undefined) { + for (const variant of config.variants) { + let existing = model.variants.find((item) => item.id === variant.id) + if (!existing) { + existing = { + id: variant.id, + headers: {}, + body: {}, + } + model.variants.push(existing) + } + Object.assign(existing.headers, variant.headers) + Object.assign(existing.body, variant.body) + } + } + if (config.cost !== undefined) { + model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({ + tier: cost.tier && { ...cost.tier }, + input: cost.input, + output: cost.output, + cache: { + read: cost.cache?.read ?? 0, + write: cost.cache?.write ?? 0, + }, + })) + } + if (config.disabled !== undefined) model.enabled = !config.disabled + if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit } + }) + } + } + } + }), + ) + }), +}) diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5fa53c41129ccfabb91d077ec7c2c1fdb3b68ba --- /dev/null +++ b/packages/core/src/config/plugin/reference.ts @@ -0,0 +1,70 @@ +export * as ConfigReferencePlugin from "./reference" + +import { define } from "../../plugin/internal" +import path from "path" +import { Effect } from "effect" +import { Config } from "../../config" +import { ConfigReference } from "../reference" +import { Reference } from "../../reference" +import { AbsolutePath } from "../../schema" +import { Global } from "../../global" +import { Location } from "../../location" + +export const Plugin = define({ + id: "core/config-reference", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const location = yield* Location.Service + const global = yield* Global.Service + yield* ctx.reference.transform( + Effect.fn(function* (draft) { + const entries = new Map() + for (const doc of (yield* config.entries()).filter( + (entry): entry is Config.Document => entry.type === "document", + )) { + const directory = doc.path ? path.dirname(doc.path) : location.directory + for (const [name, entry] of Object.entries(doc.info.references ?? {})) { + if (!validAlias(name)) continue + const description = typeof entry === "string" ? undefined : entry.description + const hidden = typeof entry === "string" ? undefined : entry.hidden + entries.set( + name, + local(entry) + ? Reference.LocalSource.make({ + type: "local", + path: AbsolutePath.make( + localPath(directory, global.home, typeof entry === "string" ? entry : entry.path), + ), + ...(description === undefined ? {} : { description }), + ...(hidden === undefined ? {} : { hidden }), + }) + : Reference.GitSource.make({ + type: "git", + repository: typeof entry === "string" ? entry : entry.repository, + ...(entry.branch === undefined ? {} : { branch: entry.branch }), + ...(description === undefined ? {} : { description }), + ...(hidden === undefined ? {} : { hidden }), + }), + ) + } + } + for (const [name, source] of entries) draft.add(name, source) + }), + ) + }), +}) + +function validAlias(name: string) { + return name.length > 0 && !/[\/\s`,]/.test(name) +} + +function local(entry: ConfigReference.Entry): entry is string | ConfigReference.Local { + return typeof entry === "string" + ? entry.startsWith(".") || entry.startsWith("/") || entry.startsWith("~") + : "path" in entry +} + +function localPath(directory: string, home: string, value: string) { + if (value.startsWith("~/")) return path.join(home, value.slice(2)) + return path.isAbsolute(value) ? value : path.resolve(directory, value) +} diff --git a/packages/core/src/config/plugin/skill.ts b/packages/core/src/config/plugin/skill.ts new file mode 100644 index 0000000000000000000000000000000000000000..765992a765b25554be9de3e47443e35d54aed3ce --- /dev/null +++ b/packages/core/src/config/plugin/skill.ts @@ -0,0 +1,50 @@ +export * as ConfigSkillPlugin from "./skill" + +import { define } from "../../plugin/internal" +import path from "path" +import { Effect } from "effect" +import { Config } from "../../config" +import { AbsolutePath } from "../../schema" +import { SkillV2 } from "../../skill" +import { Global } from "../../global" +import { Location } from "../../location" + +export const Plugin = define({ + id: "config-skill", + effect: Effect.fn(function* (ctx) { + const config = yield* Config.Service + const global = yield* Global.Service + const location = yield* Location.Service + yield* ctx.skill.transform( + Effect.fn(function* (draft) { + const entries = yield* config.entries() + const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : [])) + const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])) + for (const directory of directories) { + draft.source( + SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }), + ) + draft.source( + SkillV2.DirectorySource.make({ + type: "directory", + path: AbsolutePath.make(path.join(directory, "skills")), + }), + ) + } + for (const item of items) { + if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) { + draft.source(SkillV2.UrlSource.make({ type: "url", url: item })) + continue + } + const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item + draft.source( + SkillV2.DirectorySource.make({ + type: "directory", + path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)), + }), + ) + } + }), + ) + }), +}) diff --git a/packages/core/src/config/provider.ts b/packages/core/src/config/provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b547570783a08f90db666e1eee37b4a66cc3e5c --- /dev/null +++ b/packages/core/src/config/provider.ts @@ -0,0 +1,71 @@ +export * as ConfigProvider from "./provider" + +import { Schema } from "effect" +import { ProviderV2 } from "../provider" +import { ModelV2 } from "../model" + +export class Request extends Schema.Class("ConfigV2.Provider.Request")({ + headers: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional), + body: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), +}) {} + +class Cache extends Schema.Class("ConfigV2.Model.Cost.Cache")({ + read: Schema.Finite.pipe(Schema.optional), + write: Schema.Finite.pipe(Schema.optional), +}) {} + +class Cost extends Schema.Class("ConfigV2.Model.Cost")({ + tier: Schema.Struct({ + type: Schema.Literal("context"), + size: Schema.Int, + }).pipe(Schema.optional), + input: Schema.Finite, + output: Schema.Finite, + cache: Cache.pipe(Schema.optional), +}) {} + +class Limit extends Schema.Class("ConfigV2.Model.Limit")({ + context: Schema.Int.pipe(Schema.optional), + input: Schema.Int.pipe(Schema.optional), + output: Schema.Int.pipe(Schema.optional), +}) {} + +const ModelApi = Schema.Union([ + Schema.Struct({ + id: ModelV2.ID.pipe(Schema.optional), + ...ProviderV2.AISDK.fields, + }), + Schema.Struct({ + id: ModelV2.ID.pipe(Schema.optional), + ...ProviderV2.Native.fields, + }), + Schema.Struct({ + id: ModelV2.ID, + }), +]) + +class Model extends Schema.Class("ConfigV2.Model")({ + family: ModelV2.Family.pipe(Schema.optional), + name: Schema.String.pipe(Schema.optional), + api: ModelApi.pipe(Schema.optional), + capabilities: ModelV2.Capabilities.pipe(Schema.optional), + request: Schema.Struct({ + ...Request.fields, + variant: Schema.String.pipe(Schema.optional), + }).pipe(Schema.optional), + variants: Schema.Struct({ + id: ModelV2.VariantID, + ...Request.fields, + }).pipe(Schema.Array, Schema.optional), + cost: Schema.Union([Cost, Cost.pipe(Schema.Array)]).pipe(Schema.optional), + disabled: Schema.Boolean.pipe(Schema.optional), + limit: Limit.pipe(Schema.optional), +}) {} + +export class Info extends Schema.Class("ConfigV2.Provider")({ + name: Schema.String.pipe(Schema.optional), + env: Schema.String.pipe(Schema.Array, Schema.optional), + api: ProviderV2.Api.pipe(Schema.optional), + request: Request.pipe(Schema.optional), + models: Schema.Record(Schema.String, Model).pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/reference.ts b/packages/core/src/config/reference.ts new file mode 100644 index 0000000000000000000000000000000000000000..4518eb4f87d630c469d99a761ca9d97690c690c3 --- /dev/null +++ b/packages/core/src/config/reference.ts @@ -0,0 +1,22 @@ +export * as ConfigReference from "./reference" + +import { Schema } from "effect" + +export class Git extends Schema.Class("ConfigV2.Reference.Git")({ + repository: Schema.String, + branch: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), +}) {} + +export class Local extends Schema.Class("ConfigV2.Reference.Local")({ + path: Schema.String, + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), +}) {} + +export const Entry = Schema.Union([Schema.String, Git, Local]) +export type Entry = typeof Entry.Type + +export const Info = Schema.Record(Schema.String, Entry) +export type Info = typeof Info.Type diff --git a/packages/core/src/config/tool-output.ts b/packages/core/src/config/tool-output.ts new file mode 100644 index 0000000000000000000000000000000000000000..53e4d4d088b789c1d4716e0e0f1703d4832ccf3b --- /dev/null +++ b/packages/core/src/config/tool-output.ts @@ -0,0 +1,9 @@ +export * as ConfigToolOutput from "./tool-output" + +import { Schema } from "effect" +import { PositiveInt } from "../schema" + +export class Info extends Schema.Class("ConfigV2.ToolOutput")({ + max_lines: PositiveInt.pipe(Schema.optional), + max_bytes: PositiveInt.pipe(Schema.optional), +}) {} diff --git a/packages/core/src/config/watcher.ts b/packages/core/src/config/watcher.ts new file mode 100644 index 0000000000000000000000000000000000000000..2df6c876bfc65bd7c10d8fcda91c97eaf888554d --- /dev/null +++ b/packages/core/src/config/watcher.ts @@ -0,0 +1,7 @@ +export * as ConfigWatcher from "./watcher" + +import { Schema } from "effect" + +export class Info extends Schema.Class("ConfigV2.Watcher")({ + ignore: Schema.String.pipe(Schema.Array, Schema.optional), +}) {} diff --git a/packages/core/src/control-plane/move-session.ts b/packages/core/src/control-plane/move-session.ts new file mode 100644 index 0000000000000000000000000000000000000000..e227f88df53924a49ea9d7fdb549c41a1b196c42 --- /dev/null +++ b/packages/core/src/control-plane/move-session.ts @@ -0,0 +1,148 @@ +export * as MoveSession from "./move-session" + +import { Context, DateTime, Effect, Layer, Schema } from "effect" +import { makeGlobalNode } from "../effect/app-node" +import { EventV2 } from "../event" +import { Git } from "../git" +import { Location } from "../location" +import { ProjectV2 } from "../project" +import { SessionV2 } from "../session" +import { SessionEvent } from "../session/event" +import { SessionSchema } from "../session/schema" +import { SessionStore } from "../session/store" +import { AbsolutePath, RelativePath } from "../schema" +import path from "path" + +export const Destination = Schema.Struct({ + directory: AbsolutePath, +}).annotate({ identifier: "MoveSession.Destination" }) +export type Destination = typeof Destination.Type + +export const Input = Schema.Struct({ + sessionID: SessionSchema.ID, + destination: Destination, + moveChanges: Schema.optional(Schema.Boolean), +}).annotate({ identifier: "MoveSession.Input" }) +export type Input = typeof Input.Type + +export class DestinationProjectMismatchError extends Schema.TaggedErrorClass()( + "MoveSession.DestinationProjectMismatchError", + { + expected: ProjectV2.ID, + actual: ProjectV2.ID, + }, +) {} + +export class ApplyChangesError extends Schema.TaggedErrorClass()("MoveSession.ApplyChangesError", { + message: Schema.String, +}) {} + +export class CaptureChangesError extends Schema.TaggedErrorClass()( + "MoveSession.CaptureChangesError", + { + message: Schema.String, + }, +) {} + +export class ResetSourceChangesError extends Schema.TaggedErrorClass()( + "MoveSession.ResetSourceChangesError", + { + directory: AbsolutePath, + message: Schema.String, + cause: Schema.optional(Schema.Defect()), + }, +) {} + +export type Error = + | SessionV2.NotFoundError + | DestinationProjectMismatchError + | CaptureChangesError + | ApplyChangesError + | ResetSourceChangesError + +export interface Interface { + readonly moveSession: (input: Input) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ControlPlaneMoveSession") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const git = yield* Git.Service + const events = yield* EventV2.Service + const project = yield* ProjectV2.Service + const sessions = yield* SessionStore.Service + + const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) { + const current = yield* sessions.get(input.sessionID) + if (!current) return yield* new SessionV2.NotFoundError({ sessionID: input.sessionID }) + const directory = AbsolutePath.make(input.destination.directory) + if (current.location.directory === directory) return + + const source = yield* project.resolve(current.location.directory) + const destination = yield* project.resolve(directory) + if (current.projectID !== destination.id) { + return yield* new DestinationProjectMismatchError({ expected: current.projectID, actual: destination.id }) + } + + const moveChanges = input.moveChanges && source.directory !== destination.directory + const sourceRepository = moveChanges ? yield* git.repo.discover(current.location.directory) : undefined + if (moveChanges && !sourceRepository) + return yield* new CaptureChangesError({ message: "Source is not a Git repository" }) + const patch = sourceRepository + ? yield* git.change + .capture({ repository: sourceRepository, path: current.location.directory }) + .pipe(Effect.mapError((error) => new CaptureChangesError({ message: error.message }))) + : Git.ChangeSet.make("") + if (patch) { + const repository = yield* git.repo.discover(directory) + if (!repository) return yield* new ApplyChangesError({ message: "Destination is not a Git repository" }) + yield* git.change + .apply({ repository, path: directory, changes: patch }) + .pipe(Effect.mapError((error) => new ApplyChangesError({ message: error.message }))) + } + + yield* events.publish(SessionEvent.Moved, { + sessionID: input.sessionID, + location: Location.Ref.make({ directory }), + subdirectory: RelativePath.make(path.relative(destination.directory, directory).replaceAll("\\", "/")), + timestamp: yield* DateTime.now, + }) + + if (patch) { + const repository = yield* git.repo.discover(current.location.directory) + if (!repository) + return yield* new ResetSourceChangesError({ + directory: current.location.directory, + message: "Source is not a Git repository", + }) + yield* git.change + .discard({ + repository, + path: current.location.directory, + index: "preserve", + untracked: "remove", + }) + .pipe( + Effect.mapError( + (error) => + new ResetSourceChangesError({ + directory: current.location.directory, + message: error.message, + cause: error.cause, + }), + ), + ) + } + }) + + return Service.of({ moveSession }) + }), +) + +export const node = makeGlobalNode({ + service: Service, + layer, + deps: [Git.node, EventV2.node, ProjectV2.node, SessionStore.node], +}) diff --git a/packages/core/src/control-plane/workspace.sql.ts b/packages/core/src/control-plane/workspace.sql.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef5195216acf535817143614ccb517ef02197c5f --- /dev/null +++ b/packages/core/src/control-plane/workspace.sql.ts @@ -0,0 +1,20 @@ +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" +import { ProjectTable } from "../project/sql" +import { ProjectV2 } from "../project" +import { WorkspaceV2 } from "../workspace" + +export const WorkspaceTable = sqliteTable("workspace", { + id: text().$type().primaryKey(), + type: text().notNull(), + name: text().notNull().default(""), + branch: text(), + directory: text(), + extra: text({ mode: "json" }), + project_id: text() + .$type() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + time_used: integer() + .notNull() + .$default(() => Date.now()), +}) diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts new file mode 100644 index 0000000000000000000000000000000000000000..554073970911fe8e77e3b4708c69e04c3eb065b1 --- /dev/null +++ b/packages/core/src/credential.ts @@ -0,0 +1,138 @@ +export * as Credential from "./credential" + +import { asc, eq } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Credential } from "@opencode-ai/schema/credential" +import { Integration } from "@opencode-ai/schema/integration" +import { Database } from "./database/database" +import { makeGlobalNode } from "./effect/app-node" +import { CredentialTable } from "./credential/sql" + +export const ID = Credential.ID +export type ID = Credential.ID + +export const OAuth = Credential.OAuth +export type OAuth = Credential.OAuth + +export const Key = Credential.Key +export type Key = Credential.Key + +export const Value = Credential.Value +export type Value = Credential.Value + +export class Info extends Schema.Class("Credential.Info")({ + id: ID, + integrationID: Integration.ID, + label: Schema.String, + value: Value, +}) {} + +export interface Interface { + /** Returns every stored credential. */ + readonly all: () => Effect.Effect + /** Returns stored credentials belonging to one integration. */ + readonly list: (integrationID: Integration.ID) => Effect.Effect + /** Returns one stored credential by ID. */ + readonly get: (id: ID) => Effect.Effect + /** Replaces any credential for an integration and returns the new record. */ + readonly create: (input: { + readonly integrationID: Integration.ID + readonly value: Value + readonly label?: string + }) => Effect.Effect + /** Updates the label or secret value of a stored credential. */ + readonly update: (id: ID, updates: Partial>) => Effect.Effect + /** Removes a stored credential. */ + readonly remove: (id: ID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Credential") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + const decode = Schema.decodeUnknownSync(Value) + const stored = (row: typeof CredentialTable.$inferSelect) => { + if (!row.integration_id) return + return new Info({ + id: row.id, + integrationID: row.integration_id, + label: row.label, + value: decode(row.value), + }) + } + + return Service.of({ + all: Effect.fn("Credential.all")(function* () { + return (yield* db + .select() + .from(CredentialTable) + .orderBy(asc(CredentialTable.time_created)) + .all() + .pipe(Effect.orDie)).flatMap((row) => { + const credential = stored(row) + return credential ? [credential] : [] + }) + }), + list: Effect.fn("Credential.list")(function* (integrationID) { + return (yield* db + .select() + .from(CredentialTable) + .where(eq(CredentialTable.integration_id, integrationID)) + .orderBy(asc(CredentialTable.time_created)) + .all() + .pipe(Effect.orDie)).flatMap((row) => { + const credential = stored(row) + return credential ? [credential] : [] + }) + }), + get: Effect.fn("Credential.get")(function* (id) { + const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie) + return row ? stored(row) : undefined + }), + create: Effect.fn("Credential.create")(function* (input) { + const credential = new Info({ + id: ID.create(), + integrationID: input.integrationID, + label: input.label ?? "default", + value: input.value, + }) + yield* db + .transaction((tx) => + Effect.gen(function* () { + yield* tx + .delete(CredentialTable) + .where(eq(CredentialTable.integration_id, credential.integrationID)) + .run() + yield* tx + .insert(CredentialTable) + .values({ + id: credential.id, + integration_id: credential.integrationID, + label: credential.label, + value: credential.value, + }) + .run() + }), + ) + .pipe(Effect.orDie) + return credential + }), + update: Effect.fn("Credential.update")(function* (id, updates) { + if (!updates.label && !updates.value) return + yield* db + .update(CredentialTable) + .set({ label: updates.label, value: updates.value }) + .where(eq(CredentialTable.id, id)) + .run() + .pipe(Effect.orDie) + }), + remove: Effect.fn("Credential.remove")(function* (id) { + yield* db.delete(CredentialTable).where(eq(CredentialTable.id, id)).run().pipe(Effect.orDie) + }), + }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] }) diff --git a/packages/core/src/credential/sql.ts b/packages/core/src/credential/sql.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6b59197a3bf053b8ff3ff10a3e3b3685c3e6d95 --- /dev/null +++ b/packages/core/src/credential/sql.ts @@ -0,0 +1,14 @@ +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" +import { Timestamps } from "../database/schema.sql" +import type { Credential } from "../credential" + +export const CredentialTable = sqliteTable("credential", { + id: text().$type().primaryKey(), + integration_id: text().$type(), + label: text().notNull(), + value: text({ mode: "json" }).$type().notNull(), + connector_id: text(), + method_id: text(), + active: integer({ mode: "boolean" }), + ...Timestamps, +}) diff --git a/packages/core/src/cross-spawn-spawner.ts b/packages/core/src/cross-spawn-spawner.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ea9022acf1d0c7fd4ffec72e41d66a403c1d998 --- /dev/null +++ b/packages/core/src/cross-spawn-spawner.ts @@ -0,0 +1,507 @@ +import type * as Arr from "effect/Array" +import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node" +import * as NodePath from "@effect/platform-node/NodePath" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import * as PlatformError from "effect/PlatformError" +import * as Predicate from "effect/Predicate" +import type * as Scope from "effect/Scope" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as ChildProcess from "effect/unstable/process/ChildProcess" +import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner" +import { + ChildProcessSpawner, + ExitCode, + make as makeSpawner, + makeHandle, + ProcessId, +} from "effect/unstable/process/ChildProcessSpawner" +import * as NodeChildProcess from "node:child_process" +import { PassThrough } from "node:stream" +import launch from "cross-spawn" +import { makeGlobalNode } from "./effect/app-node" +import { filesystem, path } from "./effect/app-node-platform" + +const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err))) + +const toTag = (err: NodeJS.ErrnoException): PlatformError.SystemErrorTag => { + switch (err.code) { + case "ENOENT": + return "NotFound" + case "EACCES": + return "PermissionDenied" + case "EEXIST": + return "AlreadyExists" + case "EISDIR": + return "BadResource" + case "ENOTDIR": + return "BadResource" + case "EBUSY": + return "Busy" + case "ELOOP": + return "BadResource" + default: + return "Unknown" + } +} + +const flatten = (command: ChildProcess.Command) => { + const commands: Array = [] + const opts: Array = [] + + const walk = (cmd: ChildProcess.Command): void => { + switch (cmd._tag) { + case "StandardCommand": + commands.push(cmd) + return + case "PipedCommand": + walk(cmd.left) + opts.push(cmd.options) + walk(cmd.right) + return + } + } + + walk(command) + if (commands.length === 0) throw new Error("flatten produced empty commands array") + const [head, ...tail] = commands + return { + commands: [head, ...tail] as Arr.NonEmptyReadonlyArray, + opts, + } +} + +const toPlatformError = ( + method: string, + err: NodeJS.ErrnoException, + command: ChildProcess.Command, +): PlatformError.PlatformError => { + const cmd = flatten(command) + .commands.map((x) => `${x.command} ${x.args.join(" ")}`) + .join(" | ") + return PlatformError.systemError({ + _tag: toTag(err), + module: "ChildProcess", + method, + pathOrDescriptor: cmd, + syscall: err.syscall, + cause: err, + }) +} + +type ExitSignal = Deferred.Deferred + +export const make = Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + + const cwd = Effect.fnUntraced(function* (opts: ChildProcess.CommandOptions) { + if (Predicate.isUndefined(opts.cwd)) return undefined + yield* fs.access(opts.cwd) + return path.resolve(opts.cwd) + }) + + const env = (opts: ChildProcess.CommandOptions) => + opts.extendEnv ? { ...globalThis.process.env, ...opts.env } : opts.env + + const input = (x: ChildProcess.CommandInput | undefined): NodeChildProcess.IOType | undefined => + Stream.isStream(x) ? "pipe" : x + + const output = (x: ChildProcess.CommandOutput | undefined): NodeChildProcess.IOType | undefined => + Sink.isSink(x) ? "pipe" : x + + const stdin = (opts: ChildProcess.CommandOptions): ChildProcess.StdinConfig => { + const cfg: ChildProcess.StdinConfig = { stream: "pipe", encoding: "utf-8", endOnDone: true } + if (Predicate.isUndefined(opts.stdin)) return cfg + if (typeof opts.stdin === "string") return { ...cfg, stream: opts.stdin } + if (Stream.isStream(opts.stdin)) return { ...cfg, stream: opts.stdin } + return { + stream: opts.stdin.stream, + encoding: opts.stdin.encoding ?? cfg.encoding, + endOnDone: opts.stdin.endOnDone ?? cfg.endOnDone, + } + } + + const stdio = (opts: ChildProcess.CommandOptions, key: "stdout" | "stderr"): ChildProcess.StdoutConfig => { + const cfg = opts[key] + if (Predicate.isUndefined(cfg)) return { stream: "pipe" } + if (typeof cfg === "string") return { stream: cfg } + if (Sink.isSink(cfg)) return { stream: cfg } + return { stream: cfg.stream } + } + + const fds = (opts: ChildProcess.CommandOptions) => { + if (Predicate.isUndefined(opts.additionalFds)) return [] + return Object.entries(opts.additionalFds) + .flatMap(([name, config]) => { + const fd = ChildProcess.parseFdName(name) + return Predicate.isUndefined(fd) ? [] : [{ fd, config }] + }) + .toSorted((a, b) => a.fd - b.fd) + } + + const stdios = ( + sin: ChildProcess.StdinConfig, + sout: ChildProcess.StdoutConfig, + serr: ChildProcess.StderrConfig, + extra: ReadonlyArray<{ fd: number; config: ChildProcess.AdditionalFdConfig }>, + ): NodeChildProcess.StdioOptions => { + const pipe = (x: NodeChildProcess.IOType | undefined) => + process.platform === "win32" && x === "pipe" ? "overlapped" : x + const arr: Array = [ + pipe(input(sin.stream)), + pipe(output(sout.stream)), + pipe(output(serr.stream)), + ] + if (extra.length === 0) return arr as NodeChildProcess.StdioOptions + const max = extra.reduce((acc, x) => Math.max(acc, x.fd), 2) + for (let i = 3; i <= max; i++) arr[i] = "ignore" + for (const x of extra) arr[x.fd] = pipe("pipe") + return arr as NodeChildProcess.StdioOptions + } + + const setupFds = Effect.fnUntraced(function* ( + command: ChildProcess.StandardCommand, + proc: NodeChildProcess.ChildProcess, + extra: ReadonlyArray<{ fd: number; config: ChildProcess.AdditionalFdConfig }>, + ) { + if (extra.length === 0) { + return { + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + } + } + + const ins = new Map>() + const outs = new Map>() + + for (const x of extra) { + const node = proc.stdio[x.fd] + switch (x.config.type) { + case "input": { + let sink: Sink.Sink = Sink.drain + if (node && "write" in node) { + sink = NodeSink.fromWritable({ + evaluate: () => node, + onError: (err) => toPlatformError(`fromWritable(fd${x.fd})`, toError(err), command), + endOnDone: true, + }) + } + if (x.config.stream) yield* Effect.forkScoped(Stream.run(x.config.stream, sink)) + ins.set(x.fd, sink) + break + } + case "output": { + let stream: Stream.Stream = Stream.empty + if (node && "read" in node) { + const tap = new PassThrough() + node.on("error", (err) => tap.destroy(toError(err))) + node.pipe(tap) + stream = NodeStream.fromReadable({ + evaluate: () => tap, + onError: (err) => toPlatformError(`fromReadable(fd${x.fd})`, toError(err), command), + }) + } + if (x.config.sink) stream = Stream.transduce(stream, x.config.sink) + outs.set(x.fd, stream) + break + } + } + } + + return { + getInputFd: (fd: number) => ins.get(fd) ?? Sink.drain, + getOutputFd: (fd: number) => outs.get(fd) ?? Stream.empty, + } + }) + + const setupStdin = ( + command: ChildProcess.StandardCommand, + proc: NodeChildProcess.ChildProcess, + cfg: ChildProcess.StdinConfig, + ) => + Effect.suspend(() => { + let sink: Sink.Sink = Sink.drain + if (Predicate.isNotNull(proc.stdin)) { + sink = NodeSink.fromWritable({ + evaluate: () => proc.stdin!, + onError: (err) => toPlatformError("fromWritable(stdin)", toError(err), command), + endOnDone: cfg.endOnDone, + encoding: cfg.encoding, + }) + } + if (Stream.isStream(cfg.stream)) return Effect.as(Effect.forkScoped(Stream.run(cfg.stream, sink)), sink) + return Effect.succeed(sink) + }) + + const setupOutput = ( + command: ChildProcess.StandardCommand, + proc: NodeChildProcess.ChildProcess, + out: ChildProcess.StdoutConfig, + err: ChildProcess.StderrConfig, + ) => { + let stdout = proc.stdout + ? NodeStream.fromReadable({ + evaluate: () => proc.stdout!, + onError: (cause) => toPlatformError("fromReadable(stdout)", toError(cause), command), + }) + : Stream.empty + let stderr = proc.stderr + ? NodeStream.fromReadable({ + evaluate: () => proc.stderr!, + onError: (cause) => toPlatformError("fromReadable(stderr)", toError(cause), command), + }) + : Stream.empty + + if (Sink.isSink(out.stream)) stdout = Stream.transduce(stdout, out.stream) + if (Sink.isSink(err.stream)) stderr = Stream.transduce(stderr, err.stream) + + return { stdout, stderr, all: Stream.merge(stdout, stderr) } + } + + const spawn = (command: ChildProcess.StandardCommand, opts: NodeChildProcess.SpawnOptions) => + Effect.callback((resume) => { + const signal = Deferred.makeUnsafe() + const proc = launch(command.command, command.args, opts) + let end = false + let exit: readonly [code: number | null, signal: NodeJS.Signals | null] | undefined + proc.on("error", (err) => { + resume(Effect.fail(toPlatformError("spawn", err, command))) + }) + proc.on("exit", (...args) => { + exit = args + }) + proc.on("close", (...args) => { + if (end) return + end = true + Deferred.doneUnsafe(signal, Exit.succeed(exit ?? args)) + }) + proc.on("spawn", () => { + resume(Effect.succeed([proc, signal])) + }) + return Effect.sync(() => { + proc.kill("SIGTERM") + }) + }) + + const killGroup = ( + command: ChildProcess.StandardCommand, + proc: NodeChildProcess.ChildProcess, + signal: NodeJS.Signals, + ) => { + if (globalThis.process.platform === "win32") { + return Effect.callback((resume) => { + NodeChildProcess.exec(`taskkill /pid ${proc.pid} /T /F`, { windowsHide: true }, (err) => { + if (err) return resume(Effect.fail(toPlatformError("kill", toError(err), command))) + resume(Effect.void) + }) + }) + } + + return Effect.try({ + try: () => { + globalThis.process.kill(-proc.pid!, signal) + }, + catch: (err) => toPlatformError("kill", toError(err), command), + }) + } + + const killOne = ( + command: ChildProcess.StandardCommand, + proc: NodeChildProcess.ChildProcess, + signal: NodeJS.Signals, + ) => + Effect.suspend(() => { + if (proc.kill(signal)) return Effect.void + return Effect.fail(toPlatformError("kill", new Error("Failed to kill child process"), command)) + }) + + const timeout = + ( + proc: NodeChildProcess.ChildProcess, + command: ChildProcess.StandardCommand, + opts: ChildProcess.KillOptions | undefined, + ) => + ( + f: ( + command: ChildProcess.StandardCommand, + proc: NodeChildProcess.ChildProcess, + signal: NodeJS.Signals, + ) => Effect.Effect, + ) => { + const signal = opts?.killSignal ?? "SIGTERM" + if (Predicate.isUndefined(opts?.forceKillAfter)) return f(command, proc, signal) + return Effect.timeoutOrElse(f(command, proc, signal), { + duration: opts.forceKillAfter, + orElse: () => f(command, proc, "SIGKILL"), + }) + } + + const source = (handle: ChildProcessHandle, from: ChildProcess.PipeFromOption | undefined) => { + const opt = from ?? "stdout" + switch (opt) { + case "stdout": + return handle.stdout + case "stderr": + return handle.stderr + case "all": + return handle.all + default: { + const fd = ChildProcess.parseFdName(opt) + return Predicate.isNotUndefined(fd) ? handle.getOutputFd(fd) : handle.stdout + } + } + } + + const spawnCommand: ( + command: ChildProcess.Command, + ) => Effect.Effect = Effect.fnUntraced( + function* (command) { + switch (command._tag) { + case "StandardCommand": { + const sin = stdin(command.options) + const sout = stdio(command.options, "stdout") + const serr = stdio(command.options, "stderr") + const extra = fds(command.options) + const dir = yield* cwd(command.options) + + const [proc, signal] = yield* Effect.acquireRelease( + spawn(command, { + cwd: dir, + env: env(command.options), + stdio: stdios(sin, sout, serr, extra), + detached: command.options.detached ?? process.platform !== "win32", + shell: command.options.shell, + windowsHide: process.platform === "win32", + }), + Effect.fnUntraced(function* ([proc, signal]) { + const done = yield* Deferred.isDone(signal) + const kill = timeout(proc, command, command.options) + if (done) { + const [code] = yield* Deferred.await(signal) + if (process.platform === "win32") return yield* Effect.void + if (code !== 0 && Predicate.isNotNull(code)) return yield* Effect.ignore(kill(killGroup)) + return yield* Effect.void + } + const send = (s: NodeJS.Signals) => + Effect.catch(killGroup(command, proc, s), () => killOne(command, proc, s)) + const sig = command.options.killSignal ?? "SIGTERM" + const attempt = send(sig).pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid) + const escalated = command.options.forceKillAfter + ? Effect.timeoutOrElse(attempt, { + duration: command.options.forceKillAfter, + orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid), + }) + : attempt + return yield* Effect.ignore(escalated) + }), + ) + + const fd = yield* setupFds(command, proc, extra) + const out = setupOutput(command, proc, sout, serr) + let ref = true + return makeHandle({ + pid: ProcessId(proc.pid!), + stdin: yield* setupStdin(command, proc, sin), + stdout: out.stdout, + stderr: out.stderr, + all: out.all, + getInputFd: fd.getInputFd, + getOutputFd: fd.getOutputFd, + isRunning: Effect.map(Deferred.isDone(signal), (done) => !done), + exitCode: Effect.flatMap(Deferred.await(signal), ([code, signal]) => { + if (Predicate.isNotNull(code)) return Effect.succeed(ExitCode(code)) + return Effect.fail( + toPlatformError( + "exitCode", + new Error(`Process interrupted due to receipt of signal: '${signal}'`), + command, + ), + ) + }), + kill: (opts?: ChildProcess.KillOptions) => { + const sig = opts?.killSignal ?? "SIGTERM" + const send = (s: NodeJS.Signals) => + Effect.catch(killGroup(command, proc, s), () => killOne(command, proc, s)) + const attempt = send(sig).pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid) + if (!opts?.forceKillAfter) return attempt + return Effect.timeoutOrElse(attempt, { + duration: opts.forceKillAfter, + orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid), + }) + }, + unref: Effect.sync(() => { + if (ref) { + proc.unref() + ref = false + } + return Effect.sync(() => { + if (!ref) { + proc.ref() + ref = true + } + }) + }), + }) + } + case "PipedCommand": { + const flat = flatten(command) + const [head, ...tail] = flat.commands + let handle = spawnCommand(head) + for (let i = 0; i < tail.length; i++) { + const next = tail[i] + const opts = flat.opts[i] ?? {} + const sin = stdin(next.options) + const stream = Stream.unwrap(Effect.map(handle, (x) => source(x, opts.from))) + const to = opts.to ?? "stdin" + if (to === "stdin") { + handle = spawnCommand( + ChildProcess.make(next.command, next.args, { + ...next.options, + stdin: { ...sin, stream }, + }), + ) + continue + } + const fd = ChildProcess.parseFdName(to) + if (Predicate.isUndefined(fd)) { + handle = spawnCommand( + ChildProcess.make(next.command, next.args, { + ...next.options, + stdin: { ...sin, stream }, + }), + ) + continue + } + handle = spawnCommand( + ChildProcess.make(next.command, next.args, { + ...next.options, + additionalFds: { + ...next.options.additionalFds, + [ChildProcess.fdName(fd) as `fd${number}`]: { type: "input", stream }, + }, + }), + ) + } + return yield* handle + } + } + }, + ) + + return makeSpawner(spawnCommand) +}) + +const layer: Layer.Layer = Layer.effect( + ChildProcessSpawner, + make, +) + +export const node = makeGlobalNode({ service: ChildProcessSpawner, layer, deps: [filesystem, path] }) + +export * as CrossSpawnSpawner from "./cross-spawn-spawner" diff --git a/packages/core/src/data-migration.sql.ts b/packages/core/src/data-migration.sql.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba446b501cecc7ba2ee68e28d60c0652fc10c402 --- /dev/null +++ b/packages/core/src/data-migration.sql.ts @@ -0,0 +1,6 @@ +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" + +export const DataMigrationTable = sqliteTable("data_migration", { + name: text().primaryKey(), + time_completed: integer().notNull(), +}) diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts new file mode 100644 index 0000000000000000000000000000000000000000..d61adf047eacf13ab8af01a4726cb187faebaeb0 --- /dev/null +++ b/packages/core/src/database/database.ts @@ -0,0 +1,57 @@ +export * as Database from "./database" + +import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import { layer as sqliteLayer } from "#sqlite" +import { Context, Effect, Layer } from "effect" +import { Global } from "../global" +import { Flag } from "../flag/flag" +import { isAbsolute, join } from "path" +import { DatabaseMigration } from "./migration" +import { InstallationChannel } from "../installation/version" +import { makeGlobalNode } from "../effect/app-node" + +const makeDatabase = EffectDrizzleSqlite.makeWithDefaults() +type DatabaseShape = Effect.Success + +export interface Interface { + db: DatabaseShape +} + +export class Service extends Context.Service()("@opencode/v2/storage/Database") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const db = yield* makeDatabase + + yield* db.run("PRAGMA journal_mode = WAL") + yield* db.run("PRAGMA synchronous = NORMAL") + yield* db.run("PRAGMA busy_timeout = 5000") + yield* db.run("PRAGMA cache_size = -64000") + yield* db.run("PRAGMA foreign_keys = ON") + yield* db.run("PRAGMA wal_checkpoint(PASSIVE)") + yield* DatabaseMigration.apply(db) + + return { db } + }).pipe(Effect.orDie), +) + +export function layerFromPath(filename: string) { + return layer.pipe(Layer.provide(sqliteLayer({ filename }))) +} + +export function path() { + if (Flag.OPENCODE_DB) { + if (Flag.OPENCODE_DB === ":memory:" || isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB + return join(Global.Path.data, Flag.OPENCODE_DB) + } + if ( + ["latest", "beta", "prod"].includes(InstallationChannel) || + process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" || + process.env.OPENCODE_DISABLE_CHANNEL_DB === "true" + ) + return join(Global.Path.data, "opencode.db") + return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`) +} + +export const node = makeGlobalNode({ service: Service, layer: layerFromPath(path()), deps: [] }) diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts new file mode 100644 index 0000000000000000000000000000000000000000..e6ea4eaa1477e80397c8cc5c6808f2ac1214891a --- /dev/null +++ b/packages/core/src/database/migration.gen.ts @@ -0,0 +1,44 @@ +import type { DatabaseMigration } from "./migration" + +export const migrations = ( + await Promise.all([ + import("./migration/20260127222353_familiar_lady_ursula"), + import("./migration/20260211171708_add_project_commands"), + import("./migration/20260213144116_wakeful_the_professor"), + import("./migration/20260225215848_workspace"), + import("./migration/20260227213759_add_session_workspace_id"), + import("./migration/20260228203230_blue_harpoon"), + import("./migration/20260303231226_add_workspace_fields"), + import("./migration/20260309230000_move_org_to_state"), + import("./migration/20260312043431_session_message_cursor"), + import("./migration/20260323234822_events"), + import("./migration/20260410174513_workspace-name"), + import("./migration/20260413175956_chief_energizer"), + import("./migration/20260423070820_add_icon_url_override"), + import("./migration/20260427172553_slow_nightmare"), + import("./migration/20260428004200_add_session_path"), + import("./migration/20260501142318_next_venus"), + import("./migration/20260504145000_add_sync_owner"), + import("./migration/20260507164347_add_workspace_time"), + import("./migration/20260510033149_session_usage"), + import("./migration/20260511000411_data_migration_state"), + import("./migration/20260511173437_session-metadata"), + import("./migration/20260601010001_normalize_storage_paths"), + import("./migration/20260601202201_amazing_prowler"), + import("./migration/20260602002951_lowly_union_jack"), + import("./migration/20260602182828_add_project_directories"), + import("./migration/20260603001617_session_message_projection_indexes"), + import("./migration/20260603040000_session_message_projection_order"), + import("./migration/20260603141458_session_input_inbox"), + import("./migration/20260603160727_jittery_ezekiel_stane"), + import("./migration/20260604172448_event_sourced_session_input"), + import("./migration/20260605003541_add_session_context_snapshot"), + import("./migration/20260605042240_add_context_epoch_agent"), + import("./migration/20260611035744_credential"), + import("./migration/20260611192811_lush_chimera"), + import("./migration/20260612174303_project_dir_strategy"), + import("./migration/20260622142730_simplify_session_context_epoch"), + import("./migration/20260622170816_reset_v2_session_state"), + import("./migration/20260622202450_simplify_session_input"), + ]) +).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts new file mode 100644 index 0000000000000000000000000000000000000000..644b22ab7a26f4ddf9aa796bdb5dbb3a8d3022d1 --- /dev/null +++ b/packages/core/src/database/migration.ts @@ -0,0 +1,108 @@ +export * as DatabaseMigration from "./migration" + +import { sql } from "drizzle-orm" +import { Effect, Semaphore } from "effect" +import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" +import { migrations } from "./migration.gen" +import schema from "./schema.gen" + +type Database = EffectDrizzleSqlite.EffectSQLiteDatabase +type Transaction = Parameters[0]>[0] +const lock = Semaphore.makeUnsafe(1) + +export type Migration = { + id: string + up: (tx: Transaction) => Effect.Effect +} + +export function apply(db: Database) { + return lock.withPermit( + Effect.gen(function* () { + const tables = yield* db.all<{ name: string }>( + sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`, + ) + if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations) + if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table") + yield* db.transaction((tx) => + Effect.gen(function* () { + yield* schema.up(tx) + yield* tx.run( + sql`CREATE TABLE ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`, + ) + yield* Effect.forEach(migrations, (migration) => + tx.run( + sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`, + ), + ) + }), + ) + }), + ) +} + +export function applyOnly(db: Database, input: Migration[]) { + return Effect.gen(function* () { + yield* db.run( + sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`, + ) + let completed = new Set( + (yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id), + ) + if (completed.size === 0) { + // Existing installs used Drizzle's migration journal. Seed the new + // journal once so TypeScript migrations don't replay old SQL. + if ( + yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`) + ) { + const named = (yield* db.all<{ name: string }>( + sql`SELECT name FROM pragma_table_info('__drizzle_migrations')`, + )).some((column) => column.name === "name") + + if (named) { + yield* db.run(sql` + INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed) + SELECT name, ${Date.now()} + FROM ${sql.identifier("__drizzle_migrations")} + WHERE name IS NOT NULL + `) + } + + if (!named) { + const entries = yield* db.all<{ created_at: number; prefix: string | null }>(sql` + SELECT created_at, strftime('%Y%m%d%H%M%S', created_at / 1000, 'unixepoch') AS prefix + FROM ${sql.identifier("__drizzle_migrations")} + WHERE created_at IS NOT NULL + `) + + for (const entry of entries) { + const migration = input.find((item) => item.id.startsWith(`${entry.prefix}_`)) + if (!migration) { + return yield* Effect.die( + new Error(`Legacy migration timestamp ${entry.created_at} does not match any known migration`), + ) + } + yield* db.run(sql` + INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed) + VALUES (${migration.id}, ${Date.now()}) + `) + } + } + completed = new Set( + (yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id), + ) + } + } + + for (const migration of input) { + if (completed.has(migration.id)) continue + yield* db.transaction((tx) => + Effect.gen(function* () { + yield* migration.up(tx) + yield* tx.run( + sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`, + ) + }), + ) + } + }) +} diff --git a/packages/core/src/database/migration/20260127222353_familiar_lady_ursula.ts b/packages/core/src/database/migration/20260127222353_familiar_lady_ursula.ts new file mode 100644 index 0000000000000000000000000000000000000000..468a7103fb3d6ad1671e5484b70b10da47ec4ebb --- /dev/null +++ b/packages/core/src/database/migration/20260127222353_familiar_lady_ursula.ts @@ -0,0 +1,107 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260127222353_familiar_lady_ursula", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`project\` ( + \`id\` text PRIMARY KEY, + \`worktree\` text NOT NULL, + \`vcs\` text, + \`name\` text, + \`icon_url\` text, + \`icon_color\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_initialized\` integer, + \`sandboxes\` text NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`part\` ( + \`id\` text PRIMARY KEY, + \`message_id\` text NOT NULL, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`permission\` ( + \`project_id\` text PRIMARY KEY, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`parent_id\` text, + \`slug\` text NOT NULL, + \`directory\` text NOT NULL, + \`title\` text NOT NULL, + \`version\` text NOT NULL, + \`share_url\` text, + \`summary_additions\` integer, + \`summary_deletions\` integer, + \`summary_files\` integer, + \`summary_diffs\` text, + \`revert\` text, + \`permission\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_compacting\` integer, + \`time_archived\` integer, + CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`todo\` ( + \`session_id\` text NOT NULL, + \`content\` text NOT NULL, + \`status\` text NOT NULL, + \`priority\` text NOT NULL, + \`position\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`), + CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_share\` ( + \`session_id\` text PRIMARY KEY, + \`id\` text NOT NULL, + \`secret\` text NOT NULL, + \`url\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`CREATE INDEX \`message_session_idx\` ON \`message\` (\`session_id\`);`) + yield* tx.run(`CREATE INDEX \`part_message_idx\` ON \`part\` (\`message_id\`);`) + yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`) + yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) + yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`) + yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260211171708_add_project_commands.ts b/packages/core/src/database/migration/20260211171708_add_project_commands.ts new file mode 100644 index 0000000000000000000000000000000000000000..d31a533db3c31ec362bc8afcb38c1be0e7aafc60 --- /dev/null +++ b/packages/core/src/database/migration/20260211171708_add_project_commands.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260211171708_add_project_commands", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`project\` ADD \`commands\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260213144116_wakeful_the_professor.ts b/packages/core/src/database/migration/20260213144116_wakeful_the_professor.ts new file mode 100644 index 0000000000000000000000000000000000000000..8077182d939805054e3b29842bbff1c30a72b313 --- /dev/null +++ b/packages/core/src/database/migration/20260213144116_wakeful_the_professor.ts @@ -0,0 +1,23 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260213144116_wakeful_the_professor", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`control_account\` ( + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`active\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`) + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260225215848_workspace.ts b/packages/core/src/database/migration/20260225215848_workspace.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc816951ef972ea5a0e529469ee81091197daf13 --- /dev/null +++ b/packages/core/src/database/migration/20260225215848_workspace.ts @@ -0,0 +1,19 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260225215848_workspace", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`workspace\` ( + \`id\` text PRIMARY KEY, + \`branch\` text, + \`project_id\` text NOT NULL, + \`config\` text NOT NULL, + CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260227213759_add_session_workspace_id.ts b/packages/core/src/database/migration/20260227213759_add_session_workspace_id.ts new file mode 100644 index 0000000000000000000000000000000000000000..430407156dfdb02dca36316823c85894311a15e0 --- /dev/null +++ b/packages/core/src/database/migration/20260227213759_add_session_workspace_id.ts @@ -0,0 +1,12 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260227213759_add_session_workspace_id", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session\` ADD \`workspace_id\` text;`) + yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260228203230_blue_harpoon.ts b/packages/core/src/database/migration/20260228203230_blue_harpoon.ts new file mode 100644 index 0000000000000000000000000000000000000000..83e2978f707a9a526f11024faf10a97c9be64af6 --- /dev/null +++ b/packages/core/src/database/migration/20260228203230_blue_harpoon.ts @@ -0,0 +1,30 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260228203230_blue_harpoon", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`account\` ( + \`id\` text PRIMARY KEY, + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`selected_org_id\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account_state\` ( + \`id\` integer PRIMARY KEY NOT NULL, + \`active_account_id\` text, + FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON UPDATE no action ON DELETE set null + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260303231226_add_workspace_fields.ts b/packages/core/src/database/migration/20260303231226_add_workspace_fields.ts new file mode 100644 index 0000000000000000000000000000000000000000..380e9cc68bf9581ecd62947245684649417ac70e --- /dev/null +++ b/packages/core/src/database/migration/20260303231226_add_workspace_fields.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260303231226_add_workspace_fields", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`type\` text NOT NULL;`) + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`name\` text;`) + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`directory\` text;`) + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`extra\` text;`) + yield* tx.run(`ALTER TABLE \`workspace\` DROP COLUMN \`config\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260309230000_move_org_to_state.ts b/packages/core/src/database/migration/20260309230000_move_org_to_state.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf39f3e5bf683e6e5662614578510341fa1eec3d --- /dev/null +++ b/packages/core/src/database/migration/20260309230000_move_org_to_state.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260309230000_move_org_to_state", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`account_state\` ADD \`active_org_id\` text;`) + yield* tx.run( + `UPDATE \`account_state\` SET \`active_org_id\` = (SELECT \`selected_org_id\` FROM \`account\` WHERE \`account\`.\`id\` = \`account_state\`.\`active_account_id\`);`, + ) + yield* tx.run(`ALTER TABLE \`account\` DROP COLUMN \`selected_org_id\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260312043431_session_message_cursor.ts b/packages/core/src/database/migration/20260312043431_session_message_cursor.ts new file mode 100644 index 0000000000000000000000000000000000000000..1603c3fa739e54f06600ae8af34b14fa273cf218 --- /dev/null +++ b/packages/core/src/database/migration/20260312043431_session_message_cursor.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260312043431_session_message_cursor", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`message_session_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`part_message_idx\`;`) + yield* tx.run( + `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260323234822_events.ts b/packages/core/src/database/migration/20260323234822_events.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b1996fbacc84659d5af0f6daf2938777d881a57 --- /dev/null +++ b/packages/core/src/database/migration/20260323234822_events.ts @@ -0,0 +1,26 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260323234822_events", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`event_sequence\` ( + \`aggregate_id\` text PRIMARY KEY, + \`seq\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`event\` ( + \`id\` text PRIMARY KEY, + \`aggregate_id\` text NOT NULL, + \`seq\` integer NOT NULL, + \`type\` text NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260410174513_workspace-name.ts b/packages/core/src/database/migration/20260410174513_workspace-name.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a8557ec7aa14914ed17d3968199e5dd811981fe --- /dev/null +++ b/packages/core/src/database/migration/20260410174513_workspace-name.ts @@ -0,0 +1,32 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260410174513_workspace-name", + up(tx) { + return Effect.gen(function* () { + const columns = yield* tx.all<{ name: string }>(`PRAGMA table_info(\`workspace\`)`) + const name = columns.some((column) => column.name === "name") ? "`name`" : "''" + + yield* tx.run(`PRAGMA foreign_keys=OFF;`) + yield* tx.run(` + CREATE TABLE \`__new_workspace\` ( + \`id\` text PRIMARY KEY, + \`type\` text NOT NULL, + \`name\` text DEFAULT '' NOT NULL, + \`branch\` text, + \`directory\` text, + \`extra\` text, + \`project_id\` text NOT NULL, + CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, ${name}, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`, + ) + yield* tx.run(`DROP TABLE \`workspace\`;`) + yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`) + yield* tx.run(`PRAGMA foreign_keys=ON;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260413175956_chief_energizer.ts b/packages/core/src/database/migration/20260413175956_chief_energizer.ts new file mode 100644 index 0000000000000000000000000000000000000000..a03477e09e38a91288cc062d192dc6304cd285a8 --- /dev/null +++ b/packages/core/src/database/migration/20260413175956_chief_energizer.ts @@ -0,0 +1,24 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260413175956_chief_energizer", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_entry\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_entry_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`CREATE INDEX \`session_entry_session_idx\` ON \`session_entry\` (\`session_id\`);`) + yield* tx.run(`CREATE INDEX \`session_entry_session_type_idx\` ON \`session_entry\` (\`session_id\`,\`type\`);`) + yield* tx.run(`CREATE INDEX \`session_entry_time_created_idx\` ON \`session_entry\` (\`time_created\`);`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260423070820_add_icon_url_override.ts b/packages/core/src/database/migration/20260423070820_add_icon_url_override.ts new file mode 100644 index 0000000000000000000000000000000000000000..20b1f9163a4141beb37c3189620d18b9bf0d1cbe --- /dev/null +++ b/packages/core/src/database/migration/20260423070820_add_icon_url_override.ts @@ -0,0 +1,14 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260423070820_add_icon_url_override", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + ALTER TABLE \`project\` ADD \`icon_url_override\` text; + UPDATE \`project\` SET \`icon_url_override\` = \`icon_url\` WHERE \`icon_url\` IS NOT NULL; + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260427172553_slow_nightmare.ts b/packages/core/src/database/migration/20260427172553_slow_nightmare.ts new file mode 100644 index 0000000000000000000000000000000000000000..32e67decf3a76b0d846007f031ba26ab8c6d53fa --- /dev/null +++ b/packages/core/src/database/migration/20260427172553_slow_nightmare.ts @@ -0,0 +1,30 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260427172553_slow_nightmare", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_session_type_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_entry_time_created_idx\`;`) + yield* tx.run(`CREATE INDEX \`session_message_session_idx\` ON \`session_message\` (\`session_id\`);`) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_idx\` ON \`session_message\` (\`session_id\`,\`type\`);`, + ) + yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) + yield* tx.run(`DROP TABLE \`session_entry\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260428004200_add_session_path.ts b/packages/core/src/database/migration/20260428004200_add_session_path.ts new file mode 100644 index 0000000000000000000000000000000000000000..a60ef377fc2beee0ab01f5bf985f1972c73f019a --- /dev/null +++ b/packages/core/src/database/migration/20260428004200_add_session_path.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260428004200_add_session_path", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session\` ADD \`path\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260501142318_next_venus.ts b/packages/core/src/database/migration/20260501142318_next_venus.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c5b078f8fa8db1d79189724ac1f40c38e73e467 --- /dev/null +++ b/packages/core/src/database/migration/20260501142318_next_venus.ts @@ -0,0 +1,12 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260501142318_next_venus", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session\` ADD \`agent\` text;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`model\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260504145000_add_sync_owner.ts b/packages/core/src/database/migration/20260504145000_add_sync_owner.ts new file mode 100644 index 0000000000000000000000000000000000000000..33e8554914528b297dac4708086926e47e806173 --- /dev/null +++ b/packages/core/src/database/migration/20260504145000_add_sync_owner.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260504145000_add_sync_owner", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`event_sequence\` ADD \`owner_id\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260507164347_add_workspace_time.ts b/packages/core/src/database/migration/20260507164347_add_workspace_time.ts new file mode 100644 index 0000000000000000000000000000000000000000..df7e90fc931341fc306b8c79bb77e2d88722f005 --- /dev/null +++ b/packages/core/src/database/migration/20260507164347_add_workspace_time.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260507164347_add_workspace_time", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`workspace\` ADD \`time_used\` integer NOT NULL DEFAULT 0;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260510033149_session_usage.ts b/packages/core/src/database/migration/20260510033149_session_usage.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dcd1f658e76f340d4fc4cf665950d2aa4309af8 --- /dev/null +++ b/packages/core/src/database/migration/20260510033149_session_usage.ts @@ -0,0 +1,56 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260510033149_session_usage", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session\` ADD \`cost\` real DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_input\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_output\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_reasoning\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_cache_read\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(`ALTER TABLE \`session\` ADD \`tokens_cache_write\` integer DEFAULT 0 NOT NULL;`) + yield* tx.run(` + UPDATE session + SET + cost = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.cost'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_input = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.input'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_output = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.output'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_reasoning = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.reasoning'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_cache_read = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.cache.read'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0), + tokens_cache_write = coalesce(( + SELECT sum(coalesce(json_extract(message.data, '$.tokens.cache.write'), 0)) + FROM message + WHERE message.session_id = session.id + AND json_extract(message.data, '$.role') = 'assistant' + ), 0) + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260511000411_data_migration_state.ts b/packages/core/src/database/migration/20260511000411_data_migration_state.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ff0b6618911d19495230c98040505d0680e7f47 --- /dev/null +++ b/packages/core/src/database/migration/20260511000411_data_migration_state.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260511000411_data_migration_state", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`data_migration\` ( + \`name\` text PRIMARY KEY, + \`time_completed\` integer NOT NULL + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260511173437_session-metadata.ts b/packages/core/src/database/migration/20260511173437_session-metadata.ts new file mode 100644 index 0000000000000000000000000000000000000000..413f086671d3495137a8671ddbb943c4735a24ba --- /dev/null +++ b/packages/core/src/database/migration/20260511173437_session-metadata.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260511173437_session-metadata", + up(tx) { + return Effect.gen(function* () { + // This column briefly shipped again under 20260530232709_lovely_romulus. + if ( + (yield* tx.all<{ name: string }>(`PRAGMA table_info(\`session\`)`)).some((column) => column.name === "metadata") + ) + return + yield* tx.run(`ALTER TABLE \`session\` ADD \`metadata\` text;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260601010001_normalize_storage_paths.ts b/packages/core/src/database/migration/20260601010001_normalize_storage_paths.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3764e6aa6c482f0d5710916a7d2c42acbdb901f --- /dev/null +++ b/packages/core/src/database/migration/20260601010001_normalize_storage_paths.ts @@ -0,0 +1,22 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260601010001_normalize_storage_paths", + up(tx) { + return Effect.gen(function* () { + yield* tx.run( + `UPDATE project SET worktree = REPLACE(worktree, char(92), '/') WHERE worktree GLOB '[A-Za-z]:' || char(92) || '*' OR worktree LIKE char(92) || char(92) || '%';`, + ) + yield* tx.run( + `UPDATE project SET sandboxes = REPLACE(sandboxes, char(92) || char(92), '/') WHERE instr(sandboxes, char(92)) > 0 AND (worktree GLOB '[A-Za-z]:*' OR worktree LIKE '//%');`, + ) + yield* tx.run( + `UPDATE session SET directory = REPLACE(directory, char(92), '/') WHERE directory GLOB '[A-Za-z]:' || char(92) || '*' OR directory LIKE char(92) || char(92) || '%';`, + ) + yield* tx.run( + `UPDATE session SET path = REPLACE(path, char(92), '/') WHERE path IS NOT NULL AND instr(path, char(92)) > 0 AND (directory GLOB '[A-Za-z]:*' OR directory LIKE '//%');`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260601202201_amazing_prowler.ts b/packages/core/src/database/migration/20260601202201_amazing_prowler.ts new file mode 100644 index 0000000000000000000000000000000000000000..84b619d2fc30640a52466d373112ccc1804a3979 --- /dev/null +++ b/packages/core/src/database/migration/20260601202201_amazing_prowler.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260601202201_amazing_prowler", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP TABLE \`permission\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260602002951_lowly_union_jack.ts b/packages/core/src/database/migration/20260602002951_lowly_union_jack.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c75b52acc7ee9f5f129c2b610ea702c9100ad66 --- /dev/null +++ b/packages/core/src/database/migration/20260602002951_lowly_union_jack.ts @@ -0,0 +1,24 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260602002951_lowly_union_jack", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260602182828_add_project_directories.ts b/packages/core/src/database/migration/20260602182828_add_project_directories.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1200fd3640ba4c7aa39727319f84fbd6214610d --- /dev/null +++ b/packages/core/src/database/migration/20260602182828_add_project_directories.ts @@ -0,0 +1,20 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260602182828_add_project_directories", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text NOT NULL, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts b/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts new file mode 100644 index 0000000000000000000000000000000000000000..85b5cd946339610486518ad138157ff8d0bffde9 --- /dev/null +++ b/packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts @@ -0,0 +1,19 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603001617_session_message_projection_indexes", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_idx\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_idx\`;`) + yield* tx.run(`CREATE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run( + `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`time_created\`,\`id\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts b/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f3a43bccedbf0abcbe083c46d38745cfb4f5474 --- /dev/null +++ b/packages/core/src/database/migration/20260603040000_session_message_projection_order.ts @@ -0,0 +1,19 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603040000_session_message_projection_order", + up(tx) { + return Effect.gen(function* () { + // Pre-launch Session projections were written before durable event persistence + // became unconditional, so they cannot be assigned truthful aggregate order. + yield* tx.run(`DELETE FROM \`session_message\`;`) + yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`) + yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603141458_session_input_inbox.ts b/packages/core/src/database/migration/20260603141458_session_input_inbox.ts new file mode 100644 index 0000000000000000000000000000000000000000..329ab15c08b906c66b63030ddfe844b6da5555c4 --- /dev/null +++ b/packages/core/src/database/migration/20260603141458_session_input_inbox.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603141458_session_input_inbox", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_input\` ( + \`seq\` integer PRIMARY KEY AUTOINCREMENT, + \`id\` text NOT NULL UNIQUE, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`seq\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts b/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts new file mode 100644 index 0000000000000000000000000000000000000000..bcfc1afdd685fb0d2db4c79298c0e261f6562c05 --- /dev/null +++ b/packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts @@ -0,0 +1,20 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260603160727_jittery_ezekiel_stane", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`session_input_session_pending_seq_idx\`;`) + yield* tx.run( + `CREATE INDEX IF NOT EXISTS \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX IF NOT EXISTS \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX IF NOT EXISTS \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260604172448_event_sourced_session_input.ts b/packages/core/src/database/migration/20260604172448_event_sourced_session_input.ts new file mode 100644 index 0000000000000000000000000000000000000000..24a31bfae142b0d916c149058004623e3123996b --- /dev/null +++ b/packages/core/src/database/migration/20260604172448_event_sourced_session_input.ts @@ -0,0 +1,47 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260604172448_event_sourced_session_input", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DELETE FROM \`session_input\`;`) + yield* tx.run(`DELETE FROM \`session_message\`;`) + yield* tx.run(`DELETE FROM \`event\`;`) + yield* tx.run(`DELETE FROM \`event_sequence\`;`) + yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL;`) + yield* tx.run(`DELETE FROM \`workspace\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`event_aggregate_seq_idx\`;`) + yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_seq_idx\`;`) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`, + ) + yield* tx.run(`PRAGMA foreign_keys=OFF;`) + yield* tx.run(` + CREATE TABLE \`__new_session_input\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`admitted_seq\` integer NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`DROP TABLE \`session_input\`;`) + yield* tx.run(`ALTER TABLE \`__new_session_input\` RENAME TO \`session_input\`;`) + yield* tx.run(`PRAGMA foreign_keys=ON;`) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260605003541_add_session_context_snapshot.ts b/packages/core/src/database/migration/20260605003541_add_session_context_snapshot.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1648969dda40a7fab603bc585cef2223f995b20 --- /dev/null +++ b/packages/core/src/database/migration/20260605003541_add_session_context_snapshot.ts @@ -0,0 +1,21 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260605003541_add_session_context_snapshot", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_context_epoch\` ( + \`session_id\` text PRIMARY KEY, + \`baseline\` text NOT NULL, + \`snapshot\` text NOT NULL, + \`baseline_seq\` integer NOT NULL, + \`replacement_seq\` integer, + \`revision\` integer DEFAULT 0 NOT NULL, + CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260605042240_add_context_epoch_agent.ts b/packages/core/src/database/migration/20260605042240_add_context_epoch_agent.ts new file mode 100644 index 0000000000000000000000000000000000000000..cefd6ca037a4899c7183a1c65ea148fec8a8120e --- /dev/null +++ b/packages/core/src/database/migration/20260605042240_add_context_epoch_agent.ts @@ -0,0 +1,11 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260605042240_add_context_epoch_agent", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session_context_epoch\` ADD \`agent\` text DEFAULT 'build' NOT NULL;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260611035744_credential.ts b/packages/core/src/database/migration/20260611035744_credential.ts new file mode 100644 index 0000000000000000000000000000000000000000..80df43cdf58b1ab22dedfbebd0d6b55fb366a773 --- /dev/null +++ b/packages/core/src/database/migration/20260611035744_credential.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260611035744_credential", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`connector_id\` text NOT NULL, + \`method_id\` text NOT NULL, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`active\` integer DEFAULT false NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run( + `CREATE UNIQUE INDEX \`credential_connector_active_idx\` ON \`credential\` (\`connector_id\`) WHERE "credential"."active" = 1;`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260611192811_lush_chimera.ts b/packages/core/src/database/migration/20260611192811_lush_chimera.ts new file mode 100644 index 0000000000000000000000000000000000000000..306b10d5327f0d98d9f8a17cd988e0299454386a --- /dev/null +++ b/packages/core/src/database/migration/20260611192811_lush_chimera.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260611192811_lush_chimera", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`credential_connector_active_idx\`;`) + yield* tx.run(`DROP TABLE \`credential\`;`) + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`integration_id\` text, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`connector_id\` text, + \`method_id\` text, + \`active\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts b/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f09c40a707a55b107a1f9b05cfeebb624fd10ce --- /dev/null +++ b/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts @@ -0,0 +1,29 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260612174303_project_dir_strategy", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`project_directory\` ADD \`strategy\` text;`) + yield* tx.run(`PRAGMA foreign_keys=OFF;`) + yield* tx.run(` + CREATE TABLE \`__new_project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text, + \`strategy\` text, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `INSERT INTO \`__new_project_directory\`(\`project_id\`, \`directory\`, \`type\`, \`time_created\`) SELECT \`project_id\`, \`directory\`, \`type\`, \`time_created\` FROM \`project_directory\`;`, + ) + yield* tx.run(`DROP TABLE \`project_directory\`;`) + yield* tx.run(`ALTER TABLE \`__new_project_directory\` RENAME TO \`project_directory\`;`) + yield* tx.run(`PRAGMA foreign_keys=ON;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260622142730_simplify_session_context_epoch.ts b/packages/core/src/database/migration/20260622142730_simplify_session_context_epoch.ts new file mode 100644 index 0000000000000000000000000000000000000000..1520bac4c146f8a788a98bb76b721a1b973f8b4a --- /dev/null +++ b/packages/core/src/database/migration/20260622142730_simplify_session_context_epoch.ts @@ -0,0 +1,13 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260622142730_simplify_session_context_epoch", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`agent\`;`) + yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`replacement_seq\`;`) + yield* tx.run(`ALTER TABLE \`session_context_epoch\` DROP COLUMN \`revision\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260622170816_reset_v2_session_state.ts b/packages/core/src/database/migration/20260622170816_reset_v2_session_state.ts new file mode 100644 index 0000000000000000000000000000000000000000..b771a64bb74527ab997425bd95f648d102bc0c48 --- /dev/null +++ b/packages/core/src/database/migration/20260622170816_reset_v2_session_state.ts @@ -0,0 +1,17 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260622170816_reset_v2_session_state", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DELETE FROM \`session_context_epoch\`;`) + yield* tx.run(`DELETE FROM \`session_input\`;`) + yield* tx.run(`DELETE FROM \`session_message\`;`) + yield* tx.run(`DELETE FROM \`event\`;`) + yield* tx.run(`DELETE FROM \`event_sequence\`;`) + yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL WHERE \`workspace_id\` IS NOT NULL;`) + yield* tx.run(`DELETE FROM \`workspace\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260622202450_simplify_session_input.ts b/packages/core/src/database/migration/20260622202450_simplify_session_input.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b5ddd1bfde312f65b5e0d1d156f761051fbd13f --- /dev/null +++ b/packages/core/src/database/migration/20260622202450_simplify_session_input.ts @@ -0,0 +1,17 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260622202450_simplify_session_input", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DELETE FROM \`session_context_epoch\`;`) + yield* tx.run(`DELETE FROM \`session_input\`;`) + yield* tx.run(`DELETE FROM \`session_message\`;`) + yield* tx.run(`DELETE FROM \`event\`;`) + yield* tx.run(`DELETE FROM \`event_sequence\`;`) + yield* tx.run(`UPDATE \`session\` SET \`workspace_id\` = NULL WHERE \`workspace_id\` IS NOT NULL;`) + yield* tx.run(`DELETE FROM \`workspace\`;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/path.ts b/packages/core/src/database/path.ts new file mode 100644 index 0000000000000000000000000000000000000000..379d5f8aa76d0a13e7345e7f9bf6810a5785bd9c --- /dev/null +++ b/packages/core/src/database/path.ts @@ -0,0 +1,91 @@ +import nodePath from "path" +import { customType } from "drizzle-orm/sqlite-core" +import { AbsolutePath } from "../schema" + +function storagePath(input: string) { + if (process.platform !== "win32") return input + return input.replaceAll("\\", "/") +} + +function isWindowsStoragePath(input: string) { + return /^[A-Za-z]:\//.test(input) || input.startsWith("//") +} + +function absolute(input: string) { + const result = storagePath(input) + if (!nodePath.posix.isAbsolute(result) && !(process.platform === "win32" && isWindowsStoragePath(result))) { + throw new Error(`Path is not absolute: ${input}`) + } + return result +} + +function toPlatform(input: string) { + if (process.platform !== "win32" || !isWindowsStoragePath(input)) return input + return input.replaceAll("/", "\\") +} + +export const absoluteColumn = customType<{ + data: AbsolutePath + driverData: string + driverOutput: string +}>({ + dataType() { + return "text" + }, + toDriver(input) { + return absolute(input) + }, + fromDriver(input) { + return AbsolutePath.make(toPlatform(absolute(input))) + }, +}) + +// Legacy sessions may persist an empty directory. Keep that existing value +// readable while normalizing and validating every real directory. +export const directoryColumn = customType<{ + data: string + driverData: string + driverOutput: string +}>({ + dataType() { + return "text" + }, + toDriver(input) { + return input ? absolute(input) : input + }, + fromDriver(input) { + return input ? toPlatform(absolute(input)) : input + }, +}) + +export const pathColumn = customType<{ + data: string + driverData: string + driverOutput: string +}>({ + dataType() { + return "text" + }, + toDriver(input) { + return storagePath(input) + }, + fromDriver(input) { + return storagePath(input) + }, +}) + +export const absoluteArrayColumn = customType<{ + data: AbsolutePath[] + driverData: string + driverOutput: string +}>({ + dataType() { + return "text" + }, + toDriver(input) { + return JSON.stringify(input.map(absolute)) + }, + fromDriver(input) { + return (JSON.parse(input) as string[]).map((item) => AbsolutePath.make(toPlatform(absolute(item)))) + }, +}) diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed60fde6c55fe4df217bba241ed9e8fa1ba323d5 --- /dev/null +++ b/packages/core/src/database/schema.gen.ts @@ -0,0 +1,274 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "./migration" + +export default { + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`workspace\` ( + \`id\` text PRIMARY KEY, + \`type\` text NOT NULL, + \`name\` text DEFAULT '' NOT NULL, + \`branch\` text, + \`directory\` text, + \`extra\` text, + \`project_id\` text NOT NULL, + \`time_used\` integer NOT NULL, + CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`data_migration\` ( + \`name\` text PRIMARY KEY, + \`time_completed\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account_state\` ( + \`id\` integer PRIMARY KEY, + \`active_account_id\` text, + \`active_org_id\` text, + CONSTRAINT \`fk_account_state_active_account_id_account_id_fk\` FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON DELETE SET NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account\` ( + \`id\` text PRIMARY KEY, + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`control_account\` ( + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`active\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`) + ); + `) + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`integration_id\` text, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`connector_id\` text, + \`method_id\` text, + \`active\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`event_sequence\` ( + \`aggregate_id\` text PRIMARY KEY, + \`seq\` integer NOT NULL, + \`owner_id\` text + ); + `) + yield* tx.run(` + CREATE TABLE \`event\` ( + \`id\` text PRIMARY KEY, + \`aggregate_id\` text NOT NULL, + \`seq\` integer NOT NULL, + \`type\` text NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text, + \`strategy\` text, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`project\` ( + \`id\` text PRIMARY KEY, + \`worktree\` text NOT NULL, + \`vcs\` text, + \`name\` text, + \`icon_url\` text, + \`icon_url_override\` text, + \`icon_color\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_initialized\` integer, + \`sandboxes\` text NOT NULL, + \`commands\` text + ); + `) + yield* tx.run(` + CREATE TABLE \`message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`part\` ( + \`id\` text PRIMARY KEY, + \`message_id\` text NOT NULL, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_context_epoch\` ( + \`session_id\` text PRIMARY KEY, + \`baseline\` text NOT NULL, + \`snapshot\` text NOT NULL, + \`baseline_seq\` integer NOT NULL, + CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_input\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`admitted_seq\` integer NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`seq\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`workspace_id\` text, + \`parent_id\` text, + \`slug\` text NOT NULL, + \`directory\` text NOT NULL, + \`path\` text, + \`title\` text NOT NULL, + \`version\` text NOT NULL, + \`share_url\` text, + \`summary_additions\` integer, + \`summary_deletions\` integer, + \`summary_files\` integer, + \`summary_diffs\` text, + \`metadata\` text, + \`cost\` real DEFAULT 0 NOT NULL, + \`tokens_input\` integer DEFAULT 0 NOT NULL, + \`tokens_output\` integer DEFAULT 0 NOT NULL, + \`tokens_reasoning\` integer DEFAULT 0 NOT NULL, + \`tokens_cache_read\` integer DEFAULT 0 NOT NULL, + \`tokens_cache_write\` integer DEFAULT 0 NOT NULL, + \`revert\` text, + \`permission\` text, + \`agent\` text, + \`model\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_compacting\` integer, + \`time_archived\` integer, + CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`todo\` ( + \`session_id\` text NOT NULL, + \`content\` text NOT NULL, + \`status\` text NOT NULL, + \`priority\` text NOT NULL, + \`position\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`), + CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_share\` ( + \`session_id\` text PRIMARY KEY, + \`id\` text NOT NULL, + \`secret\` text NOT NULL, + \`url\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`) + yield* tx.run( + `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`, + ) + yield* tx.run( + `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`) + yield* tx.run( + `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`, + ) + yield* tx.run( + `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`, + ) + yield* tx.run( + `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, + ) + yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) + yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) + yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) + yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`) + yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) + }) + }, +} satisfies Omit diff --git a/packages/core/src/database/schema.sql.ts b/packages/core/src/database/schema.sql.ts new file mode 100644 index 0000000000000000000000000000000000000000..ead3518dee04e88796e2497061e68a7f13767ab1 --- /dev/null +++ b/packages/core/src/database/schema.sql.ts @@ -0,0 +1,10 @@ +import { integer } from "drizzle-orm/sqlite-core" + +export const Timestamps = { + time_created: integer() + .notNull() + .$default(() => Date.now()), + time_updated: integer() + .notNull() + .$onUpdate(() => Date.now()), +} diff --git a/packages/core/src/database/sqlite.bun.ts b/packages/core/src/database/sqlite.bun.ts new file mode 100644 index 0000000000000000000000000000000000000000..e15f4c117e46172ca34f551466b1a9841db105dc --- /dev/null +++ b/packages/core/src/database/sqlite.bun.ts @@ -0,0 +1,183 @@ +import { Database } from "bun:sqlite" +import { drizzle } from "drizzle-orm/bun-sqlite" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Semaphore from "effect/Semaphore" +import * as Stream from "effect/Stream" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import * as Client from "effect/unstable/sql/SqlClient" +import type { Connection } from "effect/unstable/sql/SqlConnection" +import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import * as Statement from "effect/unstable/sql/Statement" +import { Sqlite } from "./sqlite" + +const ATTR_DB_SYSTEM_NAME = "db.system.name" + +const TypeId = "~@opencode-ai/core/database/SqliteBun" as const +type TypeId = typeof TypeId + +interface SqliteClient extends Client.SqlClient { + readonly [TypeId]: TypeId + readonly config: Config + readonly export: Effect.Effect + readonly loadExtension: (path: string) => Effect.Effect + readonly updateValues: never +} + +interface Config { + readonly filename: string + readonly readonly?: boolean + readonly create?: boolean + readonly readwrite?: boolean + readonly disableWAL?: boolean + readonly spanAttributes?: Record + readonly transformResultNames?: (str: string) => string + readonly transformQueryNames?: (str: string) => string +} + +interface SqliteConnection extends Connection { + readonly export: Effect.Effect + readonly loadExtension: (path: string) => Effect.Effect +} + +const make = (options: Config) => + Effect.gen(function* () { + const native = (yield* Sqlite.Native) as Database + + const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) + const transformRows = options.transformResultNames + ? Statement.defaultTransforms(options.transformResultNames).array + : undefined + + const run = (query: string, params: ReadonlyArray = []) => + Effect.withFiber>, SqlError>((fiber) => { + const statement = native.query(query) + // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 + statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers)) + try { + return Effect.succeed((statement.all(...(params as any)) ?? []) as Array>) + } catch (cause) { + return Effect.fail( + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }), + }), + ) + } + }) + + const runValues = (query: string, params: ReadonlyArray = []) => + Effect.withFiber, SqlError>((fiber) => { + const statement = native.query(query) + // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 + statement.safeIntegers(Context.get(fiber.context, Client.SafeIntegers)) + try { + return Effect.succeed((statement.values(...(params as any)) ?? []) as Array) + } catch (cause) { + return Effect.fail( + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }), + }), + ) + } + }) + + const connection = identity({ + execute(query, params, transformRows) { + return transformRows ? Effect.map(run(query, params), transformRows) : run(query, params) + }, + executeRaw(query, params) { + return run(query, params) + }, + executeValues(query, params) { + return runValues(query, params) + }, + executeUnprepared(query, params, transformRows) { + return this.execute(query, params, transformRows) + }, + executeStream() { + return Stream.die("executeStream not implemented") + }, + export: Effect.try({ + try: () => native.serialize(), + catch: (cause) => + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to export database", operation: "export" }), + }), + }), + loadExtension: (path) => + Effect.try({ + try: () => native.loadExtension(path), + catch: (cause) => + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }), + }), + }), + }) + + const semaphore = yield* Semaphore.make(1) + const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)) + const transactionAcquirer = Effect.uninterruptibleMask((restore) => { + const fiber = Fiber.getCurrent()! + const scope = Context.getUnsafe(fiber.context, Scope.Scope) + return Effect.as( + Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), + connection, + ) + }) + + const client = Object.assign( + (yield* Client.make({ + acquirer, + compiler, + transactionAcquirer, + spanAttributes: [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [ATTR_DB_SYSTEM_NAME, "sqlite"], + ], + transformRows, + })) as SqliteClient, + { + [TypeId]: TypeId, + config: options, + export: Effect.flatMap(acquirer, (_) => _.export), + loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)), + }, + ) + + return client + }) + +const nativeLayer = (config: Config) => + Layer.effect( + Sqlite.Native, + Effect.gen(function* () { + const native = new Database(config.filename, { + readonly: config.readonly, + readwrite: config.readwrite ?? true, + create: config.create ?? true, + }) + yield* Effect.addFinalizer(() => Effect.sync(() => native.close())) + if (config.disableWAL !== true) native.run("PRAGMA journal_mode = WAL;") + return native + }), + ) + +const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config)) + +const drizzleLayer = Layer.effect( + Sqlite.Drizzle, + Effect.gen(function* () { + return drizzle({ client: (yield* Sqlite.Native) as Database }) + }), +) + +export const layer = (config: Config) => { + const native = nativeLayer(config) + return Layer.merge(native, Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe( + Layer.provide(Reactivity.layer), + ) +} diff --git a/packages/core/src/database/sqlite.node.ts b/packages/core/src/database/sqlite.node.ts new file mode 100644 index 0000000000000000000000000000000000000000..6eaecbee26ea13124bf9148ec3c68b39b2b92ef5 --- /dev/null +++ b/packages/core/src/database/sqlite.node.ts @@ -0,0 +1,178 @@ +import { DatabaseSync, type SQLInputValue } from "node:sqlite" +import { drizzle } from "drizzle-orm/node-sqlite" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Semaphore from "effect/Semaphore" +import * as Stream from "effect/Stream" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import * as Client from "effect/unstable/sql/SqlClient" +import type { Connection } from "effect/unstable/sql/SqlConnection" +import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" +import * as Statement from "effect/unstable/sql/Statement" +import { Sqlite } from "./sqlite" + +const ATTR_DB_SYSTEM_NAME = "db.system.name" + +const TypeId = "~@opencode-ai/core/database/SqliteNode" as const +type TypeId = typeof TypeId + +interface SqliteClient extends Client.SqlClient { + readonly [TypeId]: TypeId + readonly config: Config + readonly loadExtension: (path: string) => Effect.Effect + readonly updateValues: never +} + +interface Config { + readonly filename: string + readonly readonly?: boolean + readonly create?: boolean + readonly readwrite?: boolean + readonly disableWAL?: boolean + readonly timeout?: number + readonly allowExtension?: boolean + readonly spanAttributes?: Record + readonly transformResultNames?: (str: string) => string + readonly transformQueryNames?: (str: string) => string +} + +interface SqliteConnection extends Connection { + readonly loadExtension: (path: string) => Effect.Effect +} + +const make = (options: Config) => + Effect.gen(function* () { + const native = (yield* Sqlite.Native) as DatabaseSync + + const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) + const transformRows = options.transformResultNames + ? Statement.defaultTransforms(options.transformResultNames).array + : undefined + + const run = (query: string, params: ReadonlyArray = []) => + Effect.withFiber>, SqlError>((fiber) => { + const statement = native.prepare(query) + statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers)) + try { + return Effect.succeed(statement.all(...(params as SQLInputValue[])) as Array>) + } catch (cause) { + return Effect.fail( + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }), + }), + ) + } + }) + + const runValues = (query: string, params: ReadonlyArray = []) => + Effect.withFiber>, SqlError>((fiber) => { + const statement = native.prepare(query) + statement.setReadBigInts(Context.get(fiber.context, Client.SafeIntegers)) + statement.setReturnArrays(true) + try { + return Effect.succeed( + statement.all(...(params as SQLInputValue[])) as unknown as ReadonlyArray>, + ) + } catch (cause) { + return Effect.fail( + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to execute statement", operation: "execute" }), + }), + ) + } + }) + + const connection = identity({ + execute(query, params, transformRows) { + return transformRows ? Effect.map(run(query, params), transformRows) : run(query, params) + }, + executeRaw(query, params) { + return run(query, params) + }, + executeValues(query, params) { + return runValues(query, params) + }, + executeUnprepared(query, params, transformRows) { + return this.execute(query, params, transformRows) + }, + executeStream() { + return Stream.die("executeStream not implemented") + }, + loadExtension: (path) => + Effect.try({ + try: () => native.loadExtension(path), + catch: (cause) => + new SqlError({ + reason: classifySqliteError(cause, { message: "Failed to load extension", operation: "loadExtension" }), + }), + }), + }) + + const semaphore = yield* Semaphore.make(1) + const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)) + const transactionAcquirer = Effect.uninterruptibleMask((restore) => { + const fiber = Fiber.getCurrent()! + const scope = Context.getUnsafe(fiber.context, Scope.Scope) + return Effect.as( + Effect.tap(restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1))), + connection, + ) + }) + + const client = Object.assign( + (yield* Client.make({ + acquirer, + compiler, + transactionAcquirer, + spanAttributes: [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [ATTR_DB_SYSTEM_NAME, "sqlite"], + ], + transformRows, + })) as SqliteClient, + { + [TypeId]: TypeId, + config: options, + loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)), + }, + ) + + return client + }) + +const nativeLayer = (config: Config) => + Layer.effect( + Sqlite.Native, + Effect.gen(function* () { + const native = new DatabaseSync(config.filename, { + readOnly: config.readonly, + timeout: config.timeout, + allowExtension: config.allowExtension, + enableForeignKeyConstraints: true, + open: true, + }) + yield* Effect.addFinalizer(() => Effect.sync(() => native.close())) + if (config.disableWAL !== true && config.readonly !== true) native.exec("PRAGMA journal_mode = WAL;") + return native + }), + ) + +const sqliteLayer = (config: Config) => Layer.effect(Client.SqlClient, make(config)) + +const drizzleLayer = Layer.effect( + Sqlite.Drizzle, + Effect.gen(function* () { + return drizzle({ client: (yield* Sqlite.Native) as DatabaseSync }) as unknown as Sqlite.DrizzleClient + }), +) + +export const layer = (config: Config) => { + const native = nativeLayer(config) + return Layer.merge(native, Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe( + Layer.provide(Reactivity.layer), + ) +} diff --git a/packages/core/src/database/sqlite.ts b/packages/core/src/database/sqlite.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2304a54737acbaa161f2b6ba61456f598dea0d3 --- /dev/null +++ b/packages/core/src/database/sqlite.ts @@ -0,0 +1,8 @@ +export * as Sqlite from "./sqlite" + +import { Context } from "effect" +import type { drizzle } from "drizzle-orm/bun-sqlite" + +export type DrizzleClient = ReturnType +export class Native extends Context.Service()("@opencode-ai/core/database/SqliteNative") {} +export class Drizzle extends Context.Service()("@opencode-ai/core/database/SqliteDrizzle") {} diff --git a/packages/core/src/effect/app-node-builder.ts b/packages/core/src/effect/app-node-builder.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f01469dab09ed0e690f6931d9127659fa721821 --- /dev/null +++ b/packages/core/src/effect/app-node-builder.ts @@ -0,0 +1,23 @@ +import { buildLocationServiceMap } from "../location-services" +import { LocationServiceMap } from "../location-service-map" +import { LayerNode } from "./layer-node" +import { makeGlobalNode } from "./app-node" + +export function build(root: LayerNode.Node, replacements: LayerNode.Replacements = []) { + let allReplacements = replacements + + // Only build the location service map if it's actually needed + if (LayerNode.hasUnbound(root, LocationServiceMap.node) && !hasReplacement(replacements, LocationServiceMap.node)) { + const locationMap = buildLocationServiceMap(replacements) + const locationMapNode = makeGlobalNode({ service: LocationServiceMap.Service, layer: locationMap, deps: [] }) + allReplacements = replacements.concat([[LocationServiceMap.node, locationMapNode]]) + } + + return LayerNode.compile(root, allReplacements) +} + +function hasReplacement(replacements: LayerNode.Replacements, node: LayerNode.Node) { + return replacements.some(([source]) => source.name === node.name) +} + +export * as AppNodeBuilder from "./app-node-builder" diff --git a/packages/core/src/effect/app-node-platform.ts b/packages/core/src/effect/app-node-platform.ts new file mode 100644 index 0000000000000000000000000000000000000000..adba0eb9cc94f279ea5a505d1526820e27df7974 --- /dev/null +++ b/packages/core/src/effect/app-node-platform.ts @@ -0,0 +1,18 @@ +import { NodeFileSystem, NodePath } from "@effect/platform-node" +import { LLMClient, RequestExecutor } from "@opencode-ai/llm/route" +import { FileSystem, Path } from "effect" +import { FetchHttpClient } from "effect/unstable/http" +import { HttpClient } from "effect/unstable/http" +import { makeGlobalNode } from "./app-node" + +export const filesystem = makeGlobalNode({ service: FileSystem.FileSystem, layer: NodeFileSystem.layer, deps: [] }) +export const path = makeGlobalNode({ service: Path.Path, layer: NodePath.layer, deps: [] }) +export const httpClient = makeGlobalNode({ service: HttpClient.HttpClient, layer: FetchHttpClient.layer, deps: [] }) +export const requestExecutor = makeGlobalNode({ + service: RequestExecutor.Service, + layer: RequestExecutor.layer, + deps: [httpClient], +}) +export const llmClient = makeGlobalNode({ service: LLMClient.Service, layer: LLMClient.layer, deps: [requestExecutor] }) + +export * as LayerNodePlatform from "./app-node-platform" diff --git a/packages/core/src/effect/app-node.ts b/packages/core/src/effect/app-node.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8112f921ad7a7dcefbf6985c24995df1855c96b --- /dev/null +++ b/packages/core/src/effect/app-node.ts @@ -0,0 +1,14 @@ +import { LayerNode } from "./layer-node" + +export const tags = LayerNode.tags({ + location: ["global"], + global: [], +}) + +export type GlobalNode = LayerNode.Node +export type LocationNode = LayerNode.Node + +export const makeGlobalNode = tags.make("global") +export const makeLocationNode = tags.make("location") + +export * as Node from "./app-node" diff --git a/packages/core/src/effect/dfdf b/packages/core/src/effect/dfdf new file mode 100644 index 0000000000000000000000000000000000000000..77f2d57968907e1c1d7dcd5bfae945c50c217dfe --- /dev/null +++ b/packages/core/src/effect/dfdf @@ -0,0 +1 @@ +File to save in: ~/.local/share/opencode/worktree/012780/location-layer-tiers/packages/core/src/effect/ \ No newline at end of file diff --git a/packages/core/src/effect/keyed-mutex.ts b/packages/core/src/effect/keyed-mutex.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7c69b7db9130818ebed1b607914e2803d6910a9 --- /dev/null +++ b/packages/core/src/effect/keyed-mutex.ts @@ -0,0 +1,45 @@ +export * as KeyedMutex from "./keyed-mutex" + +import { Effect, Semaphore } from "effect" + +export interface KeyedMutex { + readonly size: Effect.Effect + readonly withLock: (key: Key) => (effect: Effect.Effect) => Effect.Effect +} + +/** + * Creates an in-memory mutex with one lock per key. Entries are removed when no + * holder or waiter remains. + * + * same key -> queue + * different key -> run independently + * + * `users` counts holders and waiters so an entry is not removed while a waiter + * will reuse it. + */ +export const makeUnsafe = (): KeyedMutex => { + const locks = new Map() + + const withLock = + (key: Key) => + (effect: Effect.Effect) => + Effect.suspend(() => { + const current = locks.get(key) + const entry = current ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 } + if (!current) locks.set(key, entry) + entry.users++ + return entry.semaphore.withPermit(effect).pipe( + Effect.ensuring( + Effect.sync(() => { + entry.users-- + if (entry.users === 0) locks.delete(key) + }), + ), + ) + }) + + return { size: Effect.sync(() => locks.size), withLock } +} + +/** Creates an in-memory keyed mutex inside an Effect workflow. */ +export const make = (): Effect.Effect> => Effect.sync(makeUnsafe) diff --git a/packages/core/src/effect/layer-node.ts b/packages/core/src/effect/layer-node.ts new file mode 100644 index 0000000000000000000000000000000000000000..9dbc3d51607be375675cc11bddcf181b17a58ad7 --- /dev/null +++ b/packages/core/src/effect/layer-node.ts @@ -0,0 +1,333 @@ +import { Brand, Context, Layer } from "effect" + +type AnyNode = Node +type RuntimeLayer = Layer.Layer +type NodeList = readonly [] | readonly [Item, ...Item[]] +export type Output = [Item] extends [never] ? never : Item extends Node ? A : never +export type Error = [Item] extends [never] ? never : Item extends Node ? E : never +type NodeTag = [Item] extends [never] ? undefined : Item extends Node ? T : never +type Missing = Exclude> +type CheckDependencies = [ + Missing, Dependencies>, +] extends [never] + ? unknown + : { readonly "Missing dependencies": Missing, Dependencies> } +declare const $OutputType: unique symbol +declare const $ErrorType: unique symbol + +export type Tag = Name & Brand.Brand<"LayerNode.Tag"> + +const makeTag = Brand.nominal() + +export interface Node { + readonly kind: "layer" | "unbound" | "group" + readonly name: string + readonly service?: Context.Service.Any + readonly implementation?: Layer.Any + readonly dependencies: readonly AnyNode[] + readonly tag?: T + readonly [$OutputType]?: () => A + readonly [$ErrorType]?: () => E +} + +type NodeIdentity = + | { readonly service: Context.Service.Any; readonly name?: never } + | { readonly name: string; readonly service?: never } +type DistributiveOmit = A extends unknown ? Omit : never + +export type TagConfig = Readonly> +type TagNames = keyof Config & string +type NodeInTags = Node | undefined> +type CheckTags = [Exclude>] extends [ + never, +] + ? unknown + : { readonly "Invalid tag dependencies": Exclude> } + +export interface Tags { + readonly values: { readonly [Name in TagNames]: Tag } + readonly make: >( + name: Name, + ) => ( + input: DistributiveOmit>, "tag"> & + CheckTags>, + ) => Node, Layer.Error | Error, Tag> +} + +export function tags( + config: Config, +): Tags { + const names = Object.keys(config) as TagNames[] + const values = Object.fromEntries(names.map((name) => [name, makeTag(name)])) as Tags["values"] + return { + values, + make: ((name: TagNames) => (input: DistributiveOmit, "tag">) => + make({ ...input, tag: values[name] })) as Tags["make"], + } +} + +// Nodes --------------------------------------------------------------------- + +type MakeInput< + Implementation extends Layer.Any, + Items extends NodeList, + T extends Tag | undefined = undefined, +> = NodeIdentity & { + readonly layer: Implementation + readonly deps: Items & CheckDependencies> + readonly tag?: T +} + +export function make< + const Implementation extends Layer.Any, + const Items extends NodeList, + const T extends Tag | undefined = undefined, +>( + input: MakeInput, +): Node, Layer.Error | Error, T> { + return { + kind: "layer", + name: input.service !== undefined ? input.service.key : input.name, + service: input.service, + implementation: input.layer, + dependencies: input.deps, + tag: input.tag, + } +} + +export function unbound(service: Context.Key, tag: T): Node { + return { + kind: "unbound", + name: service.key, + service, + dependencies: [], + tag, + } +} + +export function group( + dependencies: Items, +): Node, Error, NodeTag> { + return { kind: "group", name: "group", dependencies } +} + +export type Replacement = readonly [source: AnyNode, replacement: AnyNode | Layer.Any] +export type Replacements = readonly Replacement[] + +type CheckReplacementErrors = [Exclude] extends [never] + ? unknown + : { readonly "New replacement errors": Exclude } + +type CheckReplacement = Item extends readonly [Node, infer Replacement] + ? Replacement extends Node, infer E2, T> + ? CheckReplacementErrors> + : Replacement extends Layer.Layer, infer E2, never> + ? CheckReplacementErrors> + : { readonly "Invalid replacement": Replacement } + : { readonly "Invalid replacement": Item } + +type CheckReplacements = { + readonly [K in keyof Items]: CheckReplacement +} + +type ValidReplacements = Items & CheckReplacements + +function replacementNode(source: AnyNode, replacement: AnyNode | Layer.Any) { + const replacementNode = isNode(replacement) + ? replacement + : make({ + ...nodeMakeIdentity(source), + layer: replacement as Layer.Layer, + deps: [], + tag: source.tag, + }) + if (source.name !== replacementNode.name) { + throw new Error(`Cannot replace ${source.name} with ${replacementNode.name}`) + } + if (source.tag !== replacementNode.tag) { + throw new Error(`Cannot replace ${source.name} across tags`) + } + return replacementNode +} + +function nodeMakeIdentity(node: AnyNode): NodeIdentity { + if (node.service !== undefined) return { service: node.service } + return { name: node.name } +} + +function isNode(input: Layer.Any | AnyNode): input is AnyNode { + return "kind" in input && "dependencies" in input +} + +// Tree ----------------------------------------------------------------------- + +type Visit = (node: AnyNode, context: VisitContext) => Result + +type VisitContext = { + readonly cache: Map + readonly visit: (node: AnyNode) => Result +} + +function walk( + root: AnyNode, + visit: Visit, + options: { + readonly cache?: Map + readonly resolve?: (node: AnyNode) => AnyNode + readonly detectCycles?: boolean + } = {}, +) { + const cache = options.cache ?? new Map() + const visiting = new Set() + const stack: AnyNode[] = [] + + const recur = (node: AnyNode): Result => { + const target = options.resolve?.(node) ?? node + const cached = cache.get(target) + if (cached !== undefined || cache.has(target)) return cached! + + if (options.detectCycles !== false && visiting.has(target)) { + const start = stack.indexOf(target) + throw new Error( + `Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`, + ) + } + + visiting.add(target) + stack.push(target) + try { + const result = visit(target, { cache, visit: recur }) + if (!cache.has(target)) cache.set(target, result) + return result + } finally { + stack.pop() + visiting.delete(target) + } + } + + return recur(root) +} + +export function hoist( + root: Node, + tag: T, + replacements?: ValidReplacements, +): { + readonly node: Node + readonly hoisted: Node +} { + const hoisted = new Map() + const replacementMap = replacementMapFrom(replacements) + + const node = walk( + root, + (node, context) => { + if (node.kind === "group") { + return { ...node, dependencies: node.dependencies.map(context.visit) } + } + if (node.tag === tag) { + const existing = hoisted.get(node.name) + if (existing && existing !== node) { + throw new Error(`Tag ${tag} has conflicting implementations for ${node.name}`) + } + hoisted.set(node.name, rewriteReplacementDependencies(node, replacementMap)) + return group([]) + } + if (node.kind === "unbound") { + return node + } + return { ...node, dependencies: node.dependencies.map(context.visit) } + }, + { resolve: (node) => replacementMap.get(node.name) ?? node }, + ) + + return { + node: node as Node, + hoisted: group(Array.from(hoisted.values())) as Node, + } +} + +export function compile( + root: Node, + replacements?: ValidReplacements, +): Layer.Layer { + const replacementMap = replacementMapFrom(replacements) + const cache = new Map() + const compileNode = (node: AnyNode) => + walk( + node, + (node, context) => { + if (node.kind === "unbound") throw new Error(`Unbound layer node: ${node.name}`) + const dependencies = node.dependencies.flatMap(flatten).map(context.visit) + const implementation = node.implementation! as RuntimeLayer + return dependencies.length === 0 + ? implementation + : implementation.pipe(Layer.provide(dependencies as [RuntimeLayer, ...RuntimeLayer[]])) + }, + { cache, resolve: (node) => replacementMap.get(node.name) ?? node }, + ) + const layers = flatten(root).map((node) => compileNode(node)) + const layer = layers.reduce((result, layer) => layer.pipe(Layer.provideMerge(result)), Layer.empty) + return layer as Layer.Layer +} + +function replacementMapFrom(replacements?: Replacements) { + return ( + replacements?.reduce((map, [source, replacement]) => { + const normalized = rewriteReplacementDependencies(replacementNode(source, replacement), map) + const current = new Map([[source.name, normalized]]) + for (const [name, node] of map) map.set(name, rewriteReplacementDependencies(node, current)) + map.set(source.name, normalized) + return map + }, new Map()) ?? new Map() + ) +} + +function rewriteReplacementDependencies(root: AnyNode, replacements: ReadonlyMap) { + if (replacements.size === 0) return root + const cache = new Map() + const visiting = new Set() + const stack: AnyNode[] = [] + + const recur = (node: AnyNode, isRoot = false): AnyNode => { + const target = isRoot ? node : (replacements.get(node.name) ?? node) + const cached = cache.get(target) + if (cached !== undefined || cache.has(target)) return cached! + if (visiting.has(target)) { + const start = stack.indexOf(target) + throw new Error( + `Cycle detected in layer tree: ${[...stack.slice(start), target].map((item) => item.name).join(" -> ")}`, + ) + } + + visiting.add(target) + stack.push(target) + try { + const dependencies = target.dependencies.map((dependency) => recur(dependency)) + const result = dependencies.every((dependency, index) => dependency === target.dependencies[index]) + ? target + : { ...target, dependencies } + cache.set(target, result) + return result + } finally { + stack.pop() + visiting.delete(target) + } + } + + return recur(root, true) +} + +export function hasUnbound(root: Node, source: AnyNode): boolean { + if (source.kind !== "unbound") throw new Error(`Cannot check non-unbound layer node: ${source.name}`) + return walk(root, (node, context) => { + if (node === source) return true + return node.dependencies.some(context.visit) + }) +} + +function flatten(node: AnyNode): readonly AnyNode[] { + return node.kind === "group" ? node.dependencies.flatMap(flatten) : [node] +} + +export * as LayerNode from "./layer-node" diff --git a/packages/core/src/effect/memo-map.ts b/packages/core/src/effect/memo-map.ts new file mode 100644 index 0000000000000000000000000000000000000000..c797dbf42ed065f1f7c30be55af6a31918dcb643 --- /dev/null +++ b/packages/core/src/effect/memo-map.ts @@ -0,0 +1,3 @@ +import { Layer } from "effect" + +export const memoMap = Layer.makeMemoMapUnsafe() diff --git a/packages/core/src/effect/runtime.ts b/packages/core/src/effect/runtime.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ad0f85176caa8862f403054335479697069429c --- /dev/null +++ b/packages/core/src/effect/runtime.ts @@ -0,0 +1,21 @@ +import { Layer, type Context, ManagedRuntime, type Effect } from "effect" +import { memoMap } from "./memo-map" +import { Observability } from "../observability" + +export function makeRuntime(service: Context.Service, layer: Layer.Layer) { + let rt: ManagedRuntime.ManagedRuntime | undefined + const getRuntime = () => + (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer, { + memoMap, + })) + + return { + runSync: (fn: (svc: S) => Effect.Effect) => getRuntime().runSync(service.use(fn)), + runPromiseExit: (fn: (svc: S) => Effect.Effect, options?: Effect.RunOptions) => + getRuntime().runPromiseExit(service.use(fn), options), + runPromise: (fn: (svc: S) => Effect.Effect, options?: Effect.RunOptions) => + getRuntime().runPromise(service.use(fn), options), + runFork: (fn: (svc: S) => Effect.Effect) => getRuntime().runFork(service.use(fn)), + runCallback: (fn: (svc: S) => Effect.Effect) => getRuntime().runCallback(service.use(fn)), + } +} diff --git a/packages/core/src/effect/service-use.ts b/packages/core/src/effect/service-use.ts new file mode 100644 index 0000000000000000000000000000000000000000..89d9acf98eadbf727bc7e5d9961d9a7f7f347c6a --- /dev/null +++ b/packages/core/src/effect/service-use.ts @@ -0,0 +1,43 @@ +import { Context, Effect } from "effect" + +type EffectMethod = (...args: ReadonlyArray) => Effect.Effect + +type ServiceUse = { + readonly [Key in keyof Shape as Shape[Key] extends EffectMethod ? Key : never]: Shape[Key] extends ( + ...args: infer Args + ) => infer Return + ? Args extends ReadonlyArray + ? Return extends Effect.Effect + ? (...args: Args) => Effect.Effect + : never + : never + : never +} + +export const serviceUse = (tag: Context.Service) => { + const cache = new Map Effect.Effect>() + // This is the only dynamic boundary: TypeScript knows the accessor shape, + // but Proxy property names are runtime values. + const access = new Proxy( + {}, + { + get: (_, key) => { + if (typeof key !== "string") return undefined + const cached = cache.get(key) + if (cached) return cached + const accessor = (...args: unknown[]) => + tag.use((service) => { + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy keys are checked at runtime. + const method = service[key as keyof Shape] + if (typeof method !== "function") return Effect.die(new Error(`Service method not found: ${key}`)) + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- ServiceUse exposes only Effect-returning methods. + return (method as (...args: unknown[]) => Effect.Effect)(...args) + }) + cache.set(key, accessor) + return accessor + }, + }, + ) + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion -- Proxy implements the mapped accessor surface lazily. + return access as ServiceUse +} diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts new file mode 100644 index 0000000000000000000000000000000000000000..c92ac0ac2ce30d7cdf0f44075e9a82bb2fc0875e --- /dev/null +++ b/packages/core/src/event.ts @@ -0,0 +1,638 @@ +export * as EventV2 from "./event" + +import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect" +import { Event } from "@opencode-ai/schema/event" +import type { Data, Definition, Payload } from "@opencode-ai/schema/event" +import { and, asc, eq, gt, inArray } from "drizzle-orm" +import { Database } from "./database/database" +import { EventSequenceTable, EventTable } from "./event/sql" +import { Location } from "./location" +import { makeGlobalNode } from "./effect/app-node" +import { isDeepStrictEqual } from "node:util" +import { Durable } from "@opencode-ai/schema/durable-event-manifest" + +export const ID = Event.ID +export type ID = import("@opencode-ai/schema/event").ID +export type { Data, Definition, Payload } from "@opencode-ai/schema/event" + +export type Subscriber = (event: Payload) => Effect.Effect +export type Unsubscribe = Effect.Effect + +export const latestSequence = Effect.fn("EventV2.latestSequence")(function* ( + db: Database.Interface["db"], + aggregateID: string, +) { + const row = yield* db + .select({ seq: EventSequenceTable.seq }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .get() + .pipe(Effect.orDie) + return row?.seq ?? -1 +}) + +export type SerializedEvent = { + readonly id: ID + readonly type: string + readonly seq: number + readonly aggregateID: string + readonly data: Record +} + +export class InvalidDurableEventError extends Schema.TaggedErrorClass()( + "EventV2.InvalidDurableEvent", + { + type: Schema.String, + message: Schema.String, + }, +) {} + +const decodeSerializedEvent = (event: SerializedEvent): Payload => { + const definition = Durable.get(event.type) + if (!definition?.durable) { + throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }) + } + return { + id: event.id, + type: definition.type, + durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version }, + data: Schema.decodeUnknownSync(definition.data)(event.data), + } +} + +export const readAggregate = Effect.fn("EventV2.readAggregate")(function* ( + db: Database.Interface["db"], + input: { + readonly aggregateID: string + readonly after?: number + readonly limit: number + readonly manifest: { + readonly definitions: ReadonlyMap + readonly schema: Schema.Decoder + } + }, +) { + const after = input.after ?? -1 + const rows = yield* db + .select() + .from(EventTable) + .where( + and( + eq(EventTable.aggregate_id, input.aggregateID), + gt(EventTable.seq, after), + inArray(EventTable.type, Array.from(input.manifest.definitions.keys())), + ), + ) + .orderBy(asc(EventTable.seq)) + .limit(input.limit + 1) + .all() + .pipe(Effect.orDie) + const page = rows.slice(0, input.limit) + const decode = Schema.decodeUnknownSync(input.manifest.schema) + const events = page.map((event) => + decode({ + id: event.id, + type: input.manifest.definitions.get(event.type)?.type ?? event.type, + durable: { + aggregateID: event.aggregate_id, + seq: event.seq, + version: input.manifest.definitions.get(event.type)?.durable?.version, + }, + data: event.data, + }), + ) + return { + events, + hasMore: rows.length > input.limit, + } +}) + +export class SubscriberOverflowError extends Schema.TaggedErrorClass()( + "EventV2.SubscriberOverflow", + { capacity: Schema.Int }, +) {} + +export const define = Event.define +export const versionedType = Event.versionedType + +export interface PublishOptions { + readonly id?: ID + readonly metadata?: Record + readonly location?: Location.Ref + /** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */ + readonly commit?: (seq: number) => Effect.Effect +} + +export interface Interface { + readonly publish: ( + definition: D, + data: Data, + options?: PublishOptions, + ) => Effect.Effect> + readonly subscribe: (definition: D) => Stream.Stream> + readonly all: () => Stream.Stream + readonly durable: (input: { readonly aggregateID: string; readonly after?: number }) => Stream.Stream + /** @deprecated Use `all()` and consume the returned stream. */ + readonly listen: (listener: Subscriber) => Effect.Effect + readonly project: (definition: D, projector: Subscriber) => Effect.Effect + readonly replay: ( + event: SerializedEvent, + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, + ) => Effect.Effect + readonly replayAll: ( + events: SerializedEvent[], + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, + ) => Effect.Effect + readonly remove: (aggregateID: string) => Effect.Effect + readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Event") {} + +export const allBounded = (events: Interface, capacity: number) => + Effect.gen(function* () { + const queue = yield* Queue.dropping(capacity) + const unsubscribe = yield* events.listen((event) => + Queue.offer(queue, event).pipe( + Effect.flatMap((accepted) => + accepted ? Effect.void : Queue.fail(queue, new SubscriberOverflowError({ capacity })).pipe(Effect.asVoid), + ), + ), + ) + yield* Effect.addFinalizer(() => unsubscribe.pipe(Effect.andThen(Queue.shutdown(queue)), Effect.asVoid)) + return Stream.fromQueue(queue) + }) + +export interface LayerOptions { + readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect +} + +export const layerWith = (options?: LayerOptions) => + Layer.effect( + Service, + Effect.gen(function* () { + const pubsub = { + all: yield* PubSub.unbounded(), + durable: new Map>>(), + typed: new Map>(), + } + const projectors = new Map() + // TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads. + const listeners = new Array() + const { db } = yield* Database.Service + + const getOrCreate = (definition: Definition) => + Effect.gen(function* () { + const existing = pubsub.typed.get(definition.type) + if (existing) return existing + const created = yield* PubSub.unbounded() + pubsub.typed.set(definition.type, created) + return created + }) + + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + yield* PubSub.shutdown(pubsub.all) + yield* Effect.forEach( + pubsub.durable.values(), + (pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }), + { discard: true }, + ) + yield* Effect.forEach(pubsub.typed.values(), PubSub.shutdown, { discard: true }) + }), + ) + + function commitDurableEvent( + definition: Definition, + event: Payload, + input?: { + readonly seq: number + readonly aggregateID: string + readonly ownerID?: string + readonly strictOwner?: boolean + }, + commit?: (seq: number) => Effect.Effect, + ) { + return Effect.gen(function* () { + const durable = definition?.durable + if (durable) { + const aggregateID = (event.data as Record)[durable.aggregate] + if (typeof aggregateID !== "string") { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Expected string aggregate field ${durable.aggregate}`, + }), + ) + } else { + if (input && input.aggregateID !== aggregateID) { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Aggregate mismatch: expected ${input.aggregateID}, got ${aggregateID}`, + }), + ) + } + const list = projectors.get(event.type) ?? [] + return yield* Effect.uninterruptible( + Effect.gen(function* () { + const committed = yield* db + .transaction( + () => + Effect.gen(function* () { + const row = yield* db + .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id }) + .from(EventSequenceTable) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .get() + .pipe(Effect.orDie) + const latest = row?.seq ?? -1 + const encoded = Schema.encodeUnknownSync(definition.data)(event.data) as Record< + string, + unknown + > + if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`, + }), + ) + } + if (input && input.seq <= latest) { + const stored = yield* db + .select() + .from(EventTable) + .where(and(eq(EventTable.aggregate_id, aggregateID), eq(EventTable.seq, input.seq))) + .get() + .pipe(Effect.orDie) + if ( + stored?.id === event.id && + stored.type === versionedType(definition.type, durable.version) && + isDeepStrictEqual(stored.data, encoded) + ) { + if (input.ownerID && row?.ownerID == null) { + yield* db + .update(EventSequenceTable) + .set({ owner_id: input.ownerID }) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .run() + .pipe(Effect.orDie) + } + return + } + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Replay diverged at aggregate ${aggregateID} sequence ${input.seq}`, + }), + ) + } + if (input && row?.ownerID && row.ownerID !== input.ownerID) { + return + } + const seq = input?.seq ?? latest + 1 + if (input && seq !== latest + 1) { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`, + }), + ) + } + const stored = yield* db + .select({ aggregateID: EventTable.aggregate_id, seq: EventTable.seq }) + .from(EventTable) + .where(eq(EventTable.id, event.id)) + .get() + .pipe(Effect.orDie) + if (stored) + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`, + }), + ) + const committed = { + ...event, + durable: { aggregateID, seq, version: durable.version }, + } as Payload + for (const projector of list) { + yield* projector(committed) + } + if (commit) yield* commit(seq) + yield* db + .insert(EventSequenceTable) + .values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }]) + .onConflictDoUpdate({ + target: EventSequenceTable.aggregate_id, + set: { + seq, + ...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}), + }, + }) + .run() + .pipe(Effect.orDie) + yield* db + .insert(EventTable) + .values([ + { + id: event.id, + aggregate_id: aggregateID, + seq, + type: versionedType(definition.type, durable.version), + data: encoded, + }, + ]) + .run() + .pipe(Effect.orDie) + return { aggregateID, seq } + }), + { behavior: "immediate" }, + ) + .pipe(Effect.orDie) + if (committed) { + yield* Effect.forEach( + pubsub.durable.get(committed.aggregateID) ?? [], + (wake) => PubSub.publish(wake, undefined), + { discard: true }, + ) + } + return committed + }), + ) + } + } + }) + } + + function publishEvent(definition: D, event: Payload, commit?: PublishOptions["commit"]) { + return Effect.gen(function* () { + if (!definition?.durable && commit) + return yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: "Local commit hooks require a durable event", + }), + ) + if (definition?.durable) { + const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit) + if (committed) { + event = { + ...event, + durable: { + aggregateID: committed.aggregateID, + seq: committed.seq, + version: definition.durable.version, + }, + } + yield* notify(event as Payload, true) + return event + } + } + yield* notify(event as Payload, false) + return event + }) + } + + const observe = (event: Payload, observer: (event: Payload) => Effect.Effect) => + Effect.suspend(() => observer(event)).pipe( + Effect.catchCauseIf( + (cause) => !Cause.hasInterrupts(cause), + (cause) => Effect.logError("Event listener failed", { eventID: event.id, eventType: event.type, cause }), + ), + ) + + function notify(event: Payload, isolateListeners: boolean) { + return Effect.gen(function* () { + yield* Effect.forEach( + listeners, + (listener) => (isolateListeners ? observe(event, listener) : listener(event)), + { discard: true }, + ) + const typed = pubsub.typed.get(event.type) + if (typed) yield* PubSub.publish(typed, event) + yield* PubSub.publish(pubsub.all, event) + }) + } + + function publish(definition: D, data: Data, options?: PublishOptions) { + return Effect.gen(function* () { + const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service)) + const location = + options?.location ?? + (serviceLocation + ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID } + : undefined) + return yield* publishEvent( + definition, + { + id: options?.id ?? ID.create(), + ...(options?.metadata ? { metadata: options.metadata } : {}), + type: definition.type, + ...(location ? { location } : {}), + data, + } as Payload, + options?.commit, + ) + }) + } + + function replay( + event: SerializedEvent, + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, + ) { + return Effect.gen(function* () { + const definition = Durable.get(event.type) + if (!definition?.durable) { + yield* Effect.die( + new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }), + ) + } else { + const payload = { + id: event.id, + type: definition.type, + data: Schema.decodeUnknownSync(definition.data)(event.data), + } as Payload + const committed = yield* commitDurableEvent(definition, payload, { + seq: event.seq, + aggregateID: event.aggregateID, + ownerID: options?.ownerID, + strictOwner: options?.strictOwner, + }) + if (committed && options?.publish) { + yield* notify( + { + ...payload, + durable: { + aggregateID: committed.aggregateID, + seq: committed.seq, + version: definition.durable.version, + }, + }, + true, + ) + } + } + }) + } + + function replayAll( + events: SerializedEvent[], + options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }, + ) { + return Effect.gen(function* () { + const source = events[0]?.aggregateID + if (!source) return undefined + if (events.some((event) => event.aggregateID !== source)) { + yield* Effect.die( + new InvalidDurableEventError({ + type: events[0]?.type ?? "unknown", + message: "Replay events must belong to the same aggregate", + }), + ) + } + const start = events[0]?.seq ?? 0 + for (const [index, event] of events.entries()) { + const seq = start + index + if (event.seq !== seq) { + yield* Effect.die( + new InvalidDurableEventError({ + type: event.type, + message: `Replay sequence mismatch at index ${index}: expected ${seq}, got ${event.seq}`, + }), + ) + } + } + for (const event of events) { + yield* replay(event, options) + } + return source + }) + } + + function remove(aggregateID: string) { + return db + .transaction(() => + Effect.gen(function* () { + yield* db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run() + yield* db.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run() + }), + ) + .pipe(Effect.orDie) + } + + function claim(aggregateID: string, ownerID: string) { + return db + .update(EventSequenceTable) + .set({ owner_id: ownerID }) + .where(eq(EventSequenceTable.aggregate_id, aggregateID)) + .run() + .pipe(Effect.orDie) + } + + const subscribe = (definition: D): Stream.Stream> => + Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe( + Stream.map((event) => event as Payload), + ) + + const streamAll = (): Stream.Stream => Stream.fromPubSub(pubsub.all) + + const readAfter = (aggregateID: string, after: number) => + (options?.beforeAggregateRead?.(aggregateID) ?? Effect.void).pipe( + Effect.andThen( + db + .select() + .from(EventTable) + .where(and(eq(EventTable.aggregate_id, aggregateID), gt(EventTable.seq, after))) + .orderBy(asc(EventTable.seq)) + .all(), + ), + Effect.orDie, + Effect.map((rows) => + rows.map((event) => + decodeSerializedEvent({ + id: event.id, + aggregateID: event.aggregate_id, + seq: event.seq, + type: event.type, + data: event.data, + }), + ), + ), + ) + + const subscribeDurable = (aggregateID: string) => + Effect.gen(function* () { + const wake = yield* PubSub.sliding(1) + const subscription = yield* PubSub.subscribe(wake) + yield* Effect.acquireRelease( + Effect.sync(() => { + const wakes = pubsub.durable.get(aggregateID) ?? new Set() + wakes.add(wake) + pubsub.durable.set(aggregateID, wakes) + }), + () => + Effect.sync(() => { + const wakes = pubsub.durable.get(aggregateID) + wakes?.delete(wake) + if (wakes?.size === 0) pubsub.durable.delete(aggregateID) + }).pipe(Effect.andThen(PubSub.shutdown(wake))), + ) + return subscription + }) + + const durable = (input: { readonly aggregateID: string; readonly after?: number }): Stream.Stream => + Stream.unwrap( + Effect.gen(function* () { + const wakes = yield* subscribeDurable(input.aggregateID) + let sequence = input.after ?? -1 + const read = Effect.suspend(() => readAfter(input.aggregateID, sequence)).pipe( + Effect.tap((events) => + Effect.sync(() => { + sequence = events.at(-1)?.durable?.seq ?? sequence + }), + ), + ) + const historical = yield* read + const live = Stream.fromSubscription(wakes).pipe( + Stream.mapEffect(() => read), + Stream.flattenIterable, + ) + return Stream.concat(Stream.fromIterable(historical), live) + }), + ) + + const listen = (listener: Subscriber): Effect.Effect => + Effect.sync(() => { + listeners.push(listener) + return Effect.sync(() => { + const index = listeners.indexOf(listener) + if (index >= 0) listeners.splice(index, 1) + }) + }) + + const project = (definition: D, projector: Subscriber): Effect.Effect => + Effect.sync(() => { + const list = projectors.get(definition.type) ?? [] + list.push((event) => projector(event as Payload)) + projectors.set(definition.type, list) + }) + + return Service.of({ + publish, + subscribe, + all: streamAll, + durable, + listen, + project, + replay, + replayAll, + remove, + claim, + }) + }), + ) + +const layer = layerWith() +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] }) diff --git a/packages/core/src/event/sql.ts b/packages/core/src/event/sql.ts new file mode 100644 index 0000000000000000000000000000000000000000..38fe34f1e3244839e06d6ce9eff516cc8cd6dbca --- /dev/null +++ b/packages/core/src/event/sql.ts @@ -0,0 +1,25 @@ +import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core" +import type { EventV2 } from "../event" + +export const EventSequenceTable = sqliteTable("event_sequence", { + aggregate_id: text().notNull().primaryKey(), + seq: integer().notNull(), + owner_id: text(), +}) + +export const EventTable = sqliteTable( + "event", + { + id: text().$type().primaryKey(), + aggregate_id: text() + .notNull() + .references(() => EventSequenceTable.aggregate_id, { onDelete: "cascade" }), + seq: integer().notNull(), + type: text().notNull(), + data: text({ mode: "json" }).$type>().notNull(), + }, + (table) => [ + uniqueIndex("event_aggregate_seq_idx").on(table.aggregate_id, table.seq), + index("event_aggregate_type_seq_idx").on(table.aggregate_id, table.type, table.seq), + ], +) diff --git a/packages/core/src/file-mutation.ts b/packages/core/src/file-mutation.ts new file mode 100644 index 0000000000000000000000000000000000000000..80a3a449f6005986ed4e9333c74480eeae783bc0 --- /dev/null +++ b/packages/core/src/file-mutation.ts @@ -0,0 +1,207 @@ +export * as FileMutation from "./file-mutation" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect, Layer, Schema } from "effect" +import { dirname } from "path" +import { KeyedMutex } from "./effect/keyed-mutex" +import { FSUtil } from "./fs-util" + +export interface Target { + readonly canonical: string + readonly resource: string +} + +export interface WriteInput { + readonly target: Target + readonly content: string | Uint8Array +} + +export interface TextWriteInput { + readonly target: Target + readonly content: string +} + +export interface ConditionalWriteInput extends WriteInput { + readonly expected: Uint8Array +} + +export interface RemoveInput { + readonly target: Target +} + +export class StaleContentError extends Schema.TaggedErrorClass()("FileMutation.StaleContentError", { + path: Schema.String, +}) {} + +export class TargetExistsError extends Schema.TaggedErrorClass()("FileMutation.TargetExistsError", { + path: Schema.String, +}) {} + +export interface WriteResult { + readonly operation: "write" + readonly target: string + readonly resource: string + readonly existed: boolean +} + +export interface RemoveResult { + readonly operation: "remove" + readonly target: string + readonly resource: string + readonly existed: boolean +} + +export interface Interface { + /** Create without replacing an existing target. */ + readonly create: (input: WriteInput) => Effect.Effect + readonly write: (input: WriteInput) => Effect.Effect + /** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */ + readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect + /** Commit only if an existing target still has the expected bytes. */ + readonly writeIfUnchanged: ( + input: ConditionalWriteInput, + ) => Effect.Effect + readonly remove: (input: RemoveInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/FileMutation") {} + +/** + * Serialize file changes by canonical target. Conditional writes compare and + * write under the same process-local lock so cooperating OpenCode mutations do + * not overwrite changes made from the same stale content. + */ +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const locks = KeyedMutex.makeUnsafe() + const withTargetLock = + (target: Target) => + (effect: Effect.Effect) => + locks.withLock(target.canonical)(Effect.uninterruptible(effect)) + + const writeResult = (target: Target, existed: boolean): WriteResult => ({ + operation: "write", + target: target.canonical, + resource: target.resource, + existed, + }) + + const removeResult = (target: Target, existed: boolean): RemoveResult => ({ + operation: "remove", + target: target.canonical, + resource: target.resource, + existed, + }) + + const write = Effect.fn("FileMutation.write")((input: WriteInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const existed = yield* fs.exists(input.target.canonical) + yield* fs.writeWithDirs(input.target.canonical, input.content) + return writeResult(input.target, existed) + }), + ), + ) + + const writeTextPreservingBom = Effect.fn("FileMutation.writeTextPreservingBom")((input: TextWriteInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const next = splitBom(input.content) + const current = yield* fs + .readFile(input.target.canonical) + .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) + yield* fs.writeWithDirs( + input.target.canonical, + joinBom(next.text, Boolean(current && hasUtf8Bom(current)) || next.bom), + ) + return writeResult(input.target, current !== undefined) + }), + ), + ) + + const create = Effect.fn("FileMutation.create")((input: WriteInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const write = + typeof input.content === "string" + ? fs.writeFileString(input.target.canonical, input.content, { flag: "wx" }) + : fs.writeFile(input.target.canonical, input.content, { flag: "wx" }) + yield* write.pipe( + Effect.catchReason("PlatformError", "NotFound", () => + fs.ensureDir(dirname(input.target.canonical)).pipe(Effect.andThen(write)), + ), + Effect.catchReason("PlatformError", "AlreadyExists", () => + Effect.fail(new TargetExistsError({ path: input.target.canonical })), + ), + ) + return writeResult(input.target, false) + }), + ), + ) + + const writeIfUnchanged = Effect.fn("FileMutation.writeIfUnchanged")((input: ConditionalWriteInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const current = yield* fs.readFile(input.target.canonical) + if (!sameBytes(current, input.expected)) { + return yield* new StaleContentError({ path: input.target.canonical }) + } + yield* typeof input.content === "string" + ? fs.writeFileString(input.target.canonical, input.content) + : fs.writeFile(input.target.canonical, input.content) + return writeResult(input.target, true) + }), + ), + ) + + const remove = Effect.fn("FileMutation.remove")((input: RemoveInput) => + withTargetLock(input.target)( + Effect.gen(function* () { + const existed = yield* fs.remove(input.target.canonical).pipe( + Effect.as(true), + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(false)), + ) + return removeResult(input.target, existed) + }), + ), + ) + + return Service.of({ create, write, writeTextPreservingBom, writeIfUnchanged, remove }) + }), +) + +function splitBom(text: string) { + const stripped = text.replace(/^\uFEFF+/, "") + return { bom: stripped.length !== text.length, text: stripped } +} + +function joinBom(text: string, bom: boolean) { + const stripped = splitBom(text).text + return bom ? `\uFEFF${stripped}` : stripped +} + +function hasUtf8Bom(content: Uint8Array) { + return content[0] === 0xef && content[1] === 0xbb && content[2] === 0xbf +} + +function sameBytes(left: Uint8Array, right: Uint8Array) { + if (left.length !== right.length) return false + return left.every((byte, index) => byte === right[index]) +} + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node] }) + +/** + * Deferred until the corresponding V2 integrations exist. + */ +// TODO: Add formatter integration after V2 formatter runtime exists. +// TODO: Publish watcher/file-edit events after V2 watcher integration exists. +// TODO: Add snapshots / undo after V2 snapshot design exists. +// TODO: Notify LSP and collect diagnostics after V2 LSP runtime exists. +// TODO: Design multi-file transactions / rollback if apply_patch needs atomic edits. +// Until then, edits are sequential and report partial application. +// TODO: Define crash recovery and idempotency for side effects between Tool.Called and durable settlement. diff --git a/packages/core/src/file.ts b/packages/core/src/file.ts new file mode 100644 index 0000000000000000000000000000000000000000..87745c01eebd4d508f6a5c07190640512826db9b --- /dev/null +++ b/packages/core/src/file.ts @@ -0,0 +1,6 @@ +export * as File from "./file" + +import { Revert } from "@opencode-ai/schema/revert" + +export const Diff = Revert.FileDiff +export type Diff = typeof Diff.Type diff --git a/packages/core/src/filesystem.ts b/packages/core/src/filesystem.ts new file mode 100644 index 0000000000000000000000000000000000000000..8470134ff7265581cb9ca9dd2b9672547eb0b2b7 --- /dev/null +++ b/packages/core/src/filesystem.ts @@ -0,0 +1,118 @@ +export * as FileSystem from "./filesystem" + +import { makeLocationNode } from "./effect/app-node" +import path from "path" +import { Context, Effect, Layer, Schema } from "effect" +import { FSUtil } from "./fs-util" +import { Location } from "./location" +import { PositiveInt, RelativePath } from "./schema" +import { FileSystemSearch } from "./filesystem/search" +import { Entry, FileSystem, FindInput, Match } from "@opencode-ai/schema/filesystem" +export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem" + +export const ReadInput = Schema.Struct({ + path: RelativePath, +}) +export type ReadInput = typeof ReadInput.Type + +export const Content = Schema.Struct({ + uri: Schema.String, + name: Schema.String.pipe(Schema.optional), + content: Schema.String, + encoding: Schema.Literals(["utf8", "base64"]), + mime: Schema.String, +}).annotate({ identifier: "FileSystem.Content" }) +export type Content = typeof Content.Type + +export const ListInput = Schema.Struct({ + path: RelativePath.pipe(Schema.optional), +}) +export type ListInput = typeof ListInput.Type + +export { FindInput } + +export class GlobInput extends Schema.Class("FileSystem.GlobInput")({ + pattern: Schema.String, + path: RelativePath.pipe(Schema.optional), + limit: PositiveInt.pipe(Schema.optional), +}) {} + +export class GrepInput extends Schema.Class("FileSystem.GrepInput")({ + pattern: Schema.String, + path: RelativePath.pipe(Schema.optional), + include: Schema.String.pipe(Schema.optional), + limit: PositiveInt.pipe(Schema.optional), +}) {} + +export const Event = FileSystem.Event + +export interface Interface { + readonly read: (input: ReadInput) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }> + readonly list: (input?: ListInput) => Effect.Effect + readonly find: (input: FindInput) => Effect.Effect + readonly glob: (input: GlobInput) => Effect.Effect + readonly grep: (input: GrepInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/FileSystem") {} + +const baseLayer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const search = yield* FileSystemSearch.Service + const root = yield* fs.realPath(location.directory).pipe(Effect.orDie) + const resolve = Effect.fnUntraced(function* (input?: RelativePath) { + const absolute = path.resolve(location.directory, input ?? ".") + if (!FSUtil.contains(location.directory, absolute)) + return yield* Effect.die(new Error("Path escapes the location")) + const real = yield* fs.realPath(absolute).pipe(Effect.orDie) + if (!FSUtil.contains(root, real)) return yield* Effect.die(new Error("Path escapes the location")) + return { absolute, real, directory: location.directory, root } + }) + return Service.of({ + find: search.find, + glob: search.glob, + grep: search.grep, + read: Effect.fn("FileSystem.read")(function* (input) { + const target = yield* resolve(input.path) + const info = yield* fs.stat(target.real).pipe(Effect.orDie) + if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file")) + return { + content: yield* fs.readFile(target.real).pipe(Effect.orDie), + mime: FSUtil.mimeType(target.real), + } + }), + list: Effect.fn("FileSystem.list")(function* (input = {}) { + const target = yield* resolve(input.path) + const info = yield* fs.stat(target.real).pipe(Effect.orDie) + if (info.type !== "Directory") return yield* Effect.die(new Error("Path is not a directory")) + return yield* fs.readDirectoryEntries(target.real).pipe( + Effect.orDie, + Effect.map((items) => + items + .flatMap((item) => { + if (item.type !== "file" && item.type !== "directory") return [] + const absolute = path.join(target.absolute, item.name) + const relative = path.relative(target.directory, absolute) + return [ + Entry.make({ + path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")), + type: item.type, + }), + ] + }) + .sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1)), + ), + ) + }), + }) + }), +) + +export const node = makeLocationNode({ + service: Service, + layer: baseLayer, + deps: [FSUtil.node, Location.node, FileSystemSearch.node], +}) diff --git a/packages/core/src/filesystem/fff.bun.ts b/packages/core/src/filesystem/fff.bun.ts new file mode 100644 index 0000000000000000000000000000000000000000..9843419ed117e5aea1939f0986c6b25cbc306faa --- /dev/null +++ b/packages/core/src/filesystem/fff.bun.ts @@ -0,0 +1,140 @@ +import { + FileFinder, + type DirItem, + type DirSearchResult, + type FileItem, + type GrepCursor, + type GrepMatch, + type GrepResult, + type InitOptions, + type MixedItem, + type MixedSearchResult, + type SearchResult, +} from "@ff-labs/fff-bun" + +declare global { + const FFF_LIBC: "gnu" | "musl" +} + +export type Result = { ok: true; value: T } | { ok: false; error: string } + +export type Init = InitOptions + +export interface Search { + items: FileItem[] + scores: SearchResult["scores"] + totalMatched: number + totalFiles: number +} + +export interface DirSearch { + items: DirItem[] + scores: DirSearchResult["scores"] + totalMatched: number + totalDirs: number +} + +export interface MixedSearch { + items: MixedItem[] + scores: MixedSearchResult["scores"] + totalMatched: number + totalFiles: number + totalDirs: number +} + +export type File = FileItem +export type Directory = DirItem +export type Mixed = MixedItem +export type Cursor = GrepCursor | null +export type Hit = GrepMatch + +export interface Grep { + items: GrepResult["items"] + totalMatched: number + totalFilesSearched: number + totalFiles: number + filteredFileCount: number + nextCursor: Cursor + regexFallbackError?: string +} + +export interface Picker { + destroy(): void + isScanning(): boolean + waitForScan(timeoutMs?: number): Promise> + refreshGitStatus(): Result + fileSearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + glob( + pattern: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + directorySearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + mixedSearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + grep( + query: string, + opts?: { + mode?: "plain" | "regex" | "fuzzy" + maxMatchesPerFile?: number + timeBudgetMs?: number + beforeContext?: number + afterContext?: number + cursor?: Cursor + pageSize?: number + }, + ): Result + trackQuery(query: string, file: string): Result + getHistoricalQuery(offset: number): Result +} + +export function available() { + return FileFinder.isAvailable() +} + +export function create(opts: Init): Result { + const made = FileFinder.create(opts) + if (!made.ok) return made + const pick = made.value + return { + ok: true, + value: { + destroy: () => pick.destroy(), + isScanning: () => pick.isScanning(), + waitForScan: (timeoutMs) => pick.waitForScan(timeoutMs), + refreshGitStatus: () => pick.refreshGitStatus(), + fileSearch: (query, next) => pick.fileSearch(query, next), + glob: (pattern, next) => pick.glob(pattern, next), + directorySearch: (query, next) => pick.directorySearch(query, next), + mixedSearch: (query, next) => pick.mixedSearch(query, next), + grep: (query, next) => pick.grep(query, next), + trackQuery: (query, file) => pick.trackQuery(query, file), + getHistoricalQuery: (offset) => pick.getHistoricalQuery(offset), + }, + } +} + +export * as Fff from "./fff.bun" diff --git a/packages/core/src/filesystem/fff.node.ts b/packages/core/src/filesystem/fff.node.ts new file mode 100644 index 0000000000000000000000000000000000000000..464c2853d7ea6165fe57017b50c84c9d2ba11ef4 --- /dev/null +++ b/packages/core/src/filesystem/fff.node.ts @@ -0,0 +1,138 @@ +export type Result = { ok: true; value: T } | { ok: false; error: string } + +export interface Init { + basePath: string + frecencyDbPath?: string + historyDbPath?: string + useUnsafeNoLock?: boolean + disableMmapCache?: boolean + disableContentIndexing?: boolean + disableWatch?: boolean + aiMode?: boolean + logFilePath?: string + logLevel?: "trace" | "debug" | "info" | "warn" | "error" + enableFsRootScanning?: boolean + enableHomeDirScanning?: boolean +} + +export interface File { + relativePath: string + fileName: string + modified: number +} + +export interface Directory { + relativePath: string + dirName: string + maxAccessFrecency: number +} + +export type Mixed = { type: "file"; item: File } | { type: "directory"; item: Directory } + +export interface Search { + items: File[] + scores: Array<{ total: number }> + totalMatched: number + totalFiles: number +} + +export interface DirSearch { + items: Directory[] + scores: Array<{ total: number }> + totalMatched: number + totalDirs: number +} + +export interface MixedSearch { + items: Mixed[] + scores: Array<{ total: number }> + totalMatched: number + totalFiles: number + totalDirs: number +} + +export type Cursor = null + +export interface Hit { + relativePath: string + fileName: string + lineNumber: number + byteOffset: number + lineContent: string + matchRanges: [number, number][] + contextBefore?: string[] + contextAfter?: string[] +} + +export interface Grep { + items: Hit[] + totalMatched: number + totalFilesSearched: number + totalFiles: number + filteredFileCount: number + nextCursor: Cursor + regexFallbackError?: string +} + +export interface Picker { + destroy(): void + isScanning(): boolean + waitForScan(timeoutMs?: number): Promise> + refreshGitStatus(): Result + fileSearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + glob( + pattern: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + directorySearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + mixedSearch( + query: string, + opts?: { + currentFile?: string + pageIndex?: number + pageSize?: number + }, + ): Result + grep( + query: string, + opts?: { + mode?: "plain" | "regex" | "fuzzy" + maxMatchesPerFile?: number + timeBudgetMs?: number + beforeContext?: number + afterContext?: number + cursor?: Cursor + pageSize?: number + }, + ): Result + trackQuery(query: string, file: string): Result + getHistoricalQuery(offset: number): Result +} + +export function available() { + return false +} + +export function create(_opts: Init): Result { + return { ok: false, error: "fff unavailable on node runtime" } +} + +export * as Fff from "./fff.node" diff --git a/packages/core/src/filesystem/ignore.ts b/packages/core/src/filesystem/ignore.ts new file mode 100644 index 0000000000000000000000000000000000000000..2f5f52bf25a711ff138c2b30777a5081471537c9 --- /dev/null +++ b/packages/core/src/filesystem/ignore.ts @@ -0,0 +1,67 @@ +import { Glob } from "../util/glob" + +const FOLDERS = new Set([ + "node_modules", + "bower_components", + ".pnpm-store", + "vendor", + ".npm", + "dist", + "build", + "out", + ".next", + "target", + "bin", + "obj", + ".git", + ".svn", + ".hg", + ".vscode", + ".idea", + ".turbo", + ".output", + "desktop", + ".sst", + ".cache", + ".webkit-cache", + "__pycache__", + ".pytest_cache", + "mypy_cache", + ".history", + ".gradle", +]) + +const FILES = [ + "**/*.swp", + "**/*.swo", + "**/*.pyc", + "**/.DS_Store", + "**/Thumbs.db", + "**/logs/**", + "**/tmp/**", + "**/temp/**", + "**/*.log", + "**/coverage/**", + "**/.nyc_output/**", +] + +export const PATTERNS = [...FILES, ...FOLDERS] + +export function match(filepath: string, opts?: { extra?: string[]; whitelist?: string[] }) { + for (const pattern of opts?.whitelist || []) { + if (Glob.match(pattern, filepath)) return false + } + + const parts = filepath.split(/[/\\]/) + for (const part of parts) { + if (FOLDERS.has(part)) return true + } + + for (const pattern of [...FILES, ...(opts?.extra || [])]) { + if (Glob.match(pattern, filepath)) return true + } + + return false +} + +export * as Ignore from "./ignore" diff --git a/packages/core/src/filesystem/protected.ts b/packages/core/src/filesystem/protected.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7646dfb48f2103bac170a983af45647e48e1be4 --- /dev/null +++ b/packages/core/src/filesystem/protected.ts @@ -0,0 +1,53 @@ +import os from "os" +import path from "path" + +const home = os.homedir() + +const DARWIN_HOME = [ + "Music", + "Pictures", + "Movies", + "Downloads", + "Desktop", + "Documents", + "Public", + "Applications", + "Library", +] + +const DARWIN_LIBRARY = [ + "Application Support/AddressBook", + "Calendars", + "Mail", + "Messages", + "Safari", + "Cookies", + "Application Support/com.apple.TCC", + "PersonalizationPortrait", + "Metadata/CoreSpotlight", + "Suggestions", +] + +const DARWIN_ROOT = ["/.DocumentRevisions-V100", "/.Spotlight-V100", "/.Trashes", "/.fseventsd"] +const WIN32_HOME = ["AppData", "Downloads", "Desktop", "Documents", "Pictures", "Music", "Videos", "OneDrive"] + +/** Directory basenames to skip when scanning the home directory. */ +export function names(): ReadonlySet { + if (process.platform === "darwin") return new Set(DARWIN_HOME) + if (process.platform === "win32") return new Set(WIN32_HOME) + return new Set() +} + +/** Absolute paths that should never be watched, stated, or scanned. */ +export function paths(): string[] { + if (process.platform === "darwin") + return [ + ...DARWIN_HOME.map((name) => path.join(home, name)), + ...DARWIN_LIBRARY.map((name) => path.join(home, "Library", name)), + ...DARWIN_ROOT, + ] + if (process.platform === "win32") return WIN32_HOME.map((name) => path.join(home, name)) + return [] +} + +export * as Protected from "./protected" diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7738388bcc0c29088b015a693532848436fed8d --- /dev/null +++ b/packages/core/src/filesystem/search.ts @@ -0,0 +1,239 @@ +export * as FileSystemSearch from "./search" + +import { makeLocationNode } from "../effect/app-node" +import path from "path" +import { Context, Effect, Layer, Scope } from "effect" +import { Fff } from "#fff" +import fuzzysort from "fuzzysort" +import { FileSystem } from "../filesystem" +import { FSUtil } from "../fs-util" +import { Location } from "../location" +import { Ripgrep } from "../ripgrep" +import { RelativePath } from "../schema" +import { Flag } from "../flag/flag" + +export interface Interface { + readonly find: (input: FileSystem.FindInput) => Effect.Effect + readonly glob: (input: FileSystem.GlobInput) => Effect.Effect + readonly grep: (input: FileSystem.GrepInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/FileSystem/Search") {} + +export const ripgrepLayer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const ripgrep = yield* Ripgrep.Service + const scope = yield* Scope.Scope + const state = { + files: [] as string[], + directories: [] as string[], + } + const directories = new Set() + yield* ripgrep + .find({ + cwd: location.directory, + pattern: "*", + limit: location.vcs ? Number.MAX_SAFE_INTEGER : 100_000, + onEntry: (entry) => + Effect.sync(() => { + state.files.push(entry.path) + const parts = entry.path.split("/") + parts.slice(0, -1).forEach((_, index) => directories.add(parts.slice(0, index + 1).join("/") + path.sep)) + state.directories = Array.from(directories) + }), + }) + .pipe(Effect.orDie, Effect.asVoid, Effect.forkIn(scope)) + return Service.of({ + glob: (input) => + Effect.gen(function* () { + const target = path.resolve(location.directory, input.path ?? ".") + const info = yield* fs.stat(target).pipe(Effect.orDie) + const cwd = info.type === "File" ? path.dirname(target) : target + return yield* ripgrep + .glob({ + cwd, + pattern: input.pattern, + limit: input.limit ?? Number.MAX_SAFE_INTEGER, + }) + .pipe( + Effect.map((result) => + result.map((entry) => + FileSystem.Entry.make({ + ...entry, + path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, entry.path))), + }), + ), + ), + Effect.orDie, + ) + }), + grep: (input) => + Effect.gen(function* () { + const target = path.resolve(location.directory, input.path ?? ".") + const info = yield* fs.stat(target).pipe(Effect.orDie) + const cwd = info.type === "File" ? path.dirname(target) : target + return yield* ripgrep + .grep({ + cwd, + pattern: input.pattern, + file: info.type === "File" ? path.basename(target) : undefined, + include: input.include, + limit: input.limit ?? Number.MAX_SAFE_INTEGER, + }) + .pipe( + Effect.map((result) => + result.map((match) => + FileSystem.Match.make({ + ...match, + entry: FileSystem.Entry.make({ + ...match.entry, + path: RelativePath.make(path.relative(location.directory, path.resolve(cwd, match.entry.path))), + }), + }), + ), + ), + Effect.orDie, + ) + }), + find: (input) => + Effect.gen(function* () { + const items = + input.type === "file" + ? state.files + : input.type === "directory" + ? state.directories + : [...state.files, ...state.directories] + return fuzzysort.go(input.query, items, { limit: input.limit ?? 50 }).map((item) => { + const relative = item.target + const type = relative.endsWith(path.sep) ? ("directory" as const) : ("file" as const) + return FileSystem.Entry.make({ + path: RelativePath.make(relative), + type, + }) + }) + }), + }) + }), +) + +export const fffLayer = Layer.effect( + Service, + Effect.gen(function* () { + const location = yield* Location.Service + const result = yield* Effect.try({ + try: () => + Fff.create({ + basePath: location.directory, + aiMode: true, + disableMmapCache: true, + disableContentIndexing: true, + }), + catch: (cause) => cause, + }).pipe( + Effect.catch((error) => Effect.logWarning("failed to initialize fff", { error }).pipe(Effect.as(undefined))), + ) + if (!result?.ok) { + if (result) yield* Effect.logWarning("failed to initialize fff", { error: result.error }) + return Service.of({ + find: () => Effect.succeed([]), + glob: () => Effect.succeed([]), + grep: () => Effect.succeed([]), + }) + } + yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore)) + return Service.of({ + glob: (input) => + Effect.sync(() => { + const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "") + const found = result.value.glob(prefix ? `${prefix}/${input.pattern}` : input.pattern, { + pageIndex: 0, + pageSize: input.limit, + }) + if (!found.ok) throw found.error + return found.value.items.map((item) => + FileSystem.Entry.make({ + path: RelativePath.make(item.relativePath.replaceAll("\\", "/")), + type: "file", + }), + ) + }), + grep: (input) => + Effect.sync(() => { + const prefix = input.path?.replaceAll("\\", "/").replace(/\/$/, "") + const found = result.value.grep( + [prefix ? `${prefix}/**` : undefined, input.include, input.pattern] + .filter((value) => value !== undefined) + .join(" "), + { mode: "regex", pageSize: input.limit, timeBudgetMs: 1_500 }, + ) + if (!found.ok) throw found.error + return found.value.items.map((match) => { + const bytes = Buffer.from(match.lineContent) + return FileSystem.Match.make({ + entry: FileSystem.Entry.make({ + path: RelativePath.make(match.relativePath.replaceAll("\\", "/")), + type: "file", + }), + line: match.lineNumber, + offset: match.byteOffset, + text: match.lineContent.length > 2_000 ? match.lineContent.slice(0, 2_000) + "..." : match.lineContent, + submatches: match.matchRanges.map(([start, end]) => ({ + text: bytes.subarray(start, end).toString("utf8"), + start, + end, + })), + }) + }) + }), + find: (input) => + Effect.sync(() => { + const options = { pageIndex: 0, pageSize: input.limit ?? 50 } + const items = (() => { + if (input.type === "file") { + const found = result.value.fileSearch(input.query.trim(), options) + if (!found.ok) throw found.error + return found.value.items.map((item, index) => ({ + path: item.relativePath, + type: "file" as const, + score: found.value.scores[index]?.total ?? 0, + })) + } + if (input.type === "directory") { + const found = result.value.directorySearch(input.query.trim(), options) + if (!found.ok) throw found.error + return found.value.items.map((item, index) => ({ + path: item.relativePath, + type: "directory" as const, + score: found.value.scores[index]?.total ?? 0, + })) + } + const found = result.value.mixedSearch(input.query.trim(), options) + if (!found.ok) throw found.error + return found.value.items.map((item, index) => ({ + path: item.item.relativePath, + type: item.type, + score: found.value.scores[index]?.total ?? 0, + })) + })() + return items + .sort((a, b) => b.score - a.score || a.path.length - b.path.length) + .map((item) => { + const relative = item.path.replaceAll("\\", "/").replace(/\/$/, "") + return FileSystem.Entry.make({ + path: RelativePath.make(relative + (item.type === "directory" ? path.sep : "")), + type: item.type, + }) + }) + }), + }) + }), +) + +const layer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer))) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Ripgrep.node] }) diff --git a/packages/core/src/filesystem/watcher.ts b/packages/core/src/filesystem/watcher.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5e20631917e73a240da569c7127b2048af9f977 --- /dev/null +++ b/packages/core/src/filesystem/watcher.ts @@ -0,0 +1,140 @@ +export * as Watcher from "./watcher" + +// @ts-ignore +import { createWrapper } from "@parcel/watcher/wrapper" +import type ParcelWatcher from "@parcel/watcher" +import { makeLocationNode } from "../effect/app-node" +import { Cause, Context, Effect, Layer } from "effect" +import { FileSystemWatcher } from "@opencode-ai/schema/filesystem-watcher" +import path from "path" +import { Config } from "../config" +import { EventV2 } from "../event" +import { Flag } from "../flag/flag" +import { FSUtil } from "../fs-util" +import { Git } from "../git" +import { Location } from "../location" +import { lazy } from "../util/lazy" +import { Ignore } from "./ignore" +import { Protected } from "./protected" + +declare const OPENCODE_LIBC: string | undefined + +const SUBSCRIBE_TIMEOUT_MS = 10_000 + +export const Event = FileSystemWatcher.Event + +const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { + try { + const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC + const binding = require( + `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${libc || "glibc"}` : ""}`, + ) + return createWrapper(binding) as typeof import("@parcel/watcher") + } catch { + return + } +}) + +function getBackend() { + if (process.platform === "win32") return "windows" + if (process.platform === "darwin") return "fs-events" + if (process.platform === "linux") return "inotify" +} + +function protecteds(dir: string) { + return Protected.paths().filter((item) => { + const relative = path.relative(dir, item) + return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative) + }) +} + +export const hasNativeBinding = () => !!watcher() + +export interface Interface {} + +export class Service extends Context.Service()("@opencode/v2/FileWatcher") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + if (yield* Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) return Service.of({}) + + const backend = getBackend() + const location = yield* Location.Service + if (!backend) { + yield* Effect.logError("watcher backend not supported", { + directory: location.directory, + platform: process.platform, + }) + return Service.of({}) + } + + const w = watcher() + if (!w) return Service.of({}) + + yield* Effect.logInfo("watcher backend", { directory: location.directory, platform: process.platform, backend }) + const events = yield* EventV2.Service + const fs = yield* FSUtil.Service + const git = yield* Git.Service + const context = yield* Effect.context() + const runFork = Effect.runForkWith(context) + const subscriptions: ParcelWatcher.AsyncSubscription[] = [] + yield* Effect.addFinalizer(() => + Effect.promise(() => Promise.allSettled(subscriptions.map((subscription) => subscription.unsubscribe()))), + ) + + const callback: ParcelWatcher.SubscribeCallback = (_error, updates) => { + for (const update of updates) { + if (update.type === "create") runFork(events.publish(Event.Updated, { file: update.path, event: "add" })) + if (update.type === "update") runFork(events.publish(Event.Updated, { file: update.path, event: "change" })) + if (update.type === "delete") runFork(events.publish(Event.Updated, { file: update.path, event: "unlink" })) + } + } + + const subscribe = (directory: string, ignore: string[]) => { + const pending = w.subscribe(directory, callback, { ignore, backend }) + return Effect.promise(() => pending).pipe( + Effect.tap((subscription) => Effect.sync(() => subscriptions.push(subscription))), + Effect.timeout(SUBSCRIBE_TIMEOUT_MS), + Effect.catchCause((cause) => { + pending.then((subscription) => subscription.unsubscribe()).catch(() => {}) + return Effect.logError("failed to subscribe", { directory, cause: Cause.pretty(cause) }) + }), + ) + } + + const config = (yield* (yield* Config.Service).entries()) + .filter((entry): entry is Config.Document => entry.type === "document") + .flatMap((item) => item.info.watcher?.ignore ?? []) + if (location.vcs && (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER)) { + yield* Effect.forkScoped( + subscribe(location.directory, [...Ignore.PATTERNS, ...config, ...protecteds(location.directory)]), + ) + } + + if (location.vcs?.type === "git") { + const resolved = (yield* git.repo.discover(location.directory))?.gitDirectory + const vcs = resolved ? yield* fs.realPath(resolved).pipe(Effect.catch(() => Effect.succeed(resolved))) : undefined + if (vcs && !config.includes(".git") && !config.includes(vcs) && (!resolved || !config.includes(resolved))) { + const ignore = (yield* fs.readDirectoryEntries(vcs).pipe(Effect.catch(() => Effect.succeed([])))).flatMap( + (entry) => (entry.name === "HEAD" ? [] : [entry.name]), + ) + yield* Effect.forkScoped(subscribe(vcs, ignore)) + } + } + + return Service.of({}) + }).pipe( + Effect.catchCause((cause) => { + return Effect.logError("failed to init watcher service", { cause: Cause.pretty(cause) }).pipe( + Effect.as(Service.of({})), + ) + }), + ), +) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [FSUtil.node, Location.node, Config.node, Git.node, EventV2.node], +}) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0eb78a13e2a89b68064d9c1678ebbf1432cd38a --- /dev/null +++ b/packages/core/src/flag/flag.ts @@ -0,0 +1,78 @@ +import { Config } from "effect" + +export function truthy(key: string) { + const value = process.env[key]?.toLowerCase() + return value === "true" || value === "1" +} + +const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] +const fff = process.env["OPENCODE_DISABLE_FFF"] + +function enabledByExperimental(key: string) { + return process.env[key] === undefined ? truthy("OPENCODE_EXPERIMENTAL") : truthy(key) +} + +export const Flag = { + OTEL_EXPORTER_OTLP_ENDPOINT: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"], + OTEL_EXPORTER_OTLP_HEADERS: process.env["OTEL_EXPORTER_OTLP_HEADERS"], + + OPENCODE_AUTO_HEAP_SNAPSHOT: truthy("OPENCODE_AUTO_HEAP_SNAPSHOT"), + OPENCODE_GIT_BASH_PATH: process.env["OPENCODE_GIT_BASH_PATH"], + OPENCODE_CONFIG: process.env["OPENCODE_CONFIG"], + OPENCODE_CONFIG_CONTENT: process.env["OPENCODE_CONFIG_CONTENT"], + OPENCODE_DISABLE_AUTOUPDATE: truthy("OPENCODE_DISABLE_AUTOUPDATE"), + OPENCODE_ALWAYS_NOTIFY_UPDATE: truthy("OPENCODE_ALWAYS_NOTIFY_UPDATE"), + OPENCODE_DISABLE_PRUNE: truthy("OPENCODE_DISABLE_PRUNE"), + OPENCODE_DISABLE_TERMINAL_TITLE: truthy("OPENCODE_DISABLE_TERMINAL_TITLE"), + OPENCODE_SHOW_TTFD: truthy("OPENCODE_SHOW_TTFD"), + OPENCODE_DISABLE_AUTOCOMPACT: truthy("OPENCODE_DISABLE_AUTOCOMPACT"), + OPENCODE_DISABLE_MODELS_FETCH: truthy("OPENCODE_DISABLE_MODELS_FETCH"), + OPENCODE_DISABLE_MOUSE: truthy("OPENCODE_DISABLE_MOUSE"), + OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], + OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], + OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], + OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"), + + // Experimental + OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( + Config.withDefault(false), + ), + OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER").pipe( + Config.withDefault(false), + ), + OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT: + copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"), + OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], + OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], + OPENCODE_DB: process.env["OPENCODE_DB"], + + OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"], + OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"), + + // Evaluated at access time (not module load) because tests, the CLI, and + // external tooling set these env vars at runtime. + get OPENCODE_DISABLE_PROJECT_CONFIG() { + return truthy("OPENCODE_DISABLE_PROJECT_CONFIG") + }, + get OPENCODE_EXPERIMENTAL_REFERENCES() { + return enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES") + }, + get OPENCODE_TUI_CONFIG() { + return process.env["OPENCODE_TUI_CONFIG"] + }, + get OPENCODE_CONFIG_DIR() { + return process.env["OPENCODE_CONFIG_DIR"] + }, + get OPENCODE_PURE() { + return truthy("OPENCODE_PURE") + }, + get OPENCODE_PERMISSION() { + return process.env["OPENCODE_PERMISSION"] + }, + get OPENCODE_PLUGIN_META_FILE() { + return process.env["OPENCODE_PLUGIN_META_FILE"] + }, + get OPENCODE_CLIENT() { + return process.env["OPENCODE_CLIENT"] ?? "cli" + }, +} diff --git a/packages/core/src/fs-util.ts b/packages/core/src/fs-util.ts new file mode 100644 index 0000000000000000000000000000000000000000..93a551c9b0a2ac9612ab31424584c3377424e92e --- /dev/null +++ b/packages/core/src/fs-util.ts @@ -0,0 +1,274 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { dirname, isAbsolute, join, relative, resolve as pathResolve, sep } from "path" +import { realpathSync } from "fs" +import * as NFS from "fs/promises" +import { lookup } from "mime-types" +import { Context, Effect, FileSystem, Layer, Schema } from "effect" +import type { PlatformError } from "effect/PlatformError" +import { Glob } from "./util/glob" +import { serviceUse } from "./effect/service-use" +import { makeGlobalNode } from "./effect/app-node" +import { filesystem } from "./effect/app-node-platform" + +export namespace FSUtil { + export class FileSystemError extends Schema.TaggedErrorClass()("FileSystemError", { + method: Schema.String, + cause: Schema.optional(Schema.Defect()), + }) { + override get message() { + const detail = this.cause instanceof Error ? this.cause.message : this.cause && String(this.cause) + return `Filesystem operation failed: ${this.method}${detail ? `: ${detail}` : ""}` + } + } + + export type Error = PlatformError | FileSystemError + + export interface DirEntry { + readonly name: string + readonly type: "file" | "directory" | "symlink" | "other" + } + + export interface Interface extends FileSystem.FileSystem { + readonly isDir: (path: string) => Effect.Effect + readonly isFile: (path: string) => Effect.Effect + readonly existsSafe: (path: string) => Effect.Effect + readonly readFileStringSafe: (path: string) => Effect.Effect + readonly readJson: (path: string) => Effect.Effect + readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect + readonly ensureDir: (path: string) => Effect.Effect + readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect + readonly readDirectoryEntries: (path: string) => Effect.Effect + readonly resolve: (path: string) => Effect.Effect + readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect + readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect + readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect + readonly glob: (pattern: string, options?: Glob.Options) => Effect.Effect + readonly globMatch: (pattern: string, filepath: string) => boolean + } + + export class Service extends Context.Service()("@opencode/FileSystem") {} + + export const use = serviceUse(Service) + + const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + + const existsSafe = Effect.fn("FileSystem.existsSafe")(function* (path: string) { + return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false)) + }) + + const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path: string) { + return yield* fs.readFileString(path).pipe( + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)), + Effect.catchReason("PlatformError", "PermissionDenied", () => Effect.succeed(undefined)), + ) + }) + + const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) { + const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void)) + return info?.type === "Directory" + }) + + const isFile = Effect.fn("FileSystem.isFile")(function* (path: string) { + const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void)) + return info?.type === "File" + }) + + const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) { + return yield* Effect.tryPromise({ + try: async () => { + const entries = await NFS.readdir(dirPath, { withFileTypes: true }) + return entries.map( + (e): DirEntry => ({ + name: e.name, + type: e.isDirectory() ? "directory" : e.isSymbolicLink() ? "symlink" : e.isFile() ? "file" : "other", + }), + ) + }, + catch: (cause) => new FileSystemError({ method: "readDirectoryEntries", cause }), + }) + }) + + const resolve = Effect.fn("FileSystem.resolve")(function* (path: string) { + const resolved = pathResolve(windowsPath(path)) + return yield* fs.realPath(resolved).pipe( + Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(resolved)), + Effect.orDie, + ) + }) + + const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) { + const text = yield* fs.readFileString(path) + return yield* Effect.try({ + try: () => JSON.parse(text), + catch: (cause) => new FileSystemError({ method: "readJson", cause }), + }) + }) + + const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) { + const content = JSON.stringify(data, null, 2) + yield* fs.writeFileString(path, content) + if (mode) yield* fs.chmod(path, mode) + }) + + const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) { + yield* fs.makeDirectory(path, { recursive: true }).pipe( + // Bun on Windows can throw EEXIST here despite recursive mode. + // https://github.com/oven-sh/bun/issues/21901 + Effect.catchIf( + (error) => error.reason._tag === "AlreadyExists", + (error) => isDir(path).pipe(Effect.flatMap((exists) => (exists ? Effect.void : Effect.fail(error)))), + ), + ) + }) + + const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* ( + path: string, + content: string | Uint8Array, + mode?: number, + ) { + const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content) + + yield* write.pipe( + Effect.catchIf( + (e) => e.reason._tag === "NotFound", + () => + Effect.gen(function* () { + yield* fs.makeDirectory(dirname(path), { recursive: true }) + yield* write + }), + ), + ) + if (mode) yield* fs.chmod(path, mode) + }) + + const glob = Effect.fn("FileSystem.glob")(function* (pattern: string, options?: Glob.Options) { + return yield* Effect.tryPromise({ + try: () => Glob.scan(pattern, options), + catch: (cause) => new FileSystemError({ method: "glob", cause }), + }) + }) + + const findUp = Effect.fn("FileSystem.findUp")(function* (target: string, start: string, stop?: string) { + const result: string[] = [] + let current = start + while (true) { + const search = join(current, target) + if (yield* fs.exists(search)) result.push(search) + if (stop === current) break + const parent = dirname(current) + if (parent === current) break + current = parent + } + return result + }) + + const up = Effect.fn("FileSystem.up")(function* (options: { targets: string[]; start: string; stop?: string }) { + const result: string[] = [] + let current = options.start + while (true) { + for (const target of options.targets) { + const search = join(current, target) + if (yield* fs.exists(search)) result.push(search) + } + if (options.stop === current) break + const parent = dirname(current) + if (parent === current) break + current = parent + } + return result + }) + + const globUp = Effect.fn("FileSystem.globUp")(function* (pattern: string, start: string, stop?: string) { + const result: string[] = [] + let current = start + while (true) { + const matches = yield* glob(pattern, { cwd: current, absolute: true, include: "file", dot: true }).pipe( + Effect.catch(() => Effect.succeed([] as string[])), + ) + result.push(...matches) + if (stop === current) break + const parent = dirname(current) + if (parent === current) break + current = parent + } + return result + }) + + return Service.of({ + ...fs, + existsSafe, + readFileStringSafe, + isDir, + isFile, + readDirectoryEntries, + resolve, + readJson, + writeJson, + ensureDir, + writeWithDirs, + findUp, + up, + globUp, + glob, + globMatch: Glob.match, + }) + }), + ) + + export const node = makeGlobalNode({ service: Service, layer: layer, deps: [filesystem] }) + + // Pure helpers that don't need Effect (path manipulation, sync operations) + export function mimeType(p: string): string { + return lookup(p) || "application/octet-stream" + } + + export function normalizePath(p: string): string { + if (process.platform !== "win32") return p + const resolved = pathResolve(windowsPath(p)) + try { + return realpathSync.native(resolved) + } catch { + return resolved + } + } + + export function normalizePathPattern(p: string): string { + if (process.platform !== "win32") return p + if (p === "*") return p + const match = p.match(/^(.*)[\\/]\*$/) + if (!match) return normalizePath(p) + const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1] + return join(normalizePath(dir), "*") + } + + export function resolve(p: string): string { + const resolved = pathResolve(windowsPath(p)) + try { + return normalizePath(realpathSync(resolved)) + } catch (e: any) { + if (e?.code === "ENOENT") return normalizePath(resolved) + throw e + } + } + + export function windowsPath(p: string): string { + if (process.platform !== "win32") return p + return p + .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + .replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`) + } + + export function overlaps(a: string, b: string) { + return contains(a, b) || contains(b, a) + } + + export function contains(parent: string, child: string) { + const result = relative(parent, child) + return result === "" || (!isAbsolute(result) && result !== ".." && !result.startsWith(`..${sep}`)) + } +} diff --git a/packages/core/src/git.ts b/packages/core/src/git.ts new file mode 100644 index 0000000000000000000000000000000000000000..19dcde8961a2f659b1f5e77598ed0db7095ec6c1 --- /dev/null +++ b/packages/core/src/git.ts @@ -0,0 +1,987 @@ +export * as Git from "./git" + +import path from "path" +import { randomUUID } from "crypto" +import { Context, Effect, Layer, Schema, Stream } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { AbsolutePath, RelativePath } from "./schema" +import { FSUtil } from "./fs-util" +import { AppProcess } from "./process" +import { makeGlobalNode } from "./effect/app-node" +import { File } from "./file" +import { KeyedMutex } from "./effect/keyed-mutex" + +export class Repository extends Schema.Class("Git.Repository")({ + worktree: AbsolutePath, + gitDirectory: AbsolutePath, + commonDirectory: AbsolutePath, +}) {} + +export const ChangeSet = Schema.String.pipe(Schema.brand("Git.ChangeSet")) +export type ChangeSet = typeof ChangeSet.Type + +export const TreeID = Schema.String.pipe(Schema.brand("Git.TreeID")) +export type TreeID = typeof TreeID.Type + +export class OperationError extends Schema.TaggedErrorClass()("Git.OperationError", { + operation: Schema.Literals([ + "clone", + "fetch", + "checkout", + "reset", + "create", + "refresh", + "write_tree", + "list_files", + "diff", + "restore", + ]), + message: Schema.String, + directory: Schema.optional(AbsolutePath), + cause: Schema.optional(Schema.Defect()), +}) {} + +export class Worktree extends Schema.Class("Git.Worktree")({ + directory: AbsolutePath, + kind: Schema.Literals(["main", "linked"]), +}) {} + +export class WorktreeError extends Schema.TaggedErrorClass()("Git.WorktreeError", { + operation: Schema.Literals(["create", "remove", "list"]), + message: Schema.String, + directory: Schema.optional(AbsolutePath), + forceRequired: Schema.optional(Schema.Boolean), + cause: Schema.optional(Schema.Defect()), +}) {} + +export class PatchError extends Schema.TaggedErrorClass()("Git.PatchError", { + operation: Schema.Literals(["capture", "apply", "reset"]), + directory: AbsolutePath, + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} + +export interface Interface { + readonly repo: { + readonly discover: (input: AbsolutePath) => Effect.Effect + readonly clone: (input: { + remote: string + directory: AbsolutePath + branch?: string + depth?: number + }) => Effect.Effect + readonly create: (input: { + worktree: AbsolutePath + gitDirectory: AbsolutePath + seed?: Repository + }) => Effect.Effect + } + readonly remote: { + readonly get: (repository: Repository, name?: string) => Effect.Effect + } + readonly history: { + readonly head: (repository: Repository) => Effect.Effect + readonly branch: (repository: Repository) => Effect.Effect + readonly defaultRemoteBranch: (repository: Repository, remote?: string) => Effect.Effect + readonly rootCommits: (repository: Repository) => Effect.Effect + } + readonly sync: { + readonly fetchRemotes: (repository: Repository, input?: { prune?: boolean }) => Effect.Effect + readonly fetchBranch: ( + repository: Repository, + input: { remote?: string; branch: string; force?: boolean }, + ) => Effect.Effect + readonly checkoutRemoteBranch: ( + repository: Repository, + input: { remote?: string; branch: string; reset?: boolean }, + ) => Effect.Effect + readonly resetHard: (repository: Repository, revision: string) => Effect.Effect + } + readonly change: { + readonly capture: (input: { repository: Repository; path: AbsolutePath }) => Effect.Effect + readonly apply: (input: { + repository: Repository + path: AbsolutePath + changes: ChangeSet + }) => Effect.Effect + readonly discard: (input: { + repository: Repository + path: AbsolutePath + index: "preserve" | "reset" + untracked: "preserve" | "remove" + }) => Effect.Effect + } + readonly worktree: { + readonly create: (input: { + repository: Repository + directory: AbsolutePath + }) => Effect.Effect + readonly remove: (input: { + repository: Repository + directory: AbsolutePath + force: boolean + }) => Effect.Effect + readonly list: (repository: Repository) => Effect.Effect + } + readonly index: { + /** Refresh only the requested project-relative scope, preserving all other entries. */ + readonly refresh: (input: { + repository: Repository + scope: RelativePath + ignores?: Repository + maximumUntrackedFileBytes?: number + }) => Effect.Effect<{ readonly skipped: readonly RelativePath[] }, OperationError> + readonly ignored: (input: { + repository: Repository + paths: readonly RelativePath[] + }) => Effect.Effect, OperationError> + } + readonly tree: { + readonly capture: (input: { + repository: Repository + scopes: readonly RelativePath[] + ignores?: Repository + maximumUntrackedFileBytes?: number + }) => Effect.Effect + readonly write: (repository: Repository) => Effect.Effect + readonly files: (input: { + repository: Repository + from: TreeID + to: TreeID + }) => Effect.Effect + readonly diff: (input: { + repository: Repository + from: TreeID + to: TreeID + context?: number + paths?: readonly RelativePath[] + }) => Effect.Effect + readonly preview: (input: { + repository: Repository + current: TreeID + files: ReadonlyMap + context?: number + }) => Effect.Effect + readonly restore: (input: { + repository: Repository + files: ReadonlyMap + }) => Effect.Effect + readonly checkout: (input: { repository: Repository; tree: TreeID }) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/GitV2") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const proc = yield* AppProcess.Service + const locks = KeyedMutex.makeUnsafe() + const locked = (repository: Repository, effect: Effect.Effect) => + locks.withLock(repository.gitDirectory)(effect) + + const discover = Effect.fn("Git.repo.discover")(function* (input: AbsolutePath) { + const dotgit = yield* fs.up({ targets: [".git"], start: input }).pipe( + Effect.map((matches) => matches[0]), + Effect.catch(() => Effect.succeed(undefined)), + ) + if (!dotgit) return undefined + + const cwd = path.dirname(dotgit) + const git = run(cwd, proc) + const topLevel = yield* git(["rev-parse", "--show-toplevel"]) + const gitDir = yield* git(["rev-parse", "--git-dir"]) + const commonDir = yield* git(["rev-parse", "--git-common-dir"]) + if (gitDir.exitCode !== 0 || commonDir.exitCode !== 0) return undefined + + return new Repository({ + worktree: AbsolutePath.make(topLevel.exitCode === 0 ? resolvePath(cwd, topLevel.text) : cwd), + gitDirectory: AbsolutePath.make(resolvePath(cwd, gitDir.text)), + commonDirectory: AbsolutePath.make(resolvePath(cwd, commonDir.text)), + }) + }) + + const remote = Effect.fn("Git.remote.get")(function* (repository: Repository, name = "origin") { + const result = yield* run(repository.worktree, proc)(["remote", "get-url", name]) + if (result.exitCode !== 0) return undefined + return result.text.trim() || undefined + }) + + const roots = Effect.fn("Git.history.rootCommits")(function* (repository: Repository) { + const result = yield* run(repository.worktree, proc)(["rev-list", "--max-parents=0", "HEAD"]) + if (result.exitCode !== 0) return [] + return result.text + .split("\n") + .map((item) => item.trim()) + .filter(Boolean) + .toSorted() + }) + + const head = Effect.fn("Git.history.head")(function* (repository: Repository) { + const result = yield* run(repository.worktree, proc)(["rev-parse", "HEAD"]) + if (result.exitCode !== 0) return undefined + return result.text.trim() || undefined + }) + + const branch = Effect.fn("Git.history.branch")(function* (repository: Repository) { + const result = yield* run(repository.worktree, proc)(["symbolic-ref", "--quiet", "--short", "HEAD"]) + if (result.exitCode !== 0) return undefined + return result.text.trim() || undefined + }) + + const remoteHead = Effect.fn("Git.history.defaultRemoteBranch")(function* ( + repository: Repository, + remoteName = "origin", + ) { + const result = yield* run(repository.worktree, proc)(["symbolic-ref", `refs/remotes/${remoteName}/HEAD`]) + if (result.exitCode !== 0) return undefined + return result.text.trim().replace(new RegExp(`^refs/remotes/${remoteName}/`), "") || undefined + }) + + const operation = Effect.fnUntraced(function* ( + operation: OperationError["operation"], + directory: AbsolutePath, + args: string[], + ) { + const result = yield* execute( + directory, + proc, + )(args).pipe( + Effect.mapError((cause) => new OperationError({ operation, directory, message: cause.message, cause })), + ) + if (result.exitCode === 0) return + return yield* new OperationError({ + operation, + directory, + message: result.stderr.trim() || result.text.trim() || `Git ${operation} failed`, + }) + }) + + const clone = Effect.fn("Git.repo.clone")(function* (input: { + remote: string + directory: AbsolutePath + branch?: string + depth?: number + }) { + yield* operation("clone", AbsolutePath.make(path.dirname(input.directory)), [ + "clone", + "--depth", + String(input.depth ?? 100), + ...(input.branch ? ["--branch", input.branch] : []), + "--", + input.remote, + input.directory, + ]) + const repository = yield* discover(input.directory) + if (repository) return repository + return yield* new OperationError({ + operation: "clone", + directory: input.directory, + message: "Cloned repository could not be opened", + }) + }) + + const fetch = Effect.fn("Git.sync.fetchRemotes")(function* ( + repository: Repository, + input: { prune?: boolean } = {}, + ) { + yield* operation("fetch", repository.worktree, ["fetch", "--all", ...(input.prune === false ? [] : ["--prune"])]) + }) + + const fetchBranch = Effect.fn("Git.sync.fetchBranch")(function* ( + repository: Repository, + input: { remote?: string; branch: string; force?: boolean }, + ) { + const remoteName = input.remote ?? "origin" + const spec = `refs/heads/${input.branch}:refs/remotes/${remoteName}/${input.branch}` + yield* operation("fetch", repository.worktree, ["fetch", remoteName, input.force === false ? spec : `+${spec}`]) + }) + + const checkout = Effect.fn("Git.sync.checkoutRemoteBranch")(function* ( + repository: Repository, + input: { remote?: string; branch: string; reset?: boolean }, + ) { + const remoteName = input.remote ?? "origin" + yield* operation("checkout", repository.worktree, [ + "checkout", + ...(input.reset === false ? [input.branch] : ["-B", input.branch, `${remoteName}/${input.branch}`]), + ]) + }) + + const reset = Effect.fn("Git.sync.resetHard")(function* (repository: Repository, revision: string) { + yield* operation("reset", repository.worktree, ["reset", "--hard", revision]) + }) + + const repositoryArgs = (repository: Repository, args: string[]) => [ + "--git-dir", + repository.gitDirectory, + "--work-tree", + repository.worktree, + ...args, + ] + + const repositoryOperation = Effect.fnUntraced(function* ( + operationName: OperationError["operation"], + repository: Repository, + args: string[], + options?: { stdin?: string; env?: Record }, + ) { + const result = yield* proc + .run( + ChildProcess.make("git", repositoryArgs(repository, args), { + cwd: repository.worktree, + env: options?.env, + extendEnv: true, + }), + { stdin: options?.stdin }, + ) + .pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: operationName, + directory: repository.worktree, + message: cause.message, + cause, + }), + ), + ) + const text = result.stdout.toString("utf8") + if (result.exitCode === 0) return { text, stderr: result.stderr.toString("utf8") } + return yield* new OperationError({ + operation: operationName, + directory: repository.worktree, + message: result.stderr.toString("utf8").trim() || text.trim() || `Git ${operationName} failed`, + }) + }) + + const create = Effect.fn("Git.repo.create")(function* (input: { + worktree: AbsolutePath + gitDirectory: AbsolutePath + seed?: Repository + }) { + yield* fs.ensureDir(input.gitDirectory).pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "create", + directory: input.gitDirectory, + message: "Failed to create Git storage", + cause, + }), + ), + ) + const repository = new Repository({ + worktree: input.worktree, + gitDirectory: input.gitDirectory, + commonDirectory: input.gitDirectory, + }) + yield* repositoryOperation("create", repository, ["init"]) + yield* Effect.forEach( + [ + ["core.autocrlf", "false"], + ["core.longpaths", "true"], + ["core.symlinks", "true"], + ["core.fsmonitor", "false"], + ["feature.manyFiles", "true"], + ["index.version", "4"], + ["index.threads", "true"], + ["core.untrackedCache", "true"], + ], + ([key, value]) => repositoryOperation("create", repository, ["config", key, value]), + { discard: true }, + ) + if (!input.seed) return repository + yield* fs.ensureDir(path.join(input.gitDirectory, "objects", "info")).pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "create", + directory: input.gitDirectory, + message: "Failed to configure shared Git objects", + cause, + }), + ), + ) + yield* fs + .writeFileString( + path.join(input.gitDirectory, "objects", "info", "alternates"), + path.join(input.seed.commonDirectory, "objects") + "\n", + ) + .pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "create", + directory: input.gitDirectory, + message: "Failed to configure shared Git objects", + cause, + }), + ), + ) + yield* fs + .copyFile(path.join(input.seed.gitDirectory, "index"), path.join(input.gitDirectory, "index")) + .pipe(Effect.catch(() => Effect.void)) + return repository + }) + + const refresh = Effect.fn("Git.index.refresh")(function* (input: { + repository: Repository + scope: RelativePath + ignores?: Repository + maximumUntrackedFileBytes?: number + }) { + const list = (args: string[]) => + repositoryOperation("refresh", input.repository, args).pipe( + Effect.map((result) => result.text.split("\0").filter(Boolean)), + ) + const [tracked, untracked] = yield* Effect.all( + [ + list(["diff-files", "--name-only", "-z", "--", input.scope]), + list(["ls-files", "--others", "--exclude-standard", "-z", "--", input.scope]), + ], + { concurrency: 2 }, + ) + const candidates = Array.from(new Set([...tracked, ...untracked])) + if (!candidates.length) return { skipped: [] } + const ignored = input.ignores + ? new Set( + (yield* repositoryOperation("refresh", input.ignores, ["check-ignore", "--no-index", "--stdin", "-z"], { + stdin: candidates.join("\0") + "\0", + }).pipe(Effect.catch(() => Effect.succeed({ text: "", stderr: "" })))).text + .split("\0") + .filter(Boolean), + ) + : new Set() + const allowed = candidates.filter((item) => !ignored.has(item)) + const maximum = input.maximumUntrackedFileBytes + const skipped = maximum + ? (yield* Effect.forEach( + untracked.filter((item) => allowed.includes(item)), + (item) => + fs.stat(path.join(input.repository.worktree, item)).pipe( + Effect.map((info) => + info.type === "File" && Number(info.size) > maximum ? RelativePath.make(item) : undefined, + ), + Effect.catch(() => Effect.succeed(undefined)), + ), + { concurrency: 8 }, + )).filter((item): item is RelativePath => item !== undefined) + : [] + const stage = allowed.filter((item) => !skipped.includes(RelativePath.make(item))) + const remove = [...ignored, ...skipped] + if (remove.length) + yield* repositoryOperation( + "refresh", + input.repository, + ["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"], + { stdin: remove.join("\0") + "\0" }, + ) + if (stage.length) + yield* repositoryOperation( + "refresh", + input.repository, + ["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"], + { stdin: stage.join("\0") + "\0" }, + ) + return { skipped } + }) + + const ignored = Effect.fn("Git.index.ignored")(function* (input: { + repository: Repository + paths: readonly RelativePath[] + }) { + if (!input.paths.length) return new Set() + const result = yield* proc + .run( + ChildProcess.make("git", repositoryArgs(input.repository, ["check-ignore", "--no-index", "--stdin", "-z"]), { + cwd: input.repository.worktree, + extendEnv: true, + }), + { stdin: input.paths.join("\0") + "\0" }, + ) + .pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "list_files", + directory: input.repository.worktree, + message: cause.message, + cause, + }), + ), + ) + if (result.exitCode !== 0 && result.exitCode !== 1) + return yield* new OperationError({ + operation: "list_files", + directory: input.repository.worktree, + message: result.stderr.toString("utf8").trim() || "Failed to check ignored paths", + }) + return new Set( + result.stdout + .toString("utf8") + .split("\0") + .filter(Boolean) + .map((file) => RelativePath.make(file)), + ) + }) + + const writeTree = Effect.fn("Git.tree.write")(function* (repository: Repository) { + return TreeID.make((yield* repositoryOperation("write_tree", repository, ["write-tree"])).text.trim()) + }) + + const captureTree = Effect.fn("Git.tree.capture")( + (input: { + repository: Repository + scopes: readonly RelativePath[] + ignores?: Repository + maximumUntrackedFileBytes?: number + }) => + locked( + input.repository, + Effect.gen(function* () { + yield* Effect.forEach(input.scopes, (scope) => refresh({ ...input, scope }), { discard: true }) + return yield* writeTree(input.repository) + }), + ), + ) + + const treeFiles = Effect.fn("Git.tree.files")(function* (input: { + repository: Repository + from: TreeID + to: TreeID + }) { + return (yield* repositoryOperation("list_files", input.repository, [ + "diff", + "--name-only", + "-z", + input.from, + input.to, + ])).text + .split("\0") + .filter(Boolean) + .map((file) => RelativePath.make(file)) + }) + + const treeDiff = Effect.fn("Git.tree.diff")(function* (input: { + repository: Repository + from: TreeID + to: TreeID + context?: number + paths?: readonly RelativePath[] + }) { + const paths = input.paths ?? (yield* treeFiles(input)) + return yield* Effect.forEach(paths, (file) => + Effect.gen(function* () { + const statusText = (yield* repositoryOperation("diff", input.repository, [ + "diff", + "--name-status", + "--no-renames", + input.from, + input.to, + "--", + file, + ])).text.trim() + const status = statusText.startsWith("A") ? "added" : statusText.startsWith("D") ? "deleted" : "modified" + const stats = (yield* repositoryOperation("diff", input.repository, [ + "diff", + "--numstat", + "--no-renames", + input.from, + input.to, + "--", + file, + ])).text.split("\t") + const binary = stats[0] === "-" || stats[1] === "-" + const patch = binary + ? "" + : (yield* repositoryOperation("diff", input.repository, [ + "diff", + `--unified=${input.context ?? 3}`, + "--no-renames", + input.from, + input.to, + "--", + file, + ])).text + return { + path: file, + status, + additions: binary ? 0 : Number(stats[0] ?? 0), + deletions: binary ? 0 : Number(stats[1] ?? 0), + patch, + } satisfies File.Diff + }), + ) + }) + + const entry = Effect.fnUntraced(function* (repository: Repository, tree: TreeID, file: RelativePath) { + const text = (yield* repositoryOperation("restore", repository, [ + "ls-tree", + "-z", + tree, + "--", + file, + ])).text.replace(/\0$/, "") + if (!text) return + const match = text.match(/^(\d+)\s+\w+\s+([0-9a-f]+)\t/) + if (!match) + return yield* new OperationError({ + operation: "restore", + directory: repository.worktree, + message: `Invalid tree entry for ${file}`, + }) + return { mode: match[1], object: match[2] } + }) + + const preview = Effect.fn("Git.tree.preview")( + (input: { + repository: Repository + current: TreeID + files: ReadonlyMap + context?: number + }) => + locked( + input.repository, + Effect.gen(function* () { + const index = path.join(input.repository.gitDirectory, `preview-${randomUUID()}.index`) + const env = { GIT_INDEX_FILE: index } + return yield* Effect.gen(function* () { + yield* repositoryOperation("diff", input.repository, ["read-tree", input.current], { env }) + yield* Effect.forEach( + input.files, + ([file, tree]) => + Effect.gen(function* () { + const source = yield* entry(input.repository, tree, file) + if (!source) { + yield* repositoryOperation( + "diff", + input.repository, + ["update-index", "--force-remove", "--", file], + { env }, + ) + return + } + yield* repositoryOperation( + "diff", + input.repository, + ["update-index", "--add", "--cacheinfo", source.mode, source.object, file], + { env }, + ) + }), + { discard: true }, + ) + const target = TreeID.make( + (yield* repositoryOperation("diff", input.repository, ["write-tree"], { env })).text.trim(), + ) + return yield* treeDiff({ + repository: input.repository, + from: input.current, + to: target, + context: input.context, + paths: Array.from(input.files.keys()), + }) + }).pipe(Effect.ensuring(fs.remove(index).pipe(Effect.catch(() => Effect.void)))) + }), + ), + ) + + const restore = Effect.fn("Git.tree.restore")( + (input: { repository: Repository; files: ReadonlyMap }) => + locked( + input.repository, + Effect.forEach( + input.files, + ([file, tree]) => + Effect.gen(function* () { + if (yield* entry(input.repository, tree, file)) { + yield* repositoryOperation("restore", input.repository, ["checkout", tree, "--", file]) + return + } + yield* fs.remove(path.join(input.repository.worktree, file), { recursive: true, force: true }).pipe( + Effect.mapError( + (cause) => + new OperationError({ + operation: "restore", + directory: input.repository.worktree, + message: `Failed to remove ${file}`, + cause, + }), + ), + ) + }), + { discard: true }, + ), + ), + ) + + const checkoutTree = Effect.fn("Git.tree.checkout")((input: { repository: Repository; tree: TreeID }) => + locked( + input.repository, + Effect.gen(function* () { + yield* repositoryOperation("restore", input.repository, ["read-tree", input.tree]) + yield* repositoryOperation("restore", input.repository, ["checkout-index", "--all", "--force"]) + }), + ), + ) + + const capture = Effect.fn("Git.change.capture")(function* (input: { repository: Repository; path: AbsolutePath }) { + const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "." + const tracked = yield* execute( + input.repository.worktree, + proc, + )(["diff", "--binary", "HEAD", "--", scope]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }), + ), + ) + if (tracked.exitCode !== 0) { + return yield* new PatchError({ + operation: "capture", + directory: input.path, + message: tracked.stderr.trim() || tracked.text.trim() || "Failed to capture tracked changes", + }) + } + + const untracked = yield* execute( + input.repository.worktree, + proc, + )(["ls-files", "--others", "--exclude-standard", "-z", "--", scope]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }), + ), + ) + if (untracked.exitCode !== 0) { + return yield* new PatchError({ + operation: "capture", + directory: input.path, + message: untracked.stderr.trim() || untracked.text.trim() || "Failed to list untracked changes", + }) + } + + const created = yield* Effect.forEach(untracked.text.split("\0").filter(Boolean), (file) => + execute( + input.repository.worktree, + proc, + )(["diff", "--binary", "--no-index", "--", "/dev/null", file]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "capture", directory: input.path, message: cause.message, cause }), + ), + Effect.flatMap((result) => + // git diff --no-index returns 1 when differences were found. + result.exitCode === 0 || result.exitCode === 1 + ? Effect.succeed(result.text) + : Effect.fail( + new PatchError({ + operation: "capture", + directory: input.path, + message: + result.stderr.trim() || result.text.trim() || `Failed to capture untracked change: ${file}`, + }), + ), + ), + ), + ) + return ChangeSet.make([tracked.text, ...created].filter(Boolean).join("\n")) + }) + + const apply = Effect.fn("Git.change.apply")(function* (input: { + repository: Repository + path: AbsolutePath + changes: ChangeSet + }) { + const result = yield* proc + .run( + ChildProcess.make("git", ["apply", "-"], { + cwd: input.path, + extendEnv: true, + stdin: Stream.make(new TextEncoder().encode(input.changes)), + }), + ) + .pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "apply", directory: input.path, message: cause.message, cause }), + ), + ) + if (result.exitCode === 0) return + return yield* new PatchError({ + operation: "apply", + directory: input.path, + message: + result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Failed to apply changes", + }) + }) + + const discard = Effect.fn("Git.change.discard")(function* (input: { + repository: Repository + path: AbsolutePath + index: "preserve" | "reset" + untracked: "preserve" | "remove" + }) { + const scope = path.relative(input.repository.worktree, input.path).replaceAll("\\", "/") || "." + const restore = yield* execute( + input.repository.worktree, + proc, + )(input.index === "reset" ? ["checkout", "HEAD", "--", scope] : ["checkout", "--", scope]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }), + ), + ) + if (restore.exitCode !== 0) { + return yield* new PatchError({ + operation: "reset", + directory: input.path, + message: restore.stderr.trim() || restore.text.trim() || "Failed to restore tracked changes", + }) + } + if (input.untracked === "preserve") return + const clean = yield* execute( + input.repository.worktree, + proc, + )(["clean", "-fd", "--", scope]).pipe( + Effect.mapError( + (cause) => new PatchError({ operation: "reset", directory: input.path, message: cause.message, cause }), + ), + ) + if (clean.exitCode === 0) return + return yield* new PatchError({ + operation: "reset", + directory: input.path, + message: clean.stderr.trim() || clean.text.trim() || "Failed to clean untracked changes", + }) + }) + + const worktreeRun = Effect.fnUntraced(function* ( + operation: "create" | "remove" | "list", + repository: Repository, + args: string[], + worktreeDirectory?: AbsolutePath, + cwd = repository.worktree, + ) { + const result = yield* proc + .run(ChildProcess.make("git", args, { cwd, extendEnv: true, stdin: "ignore" })) + .pipe( + Effect.mapError( + (cause) => new WorktreeError({ operation, directory: worktreeDirectory, message: cause.message, cause }), + ), + ) + if (result.exitCode === 0) return result.stdout.toString("utf8") + const message = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim() || "Git failed" + return yield* new WorktreeError({ + operation, + directory: worktreeDirectory, + message, + forceRequired: operation === "remove" && /contains modified or untracked files|is dirty/i.test(message), + }) + }) + + const worktreeCreate = Effect.fn("Git.worktree.create")(function* (input: { + repository: Repository + directory: AbsolutePath + }) { + yield* worktreeRun( + "create", + input.repository, + ["worktree", "add", "--detach", input.directory, "HEAD"], + input.directory, + ) + const repository = yield* discover(input.directory) + if (repository) return repository + return yield* new WorktreeError({ + operation: "create", + directory: input.directory, + message: "Created worktree could not be opened", + }) + }) + + const worktreeRemove = Effect.fn("Git.worktree.remove")(function* (input: { + repository: Repository + directory: AbsolutePath + force: boolean + }) { + yield* worktreeRun( + "remove", + input.repository, + ["worktree", "remove", ...(input.force ? ["--force"] : []), input.directory], + input.directory, + input.repository.commonDirectory, + ) + }) + + const worktreeList = Effect.fn("Git.worktree.list")(function* (repository: Repository) { + return (yield* worktreeRun("list", repository, ["worktree", "list", "--porcelain"])) + .split("\n") + .filter((line) => line.startsWith("worktree ")) + .map( + (line, index) => + new Worktree({ + directory: AbsolutePath.make(resolvePath(repository.worktree, line.slice("worktree ".length).trim())), + kind: index === 0 ? "main" : "linked", + }), + ) + }) + + return Service.of({ + repo: { discover, clone, create }, + remote: { get: remote }, + history: { head, branch, defaultRemoteBranch: remoteHead, rootCommits: roots }, + sync: { fetchRemotes: fetch, fetchBranch, checkoutRemoteBranch: checkout, resetHard: reset }, + change: { capture, apply, discard }, + worktree: { create: worktreeCreate, remove: worktreeRemove, list: worktreeList }, + index: { refresh, ignored }, + tree: { + capture: captureTree, + write: writeTree, + files: treeFiles, + diff: treeDiff, + preview, + restore, + checkout: checkoutTree, + }, + }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, AppProcess.node] }) + +interface Result { + readonly exitCode: number + readonly text: string + readonly stderr: string +} + +function run(cwd: string, proc: AppProcess.Interface) { + return (args: string[]) => + execute(cwd, proc)(args).pipe(Effect.catch(() => Effect.succeed({ exitCode: 1, text: "", stderr: "" }))) +} + +function execute(cwd: string, proc: AppProcess.Interface) { + return (args: string[]) => + proc + .run( + ChildProcess.make("git", args, { + cwd, + extendEnv: true, + stdin: "ignore", + }), + ) + .pipe( + Effect.map( + (result) => + ({ + exitCode: result.exitCode, + text: result.stdout.toString("utf8"), + stderr: result.stderr.toString("utf8"), + }) satisfies Result, + ), + ) +} + +function resolvePath(cwd: string, value: string) { + const trimmed = value.replace(/[\r\n]+$/, "") + if (!trimmed) return cwd + const normalized = FSUtil.windowsPath(trimmed) + if (path.isAbsolute(normalized)) return path.normalize(normalized) + return path.resolve(cwd, normalized) +} diff --git a/packages/core/src/github-copilot/README.md b/packages/core/src/github-copilot/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d1051a4da0411e6f4ae288144154e755320b84cd --- /dev/null +++ b/packages/core/src/github-copilot/README.md @@ -0,0 +1,5 @@ +This is a temporary package used primarily for GitHub Copilot compatibility. + +These DO NOT apply for openai-compatible providers or majority of providers supporting completions/responses apis. THIS IS ONLY FOR GITHUB COPILOT!!! + +Avoid making edits to these files diff --git a/packages/core/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts b/packages/core/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4e15e0b4fb29901da556b2c3250b243ccb5d4cf --- /dev/null +++ b/packages/core/src/github-copilot/chat/convert-to-openai-compatible-chat-messages.ts @@ -0,0 +1,170 @@ +import { + type LanguageModelV3Prompt, + type SharedV3ProviderOptions, + UnsupportedFunctionalityError, +} from "@ai-sdk/provider" +import type { OpenAICompatibleChatPrompt } from "./openai-compatible-api-types" +import { convertToBase64 } from "@ai-sdk/provider-utils" + +function getOpenAIMetadata(message: { providerOptions?: SharedV3ProviderOptions }) { + return message?.providerOptions?.copilot ?? {} +} + +export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV3Prompt): OpenAICompatibleChatPrompt { + const messages: OpenAICompatibleChatPrompt = [] + for (const { role, content, ...message } of prompt) { + const metadata = getOpenAIMetadata({ ...message }) + switch (role) { + case "system": { + messages.push({ + role: "system", + content: content, + ...metadata, + }) + break + } + + case "user": { + if (content.length === 1 && content[0].type === "text") { + messages.push({ + role: "user", + content: content[0].text, + ...getOpenAIMetadata(content[0]), + }) + break + } + + messages.push({ + role: "user", + content: content.map((part) => { + const partMetadata = getOpenAIMetadata(part) + switch (part.type) { + case "text": { + return { type: "text", text: part.text, ...partMetadata } + } + case "file": { + if (part.mediaType.startsWith("image/")) { + const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType + + return { + type: "image_url", + image_url: { + url: + part.data instanceof URL + ? part.data.toString() + : `data:${mediaType};base64,${convertToBase64(part.data)}`, + }, + ...partMetadata, + } + } else { + throw new UnsupportedFunctionalityError({ + functionality: `file part media type ${part.mediaType}`, + }) + } + } + } + }), + ...metadata, + }) + + break + } + + case "assistant": { + let text = "" + let reasoningText: string | undefined + let reasoningOpaque: string | undefined + const toolCalls: Array<{ + id: string + type: "function" + function: { name: string; arguments: string } + }> = [] + + for (const part of content) { + const partMetadata = getOpenAIMetadata(part) + // Check for reasoningOpaque on any part (may be attached to text/tool-call) + const partOpaque = (part.providerOptions as { copilot?: { reasoningOpaque?: string } })?.copilot + ?.reasoningOpaque + if (partOpaque && !reasoningOpaque) { + reasoningOpaque = partOpaque + } + + switch (part.type) { + case "text": { + text += part.text + break + } + case "reasoning": { + if (part.text) reasoningText = part.text + break + } + case "tool-call": { + toolCalls.push({ + id: part.toolCallId, + type: "function", + function: { + name: part.toolName, + arguments: JSON.stringify(part.input), + }, + ...partMetadata, + }) + break + } + } + } + + messages.push({ + role: "assistant", + content: text || null, + tool_calls: toolCalls.length > 0 ? toolCalls : undefined, + reasoning_text: reasoningOpaque ? reasoningText : undefined, + reasoning_opaque: reasoningOpaque, + ...metadata, + }) + + break + } + + case "tool": { + for (const toolResponse of content) { + if (toolResponse.type === "tool-approval-response") { + continue + } + const output = toolResponse.output + + let contentValue: string + switch (output.type) { + case "text": + case "error-text": + contentValue = output.value + break + case "execution-denied": + contentValue = output.reason ?? "Tool execution denied." + break + case "content": + case "json": + case "error-json": + contentValue = JSON.stringify(output.value) + break + } + + const toolResponseMetadata = getOpenAIMetadata(toolResponse) + messages.push({ + role: "tool", + tool_call_id: toolResponse.toolCallId, + content: contentValue, + ...toolResponseMetadata, + }) + } + break + } + + default: { + const _exhaustiveCheck: never = role + throw new Error(`Unsupported role: ${_exhaustiveCheck}`) + } + } + } + + return messages +} diff --git a/packages/core/src/github-copilot/chat/get-response-metadata.ts b/packages/core/src/github-copilot/chat/get-response-metadata.ts new file mode 100644 index 0000000000000000000000000000000000000000..708fd968e3a53ba9a2d01faa57deb599bc09e255 --- /dev/null +++ b/packages/core/src/github-copilot/chat/get-response-metadata.ts @@ -0,0 +1,15 @@ +export function getResponseMetadata({ + id, + model, + created, +}: { + id?: string | undefined | null + created?: number | undefined | null + model?: string | undefined | null +}) { + return { + id: id ?? undefined, + modelId: model ?? undefined, + timestamp: created != null ? new Date(created * 1000) : undefined, + } +} diff --git a/packages/core/src/github-copilot/chat/map-openai-compatible-finish-reason.ts b/packages/core/src/github-copilot/chat/map-openai-compatible-finish-reason.ts new file mode 100644 index 0000000000000000000000000000000000000000..7186b62af90acac326c33a118ecd44e45336ec96 --- /dev/null +++ b/packages/core/src/github-copilot/chat/map-openai-compatible-finish-reason.ts @@ -0,0 +1,19 @@ +import type { LanguageModelV3FinishReason } from "@ai-sdk/provider" + +export function mapOpenAICompatibleFinishReason( + finishReason: string | null | undefined, +): LanguageModelV3FinishReason["unified"] { + switch (finishReason) { + case "stop": + return "stop" + case "length": + return "length" + case "content_filter": + return "content-filter" + case "function_call": + case "tool_calls": + return "tool-calls" + default: + return "other" + } +} diff --git a/packages/core/src/github-copilot/chat/openai-compatible-api-types.ts b/packages/core/src/github-copilot/chat/openai-compatible-api-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..c127b05b17e773b02882f34ee7e4c3107231effc --- /dev/null +++ b/packages/core/src/github-copilot/chat/openai-compatible-api-types.ts @@ -0,0 +1,64 @@ +import type { JSONValue } from "@ai-sdk/provider" + +export type OpenAICompatibleChatPrompt = Array + +export type OpenAICompatibleMessage = + | OpenAICompatibleSystemMessage + | OpenAICompatibleUserMessage + | OpenAICompatibleAssistantMessage + | OpenAICompatibleToolMessage + +// Allow for arbitrary additional properties for general purpose +// provider-metadata-specific extensibility. +type JsonRecord = Record + +export interface OpenAICompatibleSystemMessage extends JsonRecord { + role: "system" + content: string | Array +} + +export interface OpenAICompatibleSystemContentPart extends JsonRecord { + type: "text" + text: string +} + +export interface OpenAICompatibleUserMessage extends JsonRecord { + role: "user" + content: string | Array +} + +export type OpenAICompatibleContentPart = OpenAICompatibleContentPartText | OpenAICompatibleContentPartImage + +export interface OpenAICompatibleContentPartImage extends JsonRecord { + type: "image_url" + image_url: { url: string } +} + +export interface OpenAICompatibleContentPartText extends JsonRecord { + type: "text" + text: string +} + +export interface OpenAICompatibleAssistantMessage extends JsonRecord { + role: "assistant" + content?: string | null + tool_calls?: Array + // Copilot-specific reasoning fields + reasoning_text?: string + reasoning_opaque?: string +} + +export interface OpenAICompatibleMessageToolCall extends JsonRecord { + type: "function" + id: string + function: { + arguments: string + name: string + } +} + +export interface OpenAICompatibleToolMessage extends JsonRecord { + role: "tool" + content: string + tool_call_id: string +} diff --git a/packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts b/packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts new file mode 100644 index 0000000000000000000000000000000000000000..280970c41b4f2dace7a49db86735da20144b1f92 --- /dev/null +++ b/packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts @@ -0,0 +1,815 @@ +import { + APICallError, + InvalidResponseDataError, + type LanguageModelV3, + type LanguageModelV3CallOptions, + type LanguageModelV3Content, + type LanguageModelV3StreamPart, + type SharedV3ProviderMetadata, + type SharedV3Warning, +} from "@ai-sdk/provider" +import { + combineHeaders, + createEventSourceResponseHandler, + createJsonErrorResponseHandler, + createJsonResponseHandler, + type FetchFunction, + generateId, + isParsableJson, + parseProviderOptions, + type ParseResult, + postJsonToApi, + type ResponseHandler, +} from "@ai-sdk/provider-utils" +import { z } from "zod/v4" +import { convertToOpenAICompatibleChatMessages } from "./convert-to-openai-compatible-chat-messages" +import { getResponseMetadata } from "./get-response-metadata" +import { mapOpenAICompatibleFinishReason } from "./map-openai-compatible-finish-reason" +import { type OpenAICompatibleChatModelId, openaiCompatibleProviderOptions } from "./openai-compatible-chat-options" +import { defaultOpenAICompatibleErrorStructure, type ProviderErrorStructure } from "../openai-compatible-error" +import type { MetadataExtractor } from "./openai-compatible-metadata-extractor" +import { prepareTools } from "./openai-compatible-prepare-tools" + +export type OpenAICompatibleChatConfig = { + provider: string + headers: () => Record + url: (options: { modelId: string; path: string }) => string + fetch?: FetchFunction + includeUsage?: boolean + errorStructure?: ProviderErrorStructure + metadataExtractor?: MetadataExtractor + + /** + * Whether the model supports structured outputs. + */ + supportsStructuredOutputs?: boolean + + /** + * The supported URLs for the model. + */ + supportedUrls?: () => LanguageModelV3["supportedUrls"] +} + +export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { + readonly specificationVersion = "v3" + + readonly supportsStructuredOutputs: boolean + + readonly modelId: OpenAICompatibleChatModelId + private readonly config: OpenAICompatibleChatConfig + private readonly failedResponseHandler: ResponseHandler + private readonly chunkSchema // type inferred via constructor + + constructor(modelId: OpenAICompatibleChatModelId, config: OpenAICompatibleChatConfig) { + this.modelId = modelId + this.config = config + + // initialize error handling: + const errorStructure = config.errorStructure ?? defaultOpenAICompatibleErrorStructure + this.chunkSchema = createOpenAICompatibleChatChunkSchema(errorStructure.errorSchema) + this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure) + + this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false + } + + get provider(): string { + return this.config.provider + } + + private get providerOptionsName(): string { + return this.config.provider.split(".")[0].trim() + } + + get supportedUrls() { + return this.config.supportedUrls?.() ?? {} + } + + private async getArgs({ + prompt, + maxOutputTokens, + temperature, + topP, + topK, + frequencyPenalty, + presencePenalty, + providerOptions, + stopSequences, + responseFormat, + seed, + toolChoice, + tools, + }: LanguageModelV3CallOptions) { + const warnings: SharedV3Warning[] = [] + + // Parse provider options + const compatibleOptions = Object.assign( + (await parseProviderOptions({ + provider: "copilot", + providerOptions, + schema: openaiCompatibleProviderOptions, + })) ?? {}, + (await parseProviderOptions({ + provider: this.providerOptionsName, + providerOptions, + schema: openaiCompatibleProviderOptions, + })) ?? {}, + ) + + if (topK != null) { + warnings.push({ type: "unsupported", feature: "topK" }) + } + + if (responseFormat?.type === "json" && responseFormat.schema != null && !this.supportsStructuredOutputs) { + warnings.push({ + type: "unsupported", + feature: "responseFormat", + details: "JSON response format schema is only supported with structuredOutputs", + }) + } + + const { + tools: openaiTools, + toolChoice: openaiToolChoice, + toolWarnings, + } = prepareTools({ + tools, + toolChoice, + }) + + return { + args: { + // model id: + model: this.modelId, + + // model specific settings: + user: compatibleOptions.user, + + // standardized settings: + max_tokens: maxOutputTokens, + temperature, + top_p: topP, + frequency_penalty: frequencyPenalty, + presence_penalty: presencePenalty, + response_format: + responseFormat?.type === "json" + ? this.supportsStructuredOutputs === true && responseFormat.schema != null + ? { + type: "json_schema", + json_schema: { + schema: responseFormat.schema, + name: responseFormat.name ?? "response", + description: responseFormat.description, + }, + } + : { type: "json_object" } + : undefined, + + stop: stopSequences, + seed, + ...Object.fromEntries( + Object.entries(providerOptions?.[this.providerOptionsName] ?? {}).filter( + ([key]) => !Object.keys(openaiCompatibleProviderOptions.shape).includes(key), + ), + ), + + reasoning_effort: compatibleOptions.reasoningEffort, + verbosity: compatibleOptions.textVerbosity, + + // messages: + messages: convertToOpenAICompatibleChatMessages(prompt), + + // tools: + tools: openaiTools, + tool_choice: openaiToolChoice, + + // thinking_budget + thinking_budget: compatibleOptions.thinking_budget, + }, + warnings: [...warnings, ...toolWarnings], + } + } + + async doGenerate(options: LanguageModelV3CallOptions) { + const { args, warnings } = await this.getArgs({ ...options }) + + const body = JSON.stringify(args) + + const { + responseHeaders, + value: responseBody, + rawValue: rawResponse, + } = await postJsonToApi({ + url: this.config.url({ + path: "/chat/completions", + modelId: this.modelId, + }), + headers: combineHeaders(this.config.headers(), options.headers), + body: args, + failedResponseHandler: this.failedResponseHandler, + successfulResponseHandler: createJsonResponseHandler(OpenAICompatibleChatResponseSchema), + abortSignal: options.abortSignal, + fetch: this.config.fetch, + }) + + const choice = responseBody.choices[0] + const content: Array = [] + + // text content: + const text = choice.message.content + if (text != null && text.length > 0) { + content.push({ + type: "text", + text, + providerMetadata: choice.message.reasoning_opaque + ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } } + : undefined, + }) + } + + // reasoning content (Copilot uses reasoning_text): + const reasoning = choice.message.reasoning_text + if (reasoning != null && reasoning.length > 0) { + content.push({ + type: "reasoning", + text: reasoning, + // Include reasoning_opaque for Copilot multi-turn reasoning + providerMetadata: choice.message.reasoning_opaque + ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } } + : undefined, + }) + } + + // tool calls: + if (choice.message.tool_calls != null) { + for (const toolCall of choice.message.tool_calls) { + content.push({ + type: "tool-call", + toolCallId: toolCall.id ?? generateId(), + toolName: toolCall.function.name, + input: toolCall.function.arguments!, + providerMetadata: choice.message.reasoning_opaque + ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } } + : undefined, + }) + } + } + + // provider metadata: + const providerMetadata: SharedV3ProviderMetadata = { + [this.providerOptionsName]: {}, + ...(await this.config.metadataExtractor?.extractMetadata?.({ + parsedBody: rawResponse, + })), + } + const completionTokenDetails = responseBody.usage?.completion_tokens_details + if (completionTokenDetails?.accepted_prediction_tokens != null) { + providerMetadata[this.providerOptionsName].acceptedPredictionTokens = + completionTokenDetails?.accepted_prediction_tokens + } + if (completionTokenDetails?.rejected_prediction_tokens != null) { + providerMetadata[this.providerOptionsName].rejectedPredictionTokens = + completionTokenDetails?.rejected_prediction_tokens + } + + return { + content, + finishReason: { + unified: mapOpenAICompatibleFinishReason(choice.finish_reason), + raw: choice.finish_reason ?? undefined, + }, + usage: { + inputTokens: { + total: responseBody.usage?.prompt_tokens ?? undefined, + noCache: undefined, + cacheRead: responseBody.usage?.prompt_tokens_details?.cached_tokens ?? undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: responseBody.usage?.completion_tokens ?? undefined, + text: undefined, + reasoning: responseBody.usage?.completion_tokens_details?.reasoning_tokens ?? undefined, + }, + raw: responseBody.usage ?? undefined, + }, + providerMetadata, + request: { body }, + response: { + ...getResponseMetadata(responseBody), + headers: responseHeaders, + body: rawResponse, + }, + warnings, + } + } + + async doStream(options: LanguageModelV3CallOptions) { + const { args, warnings } = await this.getArgs({ ...options }) + + const body = { + ...args, + stream: true, + + // only include stream_options when in strict compatibility mode: + stream_options: this.config.includeUsage ? { include_usage: true } : undefined, + } + + const metadataExtractor = this.config.metadataExtractor?.createStreamExtractor() + + const { responseHeaders, value: response } = await postJsonToApi({ + url: this.config.url({ + path: "/chat/completions", + modelId: this.modelId, + }), + headers: combineHeaders(this.config.headers(), options.headers), + body, + failedResponseHandler: this.failedResponseHandler, + successfulResponseHandler: createEventSourceResponseHandler(this.chunkSchema), + abortSignal: options.abortSignal, + fetch: this.config.fetch, + }) + + const toolCalls: Array<{ + id: string + type: "function" + function: { + name: string + arguments: string + } + hasFinished: boolean + }> = [] + + let finishReason: { + unified: ReturnType + raw: string | undefined + } = { + unified: "other", + raw: undefined, + } + const usage: { + completionTokens: number | undefined + completionTokensDetails: { + reasoningTokens: number | undefined + acceptedPredictionTokens: number | undefined + rejectedPredictionTokens: number | undefined + } + promptTokens: number | undefined + promptTokensDetails: { + cachedTokens: number | undefined + } + totalTokens: number | undefined + } = { + completionTokens: undefined, + completionTokensDetails: { + reasoningTokens: undefined, + acceptedPredictionTokens: undefined, + rejectedPredictionTokens: undefined, + }, + promptTokens: undefined, + promptTokensDetails: { + cachedTokens: undefined, + }, + totalTokens: undefined, + } + let isFirstChunk = true + const providerOptionsName = this.providerOptionsName + let isActiveReasoning = false + let isActiveText = false + let reasoningOpaque: string | undefined + + return { + stream: response.pipeThrough( + new TransformStream>, LanguageModelV3StreamPart>({ + start(controller) { + controller.enqueue({ type: "stream-start", warnings }) + }, + + // TODO we lost type safety on Chunk, most likely due to the error schema. MUST FIX + transform(chunk, controller) { + // Emit raw chunk if requested (before anything else) + if (options.includeRawChunks) { + controller.enqueue({ type: "raw", rawValue: chunk.rawValue }) + } + + // handle failed chunk parsing / validation: + if (!chunk.success) { + finishReason = { + unified: "error", + raw: undefined, + } + controller.enqueue({ type: "error", error: chunk.error }) + return + } + const value = chunk.value + + metadataExtractor?.processChunk(chunk.rawValue) + + // handle error chunks: + if ("error" in value) { + finishReason = { + unified: "error", + raw: undefined, + } + controller.enqueue({ type: "error", error: value.error.message }) + return + } + + if (isFirstChunk) { + isFirstChunk = false + + controller.enqueue({ + type: "response-metadata", + ...getResponseMetadata(value), + }) + } + + if (value.usage != null) { + const { + prompt_tokens, + completion_tokens, + total_tokens, + prompt_tokens_details, + completion_tokens_details, + } = value.usage + + usage.promptTokens = prompt_tokens ?? undefined + usage.completionTokens = completion_tokens ?? undefined + usage.totalTokens = total_tokens ?? undefined + if (completion_tokens_details?.reasoning_tokens != null) { + usage.completionTokensDetails.reasoningTokens = completion_tokens_details?.reasoning_tokens + } + if (completion_tokens_details?.accepted_prediction_tokens != null) { + usage.completionTokensDetails.acceptedPredictionTokens = + completion_tokens_details?.accepted_prediction_tokens + } + if (completion_tokens_details?.rejected_prediction_tokens != null) { + usage.completionTokensDetails.rejectedPredictionTokens = + completion_tokens_details?.rejected_prediction_tokens + } + if (prompt_tokens_details?.cached_tokens != null) { + usage.promptTokensDetails.cachedTokens = prompt_tokens_details?.cached_tokens + } + } + + const choice = value.choices[0] + + if (choice?.finish_reason != null) { + finishReason = { + unified: mapOpenAICompatibleFinishReason(choice.finish_reason), + raw: choice.finish_reason ?? undefined, + } + } + + if (choice?.delta == null) { + return + } + + const delta = choice.delta + + // Capture reasoning_opaque for Copilot multi-turn reasoning + if (delta.reasoning_opaque) { + if (reasoningOpaque != null) { + throw new InvalidResponseDataError({ + data: delta, + message: + "Multiple reasoning_opaque values received in a single response. Only one thinking part per response is supported.", + }) + } + reasoningOpaque = delta.reasoning_opaque + } + + // enqueue reasoning before text deltas (Copilot uses reasoning_text): + const reasoningContent = delta.reasoning_text + if (reasoningContent) { + if (!isActiveReasoning) { + controller.enqueue({ + type: "reasoning-start", + id: "reasoning-0", + }) + isActiveReasoning = true + } + + controller.enqueue({ + type: "reasoning-delta", + id: "reasoning-0", + delta: reasoningContent, + }) + } + + if (delta.content) { + // If reasoning was active and we're starting text, end reasoning first + // This handles the case where reasoning_opaque and content come in the same chunk + if (isActiveReasoning && !isActiveText) { + controller.enqueue({ + type: "reasoning-end", + id: "reasoning-0", + providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined, + }) + isActiveReasoning = false + } + + if (!isActiveText) { + controller.enqueue({ + type: "text-start", + id: "txt-0", + providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined, + }) + isActiveText = true + } + + controller.enqueue({ + type: "text-delta", + id: "txt-0", + delta: delta.content, + }) + } + + if (delta.tool_calls != null) { + // If reasoning was active and we're starting tool calls, end reasoning first + // This handles the case where reasoning goes directly to tool calls with no content + if (isActiveReasoning) { + controller.enqueue({ + type: "reasoning-end", + id: "reasoning-0", + providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined, + }) + isActiveReasoning = false + } + for (const toolCallDelta of delta.tool_calls) { + const index = toolCallDelta.index + + if (toolCalls[index] == null) { + if (toolCallDelta.id == null) { + throw new InvalidResponseDataError({ + data: toolCallDelta, + message: `Expected 'id' to be a string.`, + }) + } + + if (toolCallDelta.function?.name == null) { + throw new InvalidResponseDataError({ + data: toolCallDelta, + message: `Expected 'function.name' to be a string.`, + }) + } + + controller.enqueue({ + type: "tool-input-start", + id: toolCallDelta.id, + toolName: toolCallDelta.function.name, + }) + + toolCalls[index] = { + id: toolCallDelta.id, + type: "function", + function: { + name: toolCallDelta.function.name, + arguments: toolCallDelta.function.arguments ?? "", + }, + hasFinished: false, + } + + const toolCall = toolCalls[index] + + if (toolCall.function?.name != null && toolCall.function?.arguments != null) { + // send delta if the argument text has already started: + if (toolCall.function.arguments.length > 0) { + controller.enqueue({ + type: "tool-input-delta", + id: toolCall.id, + delta: toolCall.function.arguments, + }) + } + + // check if tool call is complete + // (some providers send the full tool call in one chunk): + if (isParsableJson(toolCall.function.arguments)) { + controller.enqueue({ + type: "tool-input-end", + id: toolCall.id, + }) + + controller.enqueue({ + type: "tool-call", + toolCallId: toolCall.id ?? generateId(), + toolName: toolCall.function.name, + input: toolCall.function.arguments, + providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined, + }) + toolCall.hasFinished = true + } + } + + continue + } + + // existing tool call, merge if not finished + const toolCall = toolCalls[index] + + if (toolCall.hasFinished) { + continue + } + + if (toolCallDelta.function?.arguments != null) { + toolCall.function!.arguments += toolCallDelta.function?.arguments ?? "" + } + + // send delta + controller.enqueue({ + type: "tool-input-delta", + id: toolCall.id, + delta: toolCallDelta.function.arguments ?? "", + }) + + // check if tool call is complete + if ( + toolCall.function?.name != null && + toolCall.function?.arguments != null && + isParsableJson(toolCall.function.arguments) + ) { + controller.enqueue({ + type: "tool-input-end", + id: toolCall.id, + }) + + controller.enqueue({ + type: "tool-call", + toolCallId: toolCall.id ?? generateId(), + toolName: toolCall.function.name, + input: toolCall.function.arguments, + providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined, + }) + toolCall.hasFinished = true + } + } + } + }, + + flush(controller) { + if (isActiveReasoning) { + controller.enqueue({ + type: "reasoning-end", + id: "reasoning-0", + // Include reasoning_opaque for Copilot multi-turn reasoning + providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined, + }) + } + + if (isActiveText) { + controller.enqueue({ type: "text-end", id: "txt-0" }) + } + + // go through all tool calls and send the ones that are not finished + for (const toolCall of toolCalls.filter((toolCall) => !toolCall.hasFinished)) { + controller.enqueue({ + type: "tool-input-end", + id: toolCall.id, + }) + + controller.enqueue({ + type: "tool-call", + toolCallId: toolCall.id ?? generateId(), + toolName: toolCall.function.name, + input: toolCall.function.arguments, + }) + } + + const providerMetadata: SharedV3ProviderMetadata = { + [providerOptionsName]: {}, + // Include reasoning_opaque for Copilot multi-turn reasoning + ...(reasoningOpaque ? { copilot: { reasoningOpaque } } : {}), + ...metadataExtractor?.buildMetadata(), + } + if (usage.completionTokensDetails.acceptedPredictionTokens != null) { + providerMetadata[providerOptionsName].acceptedPredictionTokens = + usage.completionTokensDetails.acceptedPredictionTokens + } + if (usage.completionTokensDetails.rejectedPredictionTokens != null) { + providerMetadata[providerOptionsName].rejectedPredictionTokens = + usage.completionTokensDetails.rejectedPredictionTokens + } + + controller.enqueue({ + type: "finish", + finishReason, + usage: { + inputTokens: { + total: usage.promptTokens, + noCache: + usage.promptTokens != undefined && usage.promptTokensDetails.cachedTokens != undefined + ? usage.promptTokens - usage.promptTokensDetails.cachedTokens + : undefined, + cacheRead: usage.promptTokensDetails.cachedTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: usage.completionTokens, + text: undefined, + reasoning: usage.completionTokensDetails.reasoningTokens, + }, + raw: { + prompt_tokens: usage.promptTokens ?? null, + completion_tokens: usage.completionTokens ?? null, + total_tokens: usage.totalTokens ?? null, + }, + }, + providerMetadata, + }) + }, + }), + ), + request: { body }, + response: { headers: responseHeaders }, + } + } +} + +const openaiCompatibleTokenUsageSchema = z + .object({ + prompt_tokens: z.number().nullish(), + completion_tokens: z.number().nullish(), + total_tokens: z.number().nullish(), + prompt_tokens_details: z + .object({ + cached_tokens: z.number().nullish(), + }) + .nullish(), + completion_tokens_details: z + .object({ + reasoning_tokens: z.number().nullish(), + accepted_prediction_tokens: z.number().nullish(), + rejected_prediction_tokens: z.number().nullish(), + }) + .nullish(), + }) + .nullish() + +// limited version of the schema, focussed on what is needed for the implementation +// this approach limits breakages when the API changes and increases efficiency +const OpenAICompatibleChatResponseSchema = z.object({ + id: z.string().nullish(), + created: z.number().nullish(), + model: z.string().nullish(), + choices: z.array( + z.object({ + message: z.object({ + role: z.literal("assistant").nullish(), + content: z.string().nullish(), + // Copilot-specific reasoning fields + reasoning_text: z.string().nullish(), + reasoning_opaque: z.string().nullish(), + tool_calls: z + .array( + z.object({ + id: z.string().nullish(), + function: z.object({ + name: z.string(), + arguments: z.string(), + }), + }), + ) + .nullish(), + }), + finish_reason: z.string().nullish(), + }), + ), + usage: openaiCompatibleTokenUsageSchema, +}) + +// limited version of the schema, focussed on what is needed for the implementation +// this approach limits breakages when the API changes and increases efficiency +const createOpenAICompatibleChatChunkSchema = (errorSchema: ERROR_SCHEMA) => + z.union([ + z.object({ + id: z.string().nullish(), + created: z.number().nullish(), + model: z.string().nullish(), + choices: z.array( + z.object({ + delta: z + .object({ + role: z.enum(["assistant"]).nullish(), + content: z.string().nullish(), + // Copilot-specific reasoning fields + reasoning_text: z.string().nullish(), + reasoning_opaque: z.string().nullish(), + tool_calls: z + .array( + z.object({ + index: z.number(), + id: z.string().nullish(), + function: z.object({ + name: z.string().nullish(), + arguments: z.string().nullish(), + }), + }), + ) + .nullish(), + }) + .nullish(), + finish_reason: z.string().nullish(), + }), + ), + usage: openaiCompatibleTokenUsageSchema, + }), + errorSchema, + ]) diff --git a/packages/core/src/github-copilot/chat/openai-compatible-chat-options.ts b/packages/core/src/github-copilot/chat/openai-compatible-chat-options.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec5d53fbf9aa4f3a360039dbbd5c2aa2a38e3244 --- /dev/null +++ b/packages/core/src/github-copilot/chat/openai-compatible-chat-options.ts @@ -0,0 +1,28 @@ +import { z } from "zod/v4" + +export type OpenAICompatibleChatModelId = string + +export const openaiCompatibleProviderOptions = z.object({ + /** + * A unique identifier representing your end-user, which can help the provider to + * monitor and detect abuse. + */ + user: z.string().optional(), + + /** + * Reasoning effort for reasoning models. Defaults to `medium`. + */ + reasoningEffort: z.string().optional(), + + /** + * Controls the verbosity of the generated text. Defaults to `medium`. + */ + textVerbosity: z.string().optional(), + + /** + * Copilot thinking_budget used for Anthropic models. + */ + thinking_budget: z.number().optional(), +}) + +export type OpenAICompatibleProviderOptions = z.infer diff --git a/packages/core/src/github-copilot/chat/openai-compatible-metadata-extractor.ts b/packages/core/src/github-copilot/chat/openai-compatible-metadata-extractor.ts new file mode 100644 index 0000000000000000000000000000000000000000..40335f87f61d8b5b16f74207ec31b51a7d2cf412 --- /dev/null +++ b/packages/core/src/github-copilot/chat/openai-compatible-metadata-extractor.ts @@ -0,0 +1,44 @@ +import type { SharedV3ProviderMetadata } from "@ai-sdk/provider" + +/** +Extracts provider-specific metadata from API responses. +Used to standardize metadata handling across different LLM providers while allowing +provider-specific metadata to be captured. +*/ +export type MetadataExtractor = { + /** + * Extracts provider metadata from a complete, non-streaming response. + * + * @param parsedBody - The parsed response JSON body from the provider's API. + * + * @returns Provider-specific metadata or undefined if no metadata is available. + * The metadata should be under a key indicating the provider id. + */ + extractMetadata: ({ parsedBody }: { parsedBody: unknown }) => Promise + + /** + * Creates an extractor for handling streaming responses. The returned object provides + * methods to process individual chunks and build the final metadata from the accumulated + * stream data. + * + * @returns An object with methods to process chunks and build metadata from a stream + */ + createStreamExtractor: () => { + /** + * Process an individual chunk from the stream. Called for each chunk in the response stream + * to accumulate metadata throughout the streaming process. + * + * @param parsedChunk - The parsed JSON response chunk from the provider's API + */ + processChunk(parsedChunk: unknown): void + + /** + * Builds the metadata object after all chunks have been processed. + * Called at the end of the stream to generate the complete provider metadata. + * + * @returns Provider-specific metadata or undefined if no metadata is available. + * The metadata should be under a key indicating the provider id. + */ + buildMetadata(): SharedV3ProviderMetadata | undefined + } +} diff --git a/packages/core/src/github-copilot/chat/openai-compatible-prepare-tools.ts b/packages/core/src/github-copilot/chat/openai-compatible-prepare-tools.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac907f52542666905181160d17f6096b1b751b62 --- /dev/null +++ b/packages/core/src/github-copilot/chat/openai-compatible-prepare-tools.ts @@ -0,0 +1,83 @@ +import { type LanguageModelV3CallOptions, type SharedV3Warning, UnsupportedFunctionalityError } from "@ai-sdk/provider" + +export function prepareTools({ + tools, + toolChoice, +}: { + tools: LanguageModelV3CallOptions["tools"] + toolChoice?: LanguageModelV3CallOptions["toolChoice"] +}): { + tools: + | undefined + | Array<{ + type: "function" + function: { + name: string + description: string | undefined + parameters: unknown + } + }> + toolChoice: { type: "function"; function: { name: string } } | "auto" | "none" | "required" | undefined + toolWarnings: SharedV3Warning[] +} { + // when the tools array is empty, change it to undefined to prevent errors: + tools = tools?.length ? tools : undefined + + const toolWarnings: SharedV3Warning[] = [] + + if (tools == null) { + return { tools: undefined, toolChoice: undefined, toolWarnings } + } + + const openaiCompatTools: Array<{ + type: "function" + function: { + name: string + description: string | undefined + parameters: unknown + } + }> = [] + + for (const tool of tools) { + if (tool.type === "provider") { + toolWarnings.push({ type: "unsupported", feature: `tool type: ${tool.type}` }) + } else { + openaiCompatTools.push({ + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: tool.inputSchema, + }, + }) + } + } + + if (toolChoice == null) { + return { tools: openaiCompatTools, toolChoice: undefined, toolWarnings } + } + + const type = toolChoice.type + + switch (type) { + case "auto": + case "none": + case "required": + return { tools: openaiCompatTools, toolChoice: type, toolWarnings } + case "tool": + return { + tools: openaiCompatTools, + toolChoice: { + type: "function", + function: { name: toolChoice.toolName }, + }, + toolWarnings, + } + default: { + const _exhaustiveCheck: never = type + throw new UnsupportedFunctionalityError({ + functionality: `tool choice type: ${_exhaustiveCheck}`, + }) + } + } +} diff --git a/packages/core/src/github-copilot/copilot-provider.ts b/packages/core/src/github-copilot/copilot-provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9cbb6c7ccb222d9fd158776e757abd1ea60626a --- /dev/null +++ b/packages/core/src/github-copilot/copilot-provider.ts @@ -0,0 +1,100 @@ +import type { LanguageModelV3 } from "@ai-sdk/provider" +import { type FetchFunction, withoutTrailingSlash, withUserAgentSuffix } from "@ai-sdk/provider-utils" +import { OpenAICompatibleChatLanguageModel } from "./chat/openai-compatible-chat-language-model" +import { OpenAIResponsesLanguageModel } from "./responses/openai-responses-language-model" + +// Import the version or define it +const VERSION = "0.1.0" + +export type OpenaiCompatibleModelId = string + +export interface OpenaiCompatibleProviderSettings { + /** + * API key for authenticating requests. + */ + apiKey?: string + + /** + * Base URL for the OpenAI Compatible API calls. + */ + baseURL?: string + + /** + * Name of the provider. + */ + name?: string + + /** + * Custom headers to include in the requests. + */ + headers?: Record + + /** + * Custom fetch implementation. + */ + fetch?: FetchFunction +} + +export interface OpenaiCompatibleProvider { + (modelId: OpenaiCompatibleModelId): LanguageModelV3 + chat(modelId: OpenaiCompatibleModelId): LanguageModelV3 + responses(modelId: OpenaiCompatibleModelId): LanguageModelV3 + languageModel(modelId: OpenaiCompatibleModelId): LanguageModelV3 + + // embeddingModel(modelId: any): EmbeddingModelV2 + + // imageModel(modelId: any): ImageModelV2 +} + +/** + * Create an OpenAI Compatible provider instance. + */ +export function createOpenaiCompatible(options: OpenaiCompatibleProviderSettings = {}): OpenaiCompatibleProvider { + const baseURL = withoutTrailingSlash(options.baseURL ?? "https://api.openai.com/v1") + + if (!baseURL) { + throw new Error("baseURL is required") + } + + // Merge headers: defaults first, then user overrides + const headers = { + // Default OpenAI Compatible headers (can be overridden by user) + ...(options.apiKey && { Authorization: `Bearer ${options.apiKey}` }), + ...options.headers, + } + + const getHeaders = () => withUserAgentSuffix(headers, `ai-sdk/openai-compatible/${VERSION}`) + + const createChatModel = (modelId: OpenaiCompatibleModelId) => { + return new OpenAICompatibleChatLanguageModel(modelId, { + provider: `${options.name ?? "openai-compatible"}.chat`, + headers: getHeaders, + url: ({ path }) => `${baseURL}${path}`, + fetch: options.fetch, + }) + } + + const createResponsesModel = (modelId: OpenaiCompatibleModelId) => { + return new OpenAIResponsesLanguageModel(modelId, { + provider: `${options.name ?? "openai-compatible"}.responses`, + headers: getHeaders, + url: ({ path }) => `${baseURL}${path}`, + fetch: options.fetch, + }) + } + + const createLanguageModel = (modelId: OpenaiCompatibleModelId) => createChatModel(modelId) + + const provider = function (modelId: OpenaiCompatibleModelId) { + return createChatModel(modelId) + } + + provider.languageModel = createLanguageModel + provider.chat = createChatModel + provider.responses = createResponsesModel + + return provider as OpenaiCompatibleProvider +} + +// Default OpenAI Compatible provider instance +export const openaiCompatible = createOpenaiCompatible() diff --git a/packages/core/src/github-copilot/openai-compatible-error.ts b/packages/core/src/github-copilot/openai-compatible-error.ts new file mode 100644 index 0000000000000000000000000000000000000000..054c694dd3050a13918763e2caea9a2e6727f0aa --- /dev/null +++ b/packages/core/src/github-copilot/openai-compatible-error.ts @@ -0,0 +1,27 @@ +import { z, type ZodType } from "zod/v4" + +export const openaiCompatibleErrorDataSchema = z.object({ + error: z.object({ + message: z.string(), + + // The additional information below is handled loosely to support + // OpenAI-compatible providers that have slightly different error + // responses: + type: z.string().nullish(), + param: z.any().nullish(), + code: z.union([z.string(), z.number()]).nullish(), + }), +}) + +export type OpenAICompatibleErrorData = z.infer + +export type ProviderErrorStructure = { + errorSchema: ZodType + errorToMessage: (error: T) => string + isRetryable?: (response: Response, error?: T) => boolean +} + +export const defaultOpenAICompatibleErrorStructure: ProviderErrorStructure = { + errorSchema: openaiCompatibleErrorDataSchema, + errorToMessage: (data) => data.error.message, +} diff --git a/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts b/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e4f86d93355d637048cf16dd0ff5dc10c39ed7e --- /dev/null +++ b/packages/core/src/github-copilot/responses/convert-to-openai-responses-input.ts @@ -0,0 +1,335 @@ +import { + type LanguageModelV3Prompt, + type LanguageModelV3ToolCallPart, + type SharedV3Warning, + UnsupportedFunctionalityError, +} from "@ai-sdk/provider" +import { convertToBase64, parseProviderOptions } from "@ai-sdk/provider-utils" +import { z } from "zod/v4" +import type { OpenAIResponsesInput, OpenAIResponsesReasoning } from "./openai-responses-api-types" +import { localShellInputSchema, localShellOutputSchema } from "./tool/local-shell" + +/** + * Check if a string is a file ID based on the given prefixes + * Returns false if prefixes is undefined (disables file ID detection) + */ +function isFileId(data: string, prefixes?: readonly string[]): boolean { + if (!prefixes) return false + return prefixes.some((prefix) => data.startsWith(prefix)) +} + +export async function convertToOpenAIResponsesInput({ + prompt, + systemMessageMode, + fileIdPrefixes, + store, + hasLocalShellTool = false, +}: { + prompt: LanguageModelV3Prompt + systemMessageMode: "system" | "developer" | "remove" + fileIdPrefixes?: readonly string[] + store: boolean + hasLocalShellTool?: boolean +}): Promise<{ + input: OpenAIResponsesInput + warnings: Array +}> { + const input: OpenAIResponsesInput = [] + const warnings: Array = [] + const processedApprovalIds = new Set() + + for (const { role, content } of prompt) { + switch (role) { + case "system": { + switch (systemMessageMode) { + case "system": { + input.push({ role: "system", content }) + break + } + case "developer": { + input.push({ role: "developer", content }) + break + } + case "remove": { + warnings.push({ + type: "other", + message: "system messages are removed for this model", + }) + break + } + default: { + const _exhaustiveCheck: never = systemMessageMode + throw new Error(`Unsupported system message mode: ${_exhaustiveCheck}`) + } + } + break + } + + case "user": { + input.push({ + role: "user", + content: content.map((part, index) => { + switch (part.type) { + case "text": { + return { type: "input_text", text: part.text } + } + case "file": { + if (part.mediaType.startsWith("image/")) { + const mediaType = part.mediaType === "image/*" ? "image/jpeg" : part.mediaType + + return { + type: "input_image", + ...(part.data instanceof URL + ? { image_url: part.data.toString() } + : typeof part.data === "string" && isFileId(part.data, fileIdPrefixes) + ? { file_id: part.data } + : { + image_url: `data:${mediaType};base64,${convertToBase64(part.data)}`, + }), + detail: part.providerOptions?.copilot?.imageDetail, + } + } else if (part.mediaType === "application/pdf") { + if (part.data instanceof URL) { + return { + type: "input_file", + file_url: part.data.toString(), + } + } + return { + type: "input_file", + ...(typeof part.data === "string" && isFileId(part.data, fileIdPrefixes) + ? { file_id: part.data } + : { + filename: part.filename ?? `part-${index}.pdf`, + file_data: `data:application/pdf;base64,${convertToBase64(part.data)}`, + }), + } + } else { + throw new UnsupportedFunctionalityError({ + functionality: `file part media type ${part.mediaType}`, + }) + } + } + } + }), + }) + + break + } + + case "assistant": { + const reasoningMessages: Record = {} + const toolCallParts: Record = {} + + for (const part of content) { + switch (part.type) { + case "text": { + input.push({ + role: "assistant", + content: [{ type: "output_text", text: part.text }], + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, + }) + break + } + case "tool-call": { + toolCallParts[part.toolCallId] = part + + if (part.providerExecuted) { + break + } + + if (hasLocalShellTool && part.toolName === "local_shell") { + const parsedInput = localShellInputSchema.parse(part.input) + input.push({ + type: "local_shell_call", + call_id: part.toolCallId, + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, + action: { + type: "exec", + command: parsedInput.action.command, + timeout_ms: parsedInput.action.timeoutMs, + user: parsedInput.action.user, + working_directory: parsedInput.action.workingDirectory, + env: parsedInput.action.env, + }, + }) + + break + } + + input.push({ + type: "function_call", + call_id: part.toolCallId, + name: part.toolName, + arguments: JSON.stringify(part.input), + id: (part.providerOptions?.copilot?.itemId as string) ?? undefined, + }) + break + } + + // assistant tool result parts are from provider-executed tools: + case "tool-result": { + if (store) { + // use item references to refer to tool results from built-in tools + input.push({ type: "item_reference", id: part.toolCallId }) + } else { + warnings.push({ + type: "other", + message: `Results for OpenAI tool ${part.toolName} are not sent to the API when store is false`, + }) + } + + break + } + + case "reasoning": { + const providerOptions = await parseProviderOptions({ + provider: "copilot", + providerOptions: part.providerOptions, + schema: openaiResponsesReasoningProviderOptionsSchema, + }) + + const reasoningId = providerOptions?.itemId + + if (reasoningId != null) { + const reasoningMessage = reasoningMessages[reasoningId] + + if (store) { + if (reasoningMessage === undefined) { + // use item references to refer to reasoning (single reference) + input.push({ type: "item_reference", id: reasoningId }) + + // store unused reasoning message to mark id as used + reasoningMessages[reasoningId] = { + type: "reasoning", + id: reasoningId, + summary: [], + } + } + } else { + const summaryParts: Array<{ + type: "summary_text" + text: string + }> = [] + + if (part.text.length > 0) { + summaryParts.push({ + type: "summary_text", + text: part.text, + }) + } else if (reasoningMessage !== undefined) { + warnings.push({ + type: "other", + message: `Cannot append empty reasoning part to existing reasoning sequence. Skipping reasoning part: ${JSON.stringify(part)}.`, + }) + } + + if (reasoningMessage === undefined) { + reasoningMessages[reasoningId] = { + type: "reasoning", + id: reasoningId, + encrypted_content: providerOptions?.reasoningEncryptedContent, + summary: summaryParts, + } + input.push(reasoningMessages[reasoningId]) + } else { + reasoningMessage.summary.push(...summaryParts) + } + } + } else { + warnings.push({ + type: "other", + message: `Non-OpenAI reasoning parts are not supported. Skipping reasoning part: ${JSON.stringify(part)}.`, + }) + } + break + } + } + } + + break + } + + case "tool": { + for (const part of content) { + if (part.type === "tool-approval-response") { + if (processedApprovalIds.has(part.approvalId)) { + continue + } + processedApprovalIds.add(part.approvalId) + + if (store) { + input.push({ + type: "item_reference", + id: part.approvalId, + }) + } + + input.push({ + type: "mcp_approval_response", + approval_request_id: part.approvalId, + approve: part.approved, + }) + continue + } + const output = part.output + + if (output.type === "execution-denied") { + const approvalId = (output.providerOptions?.copilot as { approvalId?: string } | undefined)?.approvalId + + if (approvalId) { + continue + } + } + + if (hasLocalShellTool && part.toolName === "local_shell" && output.type === "json") { + input.push({ + type: "local_shell_call_output", + call_id: part.toolCallId, + output: localShellOutputSchema.parse(output.value).output, + }) + break + } + + let contentValue: string + switch (output.type) { + case "text": + case "error-text": + contentValue = output.value + break + case "execution-denied": + contentValue = output.reason ?? "Tool execution denied." + break + case "content": + case "json": + case "error-json": + contentValue = JSON.stringify(output.value) + break + } + + input.push({ + type: "function_call_output", + call_id: part.toolCallId, + output: contentValue, + }) + } + + break + } + + default: { + const _exhaustiveCheck: never = role + throw new Error(`Unsupported role: ${_exhaustiveCheck}`) + } + } + } + + return { input, warnings } +} + +const openaiResponsesReasoningProviderOptionsSchema = z.object({ + itemId: z.string().nullish(), + reasoningEncryptedContent: z.string().nullish(), +}) + +export type OpenAIResponsesReasoningProviderOptions = z.infer diff --git a/packages/core/src/github-copilot/responses/map-openai-responses-finish-reason.ts b/packages/core/src/github-copilot/responses/map-openai-responses-finish-reason.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f443b511bab9f9d9dbc8f8c9a2a6b3c4c749824 --- /dev/null +++ b/packages/core/src/github-copilot/responses/map-openai-responses-finish-reason.ts @@ -0,0 +1,22 @@ +import type { LanguageModelV3FinishReason } from "@ai-sdk/provider" + +export function mapOpenAIResponseFinishReason({ + finishReason, + hasFunctionCall, +}: { + finishReason: string | null | undefined + // flag that checks if there have been client-side tool calls (not executed by openai) + hasFunctionCall: boolean +}): LanguageModelV3FinishReason["unified"] { + switch (finishReason) { + case undefined: + case null: + return hasFunctionCall ? "tool-calls" : "stop" + case "max_output_tokens": + return "length" + case "content_filter": + return "content-filter" + default: + return hasFunctionCall ? "tool-calls" : "other" + } +} diff --git a/packages/core/src/github-copilot/responses/openai-config.ts b/packages/core/src/github-copilot/responses/openai-config.ts new file mode 100644 index 0000000000000000000000000000000000000000..2241dbb52491a3418bcf82ee1aa510fa9702a5e9 --- /dev/null +++ b/packages/core/src/github-copilot/responses/openai-config.ts @@ -0,0 +1,18 @@ +import type { FetchFunction } from "@ai-sdk/provider-utils" + +export type OpenAIConfig = { + provider: string + url: (options: { modelId: string; path: string }) => string + headers: () => Record + fetch?: FetchFunction + generateId?: () => string + /** + * File ID prefixes used to identify file IDs in Responses API. + * When undefined, all file data is treated as base64 content. + * + * Examples: + * - OpenAI: ['file-'] for IDs like 'file-abc123' + * - Azure OpenAI: ['assistant-'] for IDs like 'assistant-abc123' + */ + fileIdPrefixes?: readonly string[] +} diff --git a/packages/core/src/github-copilot/responses/openai-error.ts b/packages/core/src/github-copilot/responses/openai-error.ts new file mode 100644 index 0000000000000000000000000000000000000000..e78824d36416d6969264d0363f7b27e69c3a5248 --- /dev/null +++ b/packages/core/src/github-copilot/responses/openai-error.ts @@ -0,0 +1,22 @@ +import { z } from "zod/v4" +import { createJsonErrorResponseHandler } from "@ai-sdk/provider-utils" + +export const openaiErrorDataSchema = z.object({ + error: z.object({ + message: z.string(), + + // The additional information below is handled loosely to support + // OpenAI-compatible providers that have slightly different error + // responses: + type: z.string().nullish(), + param: z.any().nullish(), + code: z.union([z.string(), z.number()]).nullish(), + }), +}) + +export type OpenAIErrorData = z.infer + +export const openaiFailedResponseHandler: any = createJsonErrorResponseHandler({ + errorSchema: openaiErrorDataSchema, + errorToMessage: (data) => data.error.message, +}) diff --git a/packages/core/src/github-copilot/responses/openai-responses-api-types.ts b/packages/core/src/github-copilot/responses/openai-responses-api-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..dfdd066750a8617400fafe0a0d519532a43e0559 --- /dev/null +++ b/packages/core/src/github-copilot/responses/openai-responses-api-types.ts @@ -0,0 +1,214 @@ +import type { JSONSchema7 } from "@ai-sdk/provider" + +export type OpenAIResponsesInput = Array + +export type OpenAIResponsesInputItem = + | OpenAIResponsesSystemMessage + | OpenAIResponsesUserMessage + | OpenAIResponsesAssistantMessage + | OpenAIResponsesFunctionCall + | OpenAIResponsesFunctionCallOutput + | OpenAIResponsesComputerCall + | OpenAIResponsesLocalShellCall + | OpenAIResponsesLocalShellCallOutput + | OpenAIResponsesReasoning + | OpenAIResponsesItemReference + | OpenAIResponsesMcpApprovalResponse + +export type OpenAIResponsesIncludeValue = + | "web_search_call.action.sources" + | "code_interpreter_call.outputs" + | "computer_call_output.output.image_url" + | "file_search_call.results" + | "message.input_image.image_url" + | "message.output_text.logprobs" + | "reasoning.encrypted_content" + +export type OpenAIResponsesIncludeOptions = Array | undefined | null + +export type OpenAIResponsesSystemMessage = { + role: "system" | "developer" + content: string +} + +export type OpenAIResponsesUserMessage = { + role: "user" + content: Array< + | { type: "input_text"; text: string } + | { type: "input_image"; image_url: string } + | { type: "input_image"; file_id: string } + | { type: "input_file"; file_url: string } + | { type: "input_file"; filename: string; file_data: string } + | { type: "input_file"; file_id: string } + > +} + +export type OpenAIResponsesAssistantMessage = { + role: "assistant" + content: Array<{ type: "output_text"; text: string }> + id?: string +} + +export type OpenAIResponsesFunctionCall = { + type: "function_call" + call_id: string + name: string + arguments: string + id?: string +} + +export type OpenAIResponsesFunctionCallOutput = { + type: "function_call_output" + call_id: string + output: string +} + +export type OpenAIResponsesComputerCall = { + type: "computer_call" + id: string + status?: string +} + +export type OpenAIResponsesLocalShellCall = { + type: "local_shell_call" + id: string + call_id: string + action: { + type: "exec" + command: string[] + timeout_ms?: number + user?: string + working_directory?: string + env?: Record + } +} + +export type OpenAIResponsesLocalShellCallOutput = { + type: "local_shell_call_output" + call_id: string + output: string +} + +export type OpenAIResponsesItemReference = { + type: "item_reference" + id: string +} + +export type OpenAIResponsesMcpApprovalResponse = { + type: "mcp_approval_response" + approval_request_id: string + approve: boolean +} + +/** + * A filter used to compare a specified attribute key to a given value using a defined comparison operation. + */ +export type OpenAIResponsesFileSearchToolComparisonFilter = { + /** + * The key to compare against the value. + */ + key: string + + /** + * Specifies the comparison operator: eq, ne, gt, gte, lt, lte. + */ + type: "eq" | "ne" | "gt" | "gte" | "lt" | "lte" + + /** + * The value to compare against the attribute key; supports string, number, or boolean types. + */ + value: string | number | boolean +} + +/** + * Combine multiple filters using and or or. + */ +export type OpenAIResponsesFileSearchToolCompoundFilter = { + /** + * Type of operation: and or or. + */ + type: "and" | "or" + + /** + * Array of filters to combine. Items can be ComparisonFilter or CompoundFilter. + */ + filters: Array +} + +export type OpenAIResponsesTool = + | { + type: "function" + name: string + description: string | undefined + parameters: JSONSchema7 + strict: boolean | undefined + } + | { + type: "web_search" + filters: { allowed_domains: string[] | undefined } | undefined + search_context_size: "low" | "medium" | "high" | undefined + user_location: + | { + type: "approximate" + city?: string + country?: string + region?: string + timezone?: string + } + | undefined + } + | { + type: "web_search_preview" + search_context_size: "low" | "medium" | "high" | undefined + user_location: + | { + type: "approximate" + city?: string + country?: string + region?: string + timezone?: string + } + | undefined + } + | { + type: "code_interpreter" + container: string | { type: "auto"; file_ids: string[] | undefined } + } + | { + type: "file_search" + vector_store_ids: string[] + max_num_results: number | undefined + ranking_options: { ranker?: string; score_threshold?: number } | undefined + filters: OpenAIResponsesFileSearchToolComparisonFilter | OpenAIResponsesFileSearchToolCompoundFilter | undefined + } + | { + type: "image_generation" + background: "auto" | "opaque" | "transparent" | undefined + input_fidelity: "low" | "high" | undefined + input_image_mask: + | { + file_id: string | undefined + image_url: string | undefined + } + | undefined + model: string | undefined + moderation: "auto" | undefined + output_compression: number | undefined + output_format: "png" | "jpeg" | "webp" | undefined + partial_images: number | undefined + quality: "auto" | "low" | "medium" | "high" | undefined + size: "auto" | "1024x1024" | "1024x1536" | "1536x1024" | undefined + } + | { + type: "local_shell" + } + +export type OpenAIResponsesReasoning = { + type: "reasoning" + id: string + encrypted_content?: string | null + summary: Array<{ + type: "summary_text" + text: string + }> +} diff --git a/packages/core/src/github-copilot/responses/openai-responses-language-model.ts b/packages/core/src/github-copilot/responses/openai-responses-language-model.ts new file mode 100644 index 0000000000000000000000000000000000000000..8df1dcedadb3bcc4878cb538f5324d34d8113756 --- /dev/null +++ b/packages/core/src/github-copilot/responses/openai-responses-language-model.ts @@ -0,0 +1,1770 @@ +import { + APICallError, + type JSONValue, + type LanguageModelV3, + type LanguageModelV3CallOptions, + type LanguageModelV3Content, + type LanguageModelV3ProviderTool, + type LanguageModelV3StreamPart, + type SharedV3ProviderMetadata, + type SharedV3Warning, +} from "@ai-sdk/provider" +import { + combineHeaders, + createEventSourceResponseHandler, + createJsonResponseHandler, + generateId, + parseProviderOptions, + type ParseResult, + postJsonToApi, +} from "@ai-sdk/provider-utils" +import { z } from "zod/v4" +import type { OpenAIConfig } from "./openai-config" +import { openaiFailedResponseHandler } from "./openai-error" +import { codeInterpreterInputSchema, codeInterpreterOutputSchema } from "./tool/code-interpreter" +import { fileSearchOutputSchema } from "./tool/file-search" +import { imageGenerationOutputSchema } from "./tool/image-generation" +import { convertToOpenAIResponsesInput } from "./convert-to-openai-responses-input" +import { mapOpenAIResponseFinishReason } from "./map-openai-responses-finish-reason" +import type { OpenAIResponsesIncludeOptions, OpenAIResponsesIncludeValue } from "./openai-responses-api-types" +import { prepareResponsesTools } from "./openai-responses-prepare-tools" +import type { OpenAIResponsesModelId } from "./openai-responses-settings" +import { localShellInputSchema } from "./tool/local-shell" + +const webSearchCallItem = z.object({ + type: z.literal("web_search_call"), + id: z.string(), + status: z.string(), + action: z + .discriminatedUnion("type", [ + z.object({ + type: z.literal("search"), + query: z.string().nullish(), + }), + z.object({ + type: z.literal("open_page"), + url: z.string(), + }), + z.object({ + type: z.literal("find"), + url: z.string(), + pattern: z.string(), + }), + ]) + .nullish(), +}) + +const fileSearchCallItem = z.object({ + type: z.literal("file_search_call"), + id: z.string(), + queries: z.array(z.string()), + results: z + .array( + z.object({ + attributes: z.record(z.string(), z.unknown()), + file_id: z.string(), + filename: z.string(), + score: z.number(), + text: z.string(), + }), + ) + .nullish(), +}) + +const codeInterpreterCallItem = z.object({ + type: z.literal("code_interpreter_call"), + id: z.string(), + code: z.string().nullable(), + container_id: z.string(), + outputs: z + .array( + z.discriminatedUnion("type", [ + z.object({ type: z.literal("logs"), logs: z.string() }), + z.object({ type: z.literal("image"), url: z.string() }), + ]), + ) + .nullable(), +}) + +const localShellCallItem = z.object({ + type: z.literal("local_shell_call"), + id: z.string(), + call_id: z.string(), + action: z.object({ + type: z.literal("exec"), + command: z.array(z.string()), + timeout_ms: z.number().optional(), + user: z.string().optional(), + working_directory: z.string().optional(), + env: z.record(z.string(), z.string()).optional(), + }), +}) + +const imageGenerationCallItem = z.object({ + type: z.literal("image_generation_call"), + id: z.string(), + result: z.string(), +}) + +/** + * `top_logprobs` request body argument can be set to an integer between + * 0 and 20 specifying the number of most likely tokens to return at each + * token position, each with an associated log probability. + * + * @see https://platform.openai.com/docs/api-reference/responses/create#responses_create-top_logprobs + */ +const TOP_LOGPROBS_MAX = 20 + +const LOGPROBS_SCHEMA = z.array( + z.object({ + token: z.string(), + logprob: z.number(), + top_logprobs: z.array( + z.object({ + token: z.string(), + logprob: z.number(), + }), + ), + }), +) + +export class OpenAIResponsesLanguageModel implements LanguageModelV3 { + readonly specificationVersion = "v3" + + readonly modelId: OpenAIResponsesModelId + + private readonly config: OpenAIConfig + + constructor(modelId: OpenAIResponsesModelId, config: OpenAIConfig) { + this.modelId = modelId + this.config = config + } + + readonly supportedUrls: Record = { + "image/*": [/^https?:\/\/.*$/], + "application/pdf": [/^https?:\/\/.*$/], + } + + get provider(): string { + return this.config.provider + } + + private async getArgs({ + maxOutputTokens, + temperature, + stopSequences, + topP, + topK, + presencePenalty, + frequencyPenalty, + seed, + prompt, + providerOptions, + tools, + toolChoice, + responseFormat, + }: LanguageModelV3CallOptions) { + const warnings: SharedV3Warning[] = [] + const modelConfig = getResponsesModelConfig(this.modelId) + + if (topK != null) { + warnings.push({ type: "unsupported", feature: "topK" }) + } + + if (seed != null) { + warnings.push({ type: "unsupported", feature: "seed" }) + } + + if (presencePenalty != null) { + warnings.push({ + type: "unsupported", + feature: "presencePenalty", + }) + } + + if (frequencyPenalty != null) { + warnings.push({ + type: "unsupported", + feature: "frequencyPenalty", + }) + } + + if (stopSequences != null) { + warnings.push({ type: "unsupported", feature: "stopSequences" }) + } + + const openaiOptions = await parseProviderOptions({ + provider: "copilot", + providerOptions, + schema: openaiResponsesProviderOptionsSchema, + }) + + const { input, warnings: inputWarnings } = await convertToOpenAIResponsesInput({ + prompt, + systemMessageMode: modelConfig.systemMessageMode, + fileIdPrefixes: this.config.fileIdPrefixes, + store: openaiOptions?.store ?? true, + hasLocalShellTool: hasOpenAITool("openai.local_shell"), + }) + + warnings.push(...inputWarnings) + + const strictJsonSchema = openaiOptions?.strictJsonSchema ?? false + + let include: OpenAIResponsesIncludeOptions = openaiOptions?.include + + function addInclude(key: OpenAIResponsesIncludeValue) { + include = include != null ? [...include, key] : [key] + } + + function hasOpenAITool(id: string) { + return tools?.find((tool) => tool.type === "provider" && tool.id === id) != null + } + + // when logprobs are requested, automatically include them: + const topLogprobs = + typeof openaiOptions?.logprobs === "number" + ? openaiOptions?.logprobs + : openaiOptions?.logprobs === true + ? TOP_LOGPROBS_MAX + : undefined + + if (topLogprobs) { + addInclude("message.output_text.logprobs") + } + + // when a web search tool is present, automatically include the sources: + const webSearchToolName = ( + tools?.find( + (tool) => + tool.type === "provider" && (tool.id === "openai.web_search" || tool.id === "openai.web_search_preview"), + ) as LanguageModelV3ProviderTool | undefined + )?.name + + if (webSearchToolName) { + addInclude("web_search_call.action.sources") + } + + // when a code interpreter tool is present, automatically include the outputs: + if (hasOpenAITool("openai.code_interpreter")) { + addInclude("code_interpreter_call.outputs") + } + + const baseArgs = { + model: this.modelId, + input, + temperature, + top_p: topP, + max_output_tokens: maxOutputTokens, + + ...((responseFormat?.type === "json" || openaiOptions?.textVerbosity) && { + text: { + ...(responseFormat?.type === "json" && { + format: + responseFormat.schema != null + ? { + type: "json_schema", + strict: strictJsonSchema, + name: responseFormat.name ?? "response", + description: responseFormat.description, + schema: responseFormat.schema, + } + : { type: "json_object" }, + }), + ...(openaiOptions?.textVerbosity && { + verbosity: openaiOptions.textVerbosity, + }), + }, + }), + + // provider options: + max_tool_calls: openaiOptions?.maxToolCalls, + metadata: openaiOptions?.metadata, + parallel_tool_calls: openaiOptions?.parallelToolCalls, + previous_response_id: openaiOptions?.previousResponseId, + store: openaiOptions?.store, + user: openaiOptions?.user, + instructions: openaiOptions?.instructions, + service_tier: openaiOptions?.serviceTier, + include, + prompt_cache_key: openaiOptions?.promptCacheKey, + safety_identifier: openaiOptions?.safetyIdentifier, + top_logprobs: topLogprobs, + + // model-specific settings: + ...(modelConfig.isReasoningModel && + (openaiOptions?.reasoningEffort != null || openaiOptions?.reasoningSummary != null) && { + reasoning: { + ...(openaiOptions?.reasoningEffort != null && { + effort: openaiOptions.reasoningEffort, + }), + ...(openaiOptions?.reasoningSummary != null && { + summary: openaiOptions.reasoningSummary, + }), + }, + }), + ...(modelConfig.requiredAutoTruncation && { + truncation: "auto", + }), + } + + if (modelConfig.isReasoningModel) { + // remove unsupported settings for reasoning models + // see https://platform.openai.com/docs/guides/reasoning#limitations + if (baseArgs.temperature != null) { + baseArgs.temperature = undefined + warnings.push({ + type: "unsupported", + feature: "temperature", + details: "temperature is not supported for reasoning models", + }) + } + + if (baseArgs.top_p != null) { + baseArgs.top_p = undefined + warnings.push({ + type: "unsupported", + feature: "topP", + details: "topP is not supported for reasoning models", + }) + } + } else { + if (openaiOptions?.reasoningEffort != null) { + warnings.push({ + type: "unsupported", + feature: "reasoningEffort", + details: "reasoningEffort is not supported for non-reasoning models", + }) + } + + if (openaiOptions?.reasoningSummary != null) { + warnings.push({ + type: "unsupported", + feature: "reasoningSummary", + details: "reasoningSummary is not supported for non-reasoning models", + }) + } + } + + // Validate flex processing support + if (openaiOptions?.serviceTier === "flex" && !modelConfig.supportsFlexProcessing) { + warnings.push({ + type: "unsupported", + feature: "serviceTier", + details: "flex processing is only available for o3, o4-mini, and gpt-5 models", + }) + // Remove from args if not supported + baseArgs.service_tier = undefined + } + + // Validate priority processing support + if (openaiOptions?.serviceTier === "priority" && !modelConfig.supportsPriorityProcessing) { + warnings.push({ + type: "unsupported", + feature: "serviceTier", + details: + "priority processing is only available for supported models (gpt-4, gpt-5, gpt-5-mini, o3, o4-mini) and requires Enterprise access. gpt-5-nano is not supported", + }) + // Remove from args if not supported + baseArgs.service_tier = undefined + } + + const { + tools: openaiTools, + toolChoice: openaiToolChoice, + toolWarnings, + } = prepareResponsesTools({ + tools, + toolChoice, + strictJsonSchema, + }) + + return { + webSearchToolName, + args: { + ...baseArgs, + tools: openaiTools, + tool_choice: openaiToolChoice, + }, + warnings: [...warnings, ...toolWarnings], + } + } + + async doGenerate(options: LanguageModelV3CallOptions) { + const { args: body, warnings, webSearchToolName } = await this.getArgs(options) + const url = this.config.url({ + path: "/responses", + modelId: this.modelId, + }) + + const { + responseHeaders, + value: response, + rawValue: rawResponse, + } = await postJsonToApi({ + url, + headers: combineHeaders(this.config.headers(), options.headers), + body, + failedResponseHandler: openaiFailedResponseHandler, + successfulResponseHandler: createJsonResponseHandler( + z.object({ + id: z.string(), + created_at: z.number(), + error: z + .object({ + code: z.string(), + message: z.string(), + }) + .nullish(), + model: z.string(), + output: z.array( + z.discriminatedUnion("type", [ + z.object({ + type: z.literal("message"), + role: z.literal("assistant"), + id: z.string(), + content: z.array( + z.object({ + type: z.literal("output_text"), + text: z.string(), + logprobs: LOGPROBS_SCHEMA.nullish(), + annotations: z.array( + z.discriminatedUnion("type", [ + z.object({ + type: z.literal("url_citation"), + start_index: z.number(), + end_index: z.number(), + url: z.string(), + title: z.string(), + }), + z.object({ + type: z.literal("file_citation"), + file_id: z.string(), + filename: z.string().nullish(), + index: z.number().nullish(), + start_index: z.number().nullish(), + end_index: z.number().nullish(), + quote: z.string().nullish(), + }), + z.object({ + type: z.literal("container_file_citation"), + }), + ]), + ), + }), + ), + }), + webSearchCallItem, + fileSearchCallItem, + codeInterpreterCallItem, + imageGenerationCallItem, + localShellCallItem, + z.object({ + type: z.literal("function_call"), + call_id: z.string(), + name: z.string(), + arguments: z.string(), + id: z.string(), + }), + z.object({ + type: z.literal("computer_call"), + id: z.string(), + status: z.string().optional(), + }), + z.object({ + type: z.literal("reasoning"), + id: z.string(), + encrypted_content: z.string().nullish(), + summary: z.array( + z.object({ + type: z.literal("summary_text"), + text: z.string(), + }), + ), + }), + ]), + ), + service_tier: z.string().nullish(), + incomplete_details: z.object({ reason: z.string() }).nullish(), + usage: usageSchema, + }), + ), + abortSignal: options.abortSignal, + fetch: this.config.fetch, + }) + + if (response.error) { + throw new APICallError({ + message: response.error.message, + url, + requestBodyValues: body, + statusCode: 400, + responseHeaders, + responseBody: rawResponse as string, + isRetryable: false, + }) + } + + const content: Array = [] + const logprobs: Array> = [] + + // flag that checks if there have been client-side tool calls (not executed by openai) + let hasFunctionCall = false + + // map response content to content array + for (const part of response.output) { + switch (part.type) { + case "reasoning": { + // when there are no summary parts, we need to add an empty reasoning part: + if (part.summary.length === 0) { + part.summary.push({ type: "summary_text", text: "" }) + } + + for (const summary of part.summary) { + content.push({ + type: "reasoning" as const, + text: summary.text, + providerMetadata: { + copilot: { + itemId: part.id, + reasoningEncryptedContent: part.encrypted_content ?? null, + }, + }, + }) + } + break + } + + case "image_generation_call": { + content.push({ + type: "tool-call", + toolCallId: part.id, + toolName: "image_generation", + input: "{}", + providerExecuted: true, + }) + + content.push({ + type: "tool-result", + toolCallId: part.id, + toolName: "image_generation", + result: { + result: part.result, + } satisfies z.infer, + }) + + break + } + + case "local_shell_call": { + content.push({ + type: "tool-call", + toolCallId: part.call_id, + toolName: "local_shell", + input: JSON.stringify({ action: part.action } satisfies z.infer), + providerMetadata: { + copilot: { + itemId: part.id, + }, + }, + }) + + break + } + + case "message": { + for (const contentPart of part.content) { + if (options.providerOptions?.copilot?.logprobs && contentPart.logprobs) { + logprobs.push(contentPart.logprobs) + } + + content.push({ + type: "text", + text: contentPart.text, + providerMetadata: { + copilot: { + itemId: part.id, + }, + }, + }) + + for (const annotation of contentPart.annotations) { + if (annotation.type === "url_citation") { + content.push({ + type: "source", + sourceType: "url", + id: this.config.generateId?.() ?? generateId(), + url: annotation.url, + title: annotation.title, + }) + } else if (annotation.type === "file_citation") { + content.push({ + type: "source", + sourceType: "document", + id: this.config.generateId?.() ?? generateId(), + mediaType: "text/plain", + title: annotation.quote ?? annotation.filename ?? "Document", + filename: annotation.filename ?? annotation.file_id, + }) + } + } + } + + break + } + + case "function_call": { + hasFunctionCall = true + + content.push({ + type: "tool-call", + toolCallId: part.call_id, + toolName: part.name, + input: part.arguments, + providerMetadata: { + copilot: { + itemId: part.id, + }, + }, + }) + break + } + + case "web_search_call": { + content.push({ + type: "tool-call", + toolCallId: part.id, + toolName: webSearchToolName ?? "web_search", + input: JSON.stringify({ action: part.action }), + providerExecuted: true, + }) + + content.push({ + type: "tool-result", + toolCallId: part.id, + toolName: webSearchToolName ?? "web_search", + result: { status: part.status }, + }) + + break + } + + case "computer_call": { + content.push({ + type: "tool-call", + toolCallId: part.id, + toolName: "computer_use", + input: "", + providerExecuted: true, + }) + + content.push({ + type: "tool-result", + toolCallId: part.id, + toolName: "computer_use", + result: { + type: "computer_use_tool_result", + status: part.status || "completed", + }, + }) + break + } + + case "file_search_call": { + content.push({ + type: "tool-call", + toolCallId: part.id, + toolName: "file_search", + input: "{}", + providerExecuted: true, + }) + + content.push({ + type: "tool-result", + toolCallId: part.id, + toolName: "file_search", + result: { + queries: part.queries, + results: + part.results?.map((result) => ({ + attributes: result.attributes as Record, + fileId: result.file_id, + filename: result.filename, + score: result.score, + text: result.text, + })) ?? null, + } satisfies z.infer, + }) + break + } + + case "code_interpreter_call": { + content.push({ + type: "tool-call", + toolCallId: part.id, + toolName: "code_interpreter", + input: JSON.stringify({ + code: part.code, + containerId: part.container_id, + } satisfies z.infer), + providerExecuted: true, + }) + + content.push({ + type: "tool-result", + toolCallId: part.id, + toolName: "code_interpreter", + result: { + outputs: part.outputs, + } satisfies z.infer, + }) + break + } + } + } + + const providerMetadata: SharedV3ProviderMetadata = { + copilot: { responseId: response.id }, + } + + if (logprobs.length > 0) { + providerMetadata.copilot.logprobs = logprobs + } + + if (typeof response.service_tier === "string") { + providerMetadata.copilot.serviceTier = response.service_tier + } + + return { + content, + finishReason: { + unified: mapOpenAIResponseFinishReason({ + finishReason: response.incomplete_details?.reason, + hasFunctionCall, + }), + raw: response.incomplete_details?.reason, + }, + usage: { + inputTokens: { + total: response.usage.input_tokens, + noCache: + response.usage.input_tokens_details?.cached_tokens != null + ? response.usage.input_tokens - response.usage.input_tokens_details.cached_tokens + : undefined, + cacheRead: response.usage.input_tokens_details?.cached_tokens ?? undefined, + cacheWrite: undefined, + }, + outputTokens: { + total: response.usage.output_tokens, + text: undefined, + reasoning: response.usage.output_tokens_details?.reasoning_tokens ?? undefined, + }, + raw: response.usage, + }, + request: { body }, + response: { + id: response.id, + timestamp: new Date(response.created_at * 1000), + modelId: response.model, + headers: responseHeaders, + body: rawResponse, + }, + providerMetadata, + warnings, + } + } + + async doStream(options: LanguageModelV3CallOptions) { + const { args: body, warnings, webSearchToolName } = await this.getArgs(options) + + const { responseHeaders, value: response } = await postJsonToApi({ + url: this.config.url({ + path: "/responses", + modelId: this.modelId, + }), + headers: combineHeaders(this.config.headers(), options.headers), + body: { + ...body, + stream: true, + }, + failedResponseHandler: openaiFailedResponseHandler, + successfulResponseHandler: createEventSourceResponseHandler(openaiResponsesChunkSchema), + abortSignal: options.abortSignal, + fetch: this.config.fetch, + }) + + // oxlint-disable-next-line no-this-alias -- needed for closure scope inside generator + const self = this + + let finishReason: { + unified: ReturnType + raw: string | undefined + } = { + unified: "other", + raw: undefined, + } + const usage: { + inputTokens: number | undefined + outputTokens: number | undefined + totalTokens: number | undefined + reasoningTokens: number | undefined + cachedInputTokens: number | undefined + } = { + inputTokens: undefined, + outputTokens: undefined, + totalTokens: undefined, + reasoningTokens: undefined, + cachedInputTokens: undefined, + } + const logprobs: Array> = [] + let responseId: string | null = null + const ongoingToolCalls: Record< + number, + | { + toolName: string + toolCallId: string + codeInterpreter?: { + containerId: string + } + } + | undefined + > = {} + + // flag that checks if there have been client-side tool calls (not executed by openai) + let hasFunctionCall = false + + // Track reasoning by output_index instead of item_id + // GitHub Copilot rotates encrypted item IDs on every event + const activeReasoning: Record< + number, + { + canonicalId: string // the item.id from output_item.added + encryptedContent?: string | null + summaryParts: number[] + } + > = {} + + // Track current active reasoning output_index for correlating summary events + let currentReasoningOutputIndex: number | null = null + + // Track a stable text part id for the current assistant message. + // Copilot may change item_id across text deltas; normalize to one id. + let currentTextId: string | null = null + + let serviceTier: string | undefined + + return { + stream: response.pipeThrough( + new TransformStream>, LanguageModelV3StreamPart>({ + start(controller) { + controller.enqueue({ type: "stream-start", warnings }) + }, + + transform(chunk, controller) { + if (options.includeRawChunks) { + controller.enqueue({ type: "raw", rawValue: chunk.rawValue }) + } + + // handle failed chunk parsing / validation: + if (!chunk.success) { + finishReason = { + unified: "error", + raw: undefined, + } + controller.enqueue({ type: "error", error: chunk.error }) + return + } + + const value = chunk.value + + if (isResponseOutputItemAddedChunk(value)) { + if (value.item.type === "function_call") { + ongoingToolCalls[value.output_index] = { + toolName: value.item.name, + toolCallId: value.item.call_id, + } + + controller.enqueue({ + type: "tool-input-start", + id: value.item.call_id, + toolName: value.item.name, + }) + } else if (value.item.type === "web_search_call") { + ongoingToolCalls[value.output_index] = { + toolName: webSearchToolName ?? "web_search", + toolCallId: value.item.id, + } + + controller.enqueue({ + type: "tool-input-start", + id: value.item.id, + toolName: webSearchToolName ?? "web_search", + }) + } else if (value.item.type === "computer_call") { + ongoingToolCalls[value.output_index] = { + toolName: "computer_use", + toolCallId: value.item.id, + } + + controller.enqueue({ + type: "tool-input-start", + id: value.item.id, + toolName: "computer_use", + }) + } else if (value.item.type === "code_interpreter_call") { + ongoingToolCalls[value.output_index] = { + toolName: "code_interpreter", + toolCallId: value.item.id, + codeInterpreter: { + containerId: value.item.container_id, + }, + } + + controller.enqueue({ + type: "tool-input-start", + id: value.item.id, + toolName: "code_interpreter", + }) + + controller.enqueue({ + type: "tool-input-delta", + id: value.item.id, + delta: `{"containerId":"${value.item.container_id}","code":"`, + }) + } else if (value.item.type === "file_search_call") { + controller.enqueue({ + type: "tool-call", + toolCallId: value.item.id, + toolName: "file_search", + input: "{}", + providerExecuted: true, + }) + } else if (value.item.type === "image_generation_call") { + controller.enqueue({ + type: "tool-call", + toolCallId: value.item.id, + toolName: "image_generation", + input: "{}", + providerExecuted: true, + }) + } else if (value.item.type === "message") { + // Start a stable text part for this assistant message + currentTextId = value.item.id + controller.enqueue({ + type: "text-start", + id: value.item.id, + providerMetadata: { + copilot: { + itemId: value.item.id, + }, + }, + }) + } else if (isResponseOutputItemAddedReasoningChunk(value)) { + activeReasoning[value.output_index] = { + canonicalId: value.item.id, + encryptedContent: value.item.encrypted_content, + summaryParts: [0], + } + currentReasoningOutputIndex = value.output_index + + controller.enqueue({ + type: "reasoning-start", + id: `${value.item.id}:0`, + providerMetadata: { + copilot: { + itemId: value.item.id, + reasoningEncryptedContent: value.item.encrypted_content ?? null, + }, + }, + }) + } + } else if (isResponseOutputItemDoneChunk(value)) { + if (value.item.type === "function_call") { + ongoingToolCalls[value.output_index] = undefined + hasFunctionCall = true + + controller.enqueue({ + type: "tool-input-end", + id: value.item.call_id, + }) + + controller.enqueue({ + type: "tool-call", + toolCallId: value.item.call_id, + toolName: value.item.name, + input: value.item.arguments, + providerMetadata: { + copilot: { + itemId: value.item.id, + }, + }, + }) + } else if (value.item.type === "web_search_call") { + ongoingToolCalls[value.output_index] = undefined + + controller.enqueue({ + type: "tool-input-end", + id: value.item.id, + }) + + controller.enqueue({ + type: "tool-call", + toolCallId: value.item.id, + toolName: "web_search", + input: JSON.stringify({ action: value.item.action }), + providerExecuted: true, + }) + + controller.enqueue({ + type: "tool-result", + toolCallId: value.item.id, + toolName: "web_search", + result: { status: value.item.status }, + }) + } else if (value.item.type === "computer_call") { + ongoingToolCalls[value.output_index] = undefined + + controller.enqueue({ + type: "tool-input-end", + id: value.item.id, + }) + + controller.enqueue({ + type: "tool-call", + toolCallId: value.item.id, + toolName: "computer_use", + input: "", + providerExecuted: true, + }) + + controller.enqueue({ + type: "tool-result", + toolCallId: value.item.id, + toolName: "computer_use", + result: { + type: "computer_use_tool_result", + status: value.item.status || "completed", + }, + }) + } else if (value.item.type === "file_search_call") { + ongoingToolCalls[value.output_index] = undefined + + controller.enqueue({ + type: "tool-result", + toolCallId: value.item.id, + toolName: "file_search", + result: { + queries: value.item.queries, + results: + value.item.results?.map((result) => ({ + attributes: result.attributes as Record, + fileId: result.file_id, + filename: result.filename, + score: result.score, + text: result.text, + })) ?? null, + } satisfies z.infer, + }) + } else if (value.item.type === "code_interpreter_call") { + ongoingToolCalls[value.output_index] = undefined + + controller.enqueue({ + type: "tool-result", + toolCallId: value.item.id, + toolName: "code_interpreter", + result: { + outputs: value.item.outputs, + } satisfies z.infer, + }) + } else if (value.item.type === "image_generation_call") { + controller.enqueue({ + type: "tool-result", + toolCallId: value.item.id, + toolName: "image_generation", + result: { + result: value.item.result, + } satisfies z.infer, + }) + } else if (value.item.type === "local_shell_call") { + ongoingToolCalls[value.output_index] = undefined + + controller.enqueue({ + type: "tool-call", + toolCallId: value.item.call_id, + toolName: "local_shell", + input: JSON.stringify({ + action: { + type: "exec", + command: value.item.action.command, + timeoutMs: value.item.action.timeout_ms, + user: value.item.action.user, + workingDirectory: value.item.action.working_directory, + env: value.item.action.env, + }, + } satisfies z.infer), + providerMetadata: { + copilot: { itemId: value.item.id }, + }, + }) + } else if (value.item.type === "message") { + if (currentTextId) { + controller.enqueue({ + type: "text-end", + id: currentTextId, + }) + currentTextId = null + } + } else if (isResponseOutputItemDoneReasoningChunk(value)) { + const activeReasoningPart = activeReasoning[value.output_index] + if (activeReasoningPart) { + for (const summaryIndex of activeReasoningPart.summaryParts) { + controller.enqueue({ + type: "reasoning-end", + id: `${activeReasoningPart.canonicalId}:${summaryIndex}`, + providerMetadata: { + copilot: { + itemId: activeReasoningPart.canonicalId, + reasoningEncryptedContent: value.item.encrypted_content ?? null, + }, + }, + }) + } + delete activeReasoning[value.output_index] + if (currentReasoningOutputIndex === value.output_index) { + currentReasoningOutputIndex = null + } + } + } + } else if (isResponseFunctionCallArgumentsDeltaChunk(value)) { + const toolCall = ongoingToolCalls[value.output_index] + + if (toolCall != null) { + controller.enqueue({ + type: "tool-input-delta", + id: toolCall.toolCallId, + delta: value.delta, + }) + } + } else if (isResponseImageGenerationCallPartialImageChunk(value)) { + controller.enqueue({ + type: "tool-result", + toolCallId: value.item_id, + toolName: "image_generation", + result: { + result: value.partial_image_b64, + } satisfies z.infer, + }) + } else if (isResponseCodeInterpreterCallCodeDeltaChunk(value)) { + const toolCall = ongoingToolCalls[value.output_index] + + if (toolCall != null) { + controller.enqueue({ + type: "tool-input-delta", + id: toolCall.toolCallId, + // The delta is code, which is embedding in a JSON string. + // To escape it, we use JSON.stringify and slice to remove the outer quotes. + delta: JSON.stringify(value.delta).slice(1, -1), + }) + } + } else if (isResponseCodeInterpreterCallCodeDoneChunk(value)) { + const toolCall = ongoingToolCalls[value.output_index] + + if (toolCall != null) { + controller.enqueue({ + type: "tool-input-delta", + id: toolCall.toolCallId, + delta: '"}', + }) + + controller.enqueue({ + type: "tool-input-end", + id: toolCall.toolCallId, + }) + + // immediately send the tool call after the input end: + controller.enqueue({ + type: "tool-call", + toolCallId: toolCall.toolCallId, + toolName: "code_interpreter", + input: JSON.stringify({ + code: value.code, + containerId: toolCall.codeInterpreter!.containerId, + } satisfies z.infer), + providerExecuted: true, + }) + } + } else if (isResponseCreatedChunk(value)) { + responseId = value.response.id + controller.enqueue({ + type: "response-metadata", + id: value.response.id, + timestamp: new Date(value.response.created_at * 1000), + modelId: value.response.model, + }) + } else if (isTextDeltaChunk(value)) { + // Ensure a text-start exists, and normalize deltas to a stable id + if (!currentTextId) { + currentTextId = value.item_id + controller.enqueue({ + type: "text-start", + id: currentTextId, + providerMetadata: { + copilot: { itemId: value.item_id }, + }, + }) + } + + controller.enqueue({ + type: "text-delta", + id: currentTextId, + delta: value.delta, + }) + + if (options.providerOptions?.copilot?.logprobs && value.logprobs) { + logprobs.push(value.logprobs) + } + } else if (isResponseReasoningSummaryPartAddedChunk(value)) { + const activeItem = + currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null + + // the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk. + if (activeItem && value.summary_index > 0) { + activeItem.summaryParts.push(value.summary_index) + + controller.enqueue({ + type: "reasoning-start", + id: `${activeItem.canonicalId}:${value.summary_index}`, + providerMetadata: { + copilot: { + itemId: activeItem.canonicalId, + reasoningEncryptedContent: activeItem.encryptedContent ?? null, + }, + }, + }) + } + } else if (isResponseReasoningSummaryTextDeltaChunk(value)) { + const activeItem = + currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null + + if (activeItem) { + controller.enqueue({ + type: "reasoning-delta", + id: `${activeItem.canonicalId}:${value.summary_index}`, + delta: value.delta, + providerMetadata: { + copilot: { + itemId: activeItem.canonicalId, + }, + }, + }) + } + } else if (isResponseFinishedChunk(value)) { + finishReason = { + unified: mapOpenAIResponseFinishReason({ + finishReason: value.response.incomplete_details?.reason, + hasFunctionCall, + }), + raw: value.response.incomplete_details?.reason ?? undefined, + } + usage.inputTokens = value.response.usage.input_tokens + usage.outputTokens = value.response.usage.output_tokens + usage.totalTokens = value.response.usage.input_tokens + value.response.usage.output_tokens + usage.reasoningTokens = value.response.usage.output_tokens_details?.reasoning_tokens ?? undefined + usage.cachedInputTokens = value.response.usage.input_tokens_details?.cached_tokens ?? undefined + if (typeof value.response.service_tier === "string") { + serviceTier = value.response.service_tier + } + } else if (isResponseAnnotationAddedChunk(value)) { + if (value.annotation.type === "url_citation") { + controller.enqueue({ + type: "source", + sourceType: "url", + id: self.config.generateId?.() ?? generateId(), + url: value.annotation.url, + title: value.annotation.title, + }) + } else if (value.annotation.type === "file_citation") { + controller.enqueue({ + type: "source", + sourceType: "document", + id: self.config.generateId?.() ?? generateId(), + mediaType: "text/plain", + title: value.annotation.quote ?? value.annotation.filename ?? "Document", + filename: value.annotation.filename ?? value.annotation.file_id, + }) + } + } else if (isErrorChunk(value)) { + controller.enqueue({ type: "error", error: value }) + } + }, + + flush(controller) { + // Close any dangling text part + if (currentTextId) { + controller.enqueue({ type: "text-end", id: currentTextId }) + currentTextId = null + } + + const providerMetadata: SharedV3ProviderMetadata = { + copilot: { + responseId, + }, + } + + if (logprobs.length > 0) { + providerMetadata.copilot.logprobs = logprobs + } + + if (serviceTier !== undefined) { + providerMetadata.copilot.serviceTier = serviceTier + } + + controller.enqueue({ + type: "finish", + finishReason, + usage: { + inputTokens: { + total: usage.inputTokens, + noCache: + usage.inputTokens != null && usage.cachedInputTokens != null + ? usage.inputTokens - usage.cachedInputTokens + : undefined, + cacheRead: usage.cachedInputTokens, + cacheWrite: undefined, + }, + outputTokens: { + total: usage.outputTokens, + text: undefined, + reasoning: usage.reasoningTokens, + }, + raw: { + input_tokens: usage.inputTokens, + output_tokens: usage.outputTokens, + total_tokens: usage.totalTokens, + }, + }, + providerMetadata, + }) + }, + }), + ), + request: { body }, + response: { headers: responseHeaders }, + } + } +} + +const usageSchema = z.object({ + input_tokens: z.number(), + input_tokens_details: z.object({ cached_tokens: z.number().nullish() }).nullish(), + output_tokens: z.number(), + output_tokens_details: z.object({ reasoning_tokens: z.number().nullish() }).nullish(), +}) + +const textDeltaChunkSchema = z.object({ + type: z.literal("response.output_text.delta"), + item_id: z.string(), + delta: z.string(), + logprobs: LOGPROBS_SCHEMA.nullish(), +}) + +const errorChunkSchema = z.object({ + type: z.literal("error"), + code: z.string(), + message: z.string(), + param: z.string().nullish(), + sequence_number: z.number(), +}) + +const responseFinishedChunkSchema = z.object({ + type: z.enum(["response.completed", "response.incomplete"]), + response: z.object({ + incomplete_details: z.object({ reason: z.string() }).nullish(), + usage: usageSchema, + service_tier: z.string().nullish(), + }), +}) + +const responseCreatedChunkSchema = z.object({ + type: z.literal("response.created"), + response: z.object({ + id: z.string(), + created_at: z.number(), + model: z.string(), + service_tier: z.string().nullish(), + }), +}) + +const responseOutputItemAddedSchema = z.object({ + type: z.literal("response.output_item.added"), + output_index: z.number(), + item: z.discriminatedUnion("type", [ + z.object({ + type: z.literal("message"), + id: z.string(), + }), + z.object({ + type: z.literal("reasoning"), + id: z.string(), + encrypted_content: z.string().nullish(), + }), + z.object({ + type: z.literal("function_call"), + id: z.string(), + call_id: z.string(), + name: z.string(), + arguments: z.string(), + }), + z.object({ + type: z.literal("web_search_call"), + id: z.string(), + status: z.string(), + action: z + .object({ + type: z.literal("search"), + query: z.string().optional(), + }) + .nullish(), + }), + z.object({ + type: z.literal("computer_call"), + id: z.string(), + status: z.string(), + }), + z.object({ + type: z.literal("file_search_call"), + id: z.string(), + }), + z.object({ + type: z.literal("image_generation_call"), + id: z.string(), + }), + z.object({ + type: z.literal("code_interpreter_call"), + id: z.string(), + container_id: z.string(), + code: z.string().nullable(), + outputs: z + .array( + z.discriminatedUnion("type", [ + z.object({ type: z.literal("logs"), logs: z.string() }), + z.object({ type: z.literal("image"), url: z.string() }), + ]), + ) + .nullable(), + status: z.string(), + }), + ]), +}) + +const responseOutputItemDoneSchema = z.object({ + type: z.literal("response.output_item.done"), + output_index: z.number(), + item: z.discriminatedUnion("type", [ + z.object({ + type: z.literal("message"), + id: z.string(), + }), + z.object({ + type: z.literal("reasoning"), + id: z.string(), + encrypted_content: z.string().nullish(), + }), + z.object({ + type: z.literal("function_call"), + id: z.string(), + call_id: z.string(), + name: z.string(), + arguments: z.string(), + status: z.literal("completed"), + }), + codeInterpreterCallItem, + imageGenerationCallItem, + webSearchCallItem, + fileSearchCallItem, + localShellCallItem, + z.object({ + type: z.literal("computer_call"), + id: z.string(), + status: z.literal("completed"), + }), + ]), +}) + +const responseFunctionCallArgumentsDeltaSchema = z.object({ + type: z.literal("response.function_call_arguments.delta"), + item_id: z.string(), + output_index: z.number(), + delta: z.string(), +}) + +const responseImageGenerationCallPartialImageSchema = z.object({ + type: z.literal("response.image_generation_call.partial_image"), + item_id: z.string(), + output_index: z.number(), + partial_image_b64: z.string(), +}) + +const responseCodeInterpreterCallCodeDeltaSchema = z.object({ + type: z.literal("response.code_interpreter_call_code.delta"), + item_id: z.string(), + output_index: z.number(), + delta: z.string(), +}) + +const responseCodeInterpreterCallCodeDoneSchema = z.object({ + type: z.literal("response.code_interpreter_call_code.done"), + item_id: z.string(), + output_index: z.number(), + code: z.string(), +}) + +const responseAnnotationAddedSchema = z.object({ + type: z.literal("response.output_text.annotation.added"), + annotation: z.discriminatedUnion("type", [ + z.object({ + type: z.literal("url_citation"), + url: z.string(), + title: z.string(), + }), + z.object({ + type: z.literal("file_citation"), + file_id: z.string(), + filename: z.string().nullish(), + index: z.number().nullish(), + start_index: z.number().nullish(), + end_index: z.number().nullish(), + quote: z.string().nullish(), + }), + ]), +}) + +const responseReasoningSummaryPartAddedSchema = z.object({ + type: z.literal("response.reasoning_summary_part.added"), + item_id: z.string(), + summary_index: z.number(), +}) + +const responseReasoningSummaryTextDeltaSchema = z.object({ + type: z.literal("response.reasoning_summary_text.delta"), + item_id: z.string(), + summary_index: z.number(), + delta: z.string(), +}) + +const openaiResponsesChunkSchema = z.union([ + textDeltaChunkSchema, + responseFinishedChunkSchema, + responseCreatedChunkSchema, + responseOutputItemAddedSchema, + responseOutputItemDoneSchema, + responseFunctionCallArgumentsDeltaSchema, + responseImageGenerationCallPartialImageSchema, + responseCodeInterpreterCallCodeDeltaSchema, + responseCodeInterpreterCallCodeDoneSchema, + responseAnnotationAddedSchema, + responseReasoningSummaryPartAddedSchema, + responseReasoningSummaryTextDeltaSchema, + errorChunkSchema, + z.object({ type: z.string() }).loose(), // fallback for unknown chunks +]) + +type ExtractByType = T extends { type: K } ? T : never + +function isTextDeltaChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.output_text.delta" +} + +function isResponseOutputItemDoneChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.output_item.done" +} + +function isResponseOutputItemDoneReasoningChunk(chunk: z.infer): chunk is z.infer< + typeof responseOutputItemDoneSchema +> & { + item: ExtractByType["item"], "reasoning"> +} { + return isResponseOutputItemDoneChunk(chunk) && chunk.item.type === "reasoning" +} + +function isResponseFinishedChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.completed" || chunk.type === "response.incomplete" +} + +function isResponseCreatedChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.created" +} + +function isResponseFunctionCallArgumentsDeltaChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.function_call_arguments.delta" +} +function isResponseImageGenerationCallPartialImageChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.image_generation_call.partial_image" +} + +function isResponseCodeInterpreterCallCodeDeltaChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.code_interpreter_call_code.delta" +} + +function isResponseCodeInterpreterCallCodeDoneChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.code_interpreter_call_code.done" +} + +function isResponseOutputItemAddedChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.output_item.added" +} + +function isResponseOutputItemAddedReasoningChunk(chunk: z.infer): chunk is z.infer< + typeof responseOutputItemAddedSchema +> & { + item: ExtractByType["item"], "reasoning"> +} { + return isResponseOutputItemAddedChunk(chunk) && chunk.item.type === "reasoning" +} + +function isResponseAnnotationAddedChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.output_text.annotation.added" +} + +function isResponseReasoningSummaryPartAddedChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.reasoning_summary_part.added" +} + +function isResponseReasoningSummaryTextDeltaChunk( + chunk: z.infer, +): chunk is z.infer { + return chunk.type === "response.reasoning_summary_text.delta" +} + +function isErrorChunk(chunk: z.infer): chunk is z.infer { + return chunk.type === "error" +} + +type ResponsesModelConfig = { + isReasoningModel: boolean + systemMessageMode: "remove" | "system" | "developer" + requiredAutoTruncation: boolean + supportsFlexProcessing: boolean + supportsPriorityProcessing: boolean +} + +function getResponsesModelConfig(modelId: string): ResponsesModelConfig { + const supportsFlexProcessing = + modelId.startsWith("o3") || + modelId.startsWith("o4-mini") || + (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-chat")) + const supportsPriorityProcessing = + modelId.startsWith("gpt-4") || + modelId.startsWith("gpt-5-mini") || + (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-nano") && !modelId.startsWith("gpt-5-chat")) || + modelId.startsWith("o3") || + modelId.startsWith("o4-mini") + const defaults = { + requiredAutoTruncation: false, + systemMessageMode: "system" as const, + supportsFlexProcessing, + supportsPriorityProcessing, + } + + // gpt-5-chat models are non-reasoning + if (modelId.startsWith("gpt-5-chat")) { + return { + ...defaults, + isReasoningModel: false, + } + } + + // o series reasoning models: + if ( + modelId.startsWith("o") || + modelId.startsWith("gpt-5") || + modelId.startsWith("codex-") || + modelId.startsWith("computer-use") + ) { + if (modelId.startsWith("o1-mini") || modelId.startsWith("o1-preview")) { + return { + ...defaults, + isReasoningModel: true, + systemMessageMode: "remove", + } + } + + return { + ...defaults, + isReasoningModel: true, + systemMessageMode: "developer", + } + } + + // gpt models: + return { + ...defaults, + isReasoningModel: false, + } +} + +// TODO AI SDK 6: use optional here instead of nullish +const openaiResponsesProviderOptionsSchema = z.object({ + include: z + .array(z.enum(["reasoning.encrypted_content", "file_search_call.results", "message.output_text.logprobs"])) + .nullish(), + instructions: z.string().nullish(), + + /** + * Return the log probabilities of the tokens. + * + * Setting to true will return the log probabilities of the tokens that + * were generated. + * + * Setting to a number will return the log probabilities of the top n + * tokens that were generated. + * + * @see https://platform.openai.com/docs/api-reference/responses/create + * @see https://cookbook.openai.com/examples/using_logprobs + */ + logprobs: z.union([z.boolean(), z.number().min(1).max(TOP_LOGPROBS_MAX)]).optional(), + + /** + * The maximum number of total calls to built-in tools that can be processed in a response. + * This maximum number applies across all built-in tool calls, not per individual tool. + * Any further attempts to call a tool by the model will be ignored. + */ + maxToolCalls: z.number().nullish(), + + metadata: z.any().nullish(), + parallelToolCalls: z.boolean().nullish(), + previousResponseId: z.string().nullish(), + promptCacheKey: z.string().nullish(), + reasoningEffort: z.string().nullish(), + reasoningSummary: z.string().nullish(), + safetyIdentifier: z.string().nullish(), + serviceTier: z.enum(["auto", "flex", "priority"]).nullish(), + store: z.boolean().nullish(), + strictJsonSchema: z.boolean().nullish(), + textVerbosity: z.enum(["low", "medium", "high"]).nullish(), + user: z.string().nullish(), +}) + +export type OpenAIResponsesProviderOptions = z.infer diff --git a/packages/core/src/github-copilot/responses/openai-responses-prepare-tools.ts b/packages/core/src/github-copilot/responses/openai-responses-prepare-tools.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b2eb01673a48e05c72ee72d6681bb3aa127729d --- /dev/null +++ b/packages/core/src/github-copilot/responses/openai-responses-prepare-tools.ts @@ -0,0 +1,173 @@ +import { type LanguageModelV3CallOptions, type SharedV3Warning, UnsupportedFunctionalityError } from "@ai-sdk/provider" +import { codeInterpreterArgsSchema } from "./tool/code-interpreter" +import { fileSearchArgsSchema } from "./tool/file-search" +import { webSearchArgsSchema } from "./tool/web-search" +import { webSearchPreviewArgsSchema } from "./tool/web-search-preview" +import { imageGenerationArgsSchema } from "./tool/image-generation" +import type { OpenAIResponsesTool } from "./openai-responses-api-types" + +export function prepareResponsesTools({ + tools, + toolChoice, + strictJsonSchema, +}: { + tools: LanguageModelV3CallOptions["tools"] + toolChoice?: LanguageModelV3CallOptions["toolChoice"] + strictJsonSchema: boolean +}): { + tools?: Array + toolChoice?: + | "auto" + | "none" + | "required" + | { type: "file_search" } + | { type: "web_search_preview" } + | { type: "web_search" } + | { type: "function"; name: string } + | { type: "code_interpreter" } + | { type: "image_generation" } + toolWarnings: SharedV3Warning[] +} { + // when the tools array is empty, change it to undefined to prevent errors: + tools = tools?.length ? tools : undefined + + const toolWarnings: SharedV3Warning[] = [] + + if (tools == null) { + return { tools: undefined, toolChoice: undefined, toolWarnings } + } + + const openaiTools: Array = [] + + for (const tool of tools) { + switch (tool.type) { + case "function": + openaiTools.push({ + type: "function", + name: tool.name, + description: tool.description, + parameters: tool.inputSchema, + strict: strictJsonSchema, + }) + break + case "provider": { + switch (tool.id) { + case "openai.file_search": { + const args = fileSearchArgsSchema.parse(tool.args) + + openaiTools.push({ + type: "file_search", + vector_store_ids: args.vectorStoreIds, + max_num_results: args.maxNumResults, + ranking_options: args.ranking + ? { + ranker: args.ranking.ranker, + score_threshold: args.ranking.scoreThreshold, + } + : undefined, + filters: args.filters, + }) + + break + } + case "openai.local_shell": { + openaiTools.push({ + type: "local_shell", + }) + break + } + case "openai.web_search_preview": { + const args = webSearchPreviewArgsSchema.parse(tool.args) + openaiTools.push({ + type: "web_search_preview", + search_context_size: args.searchContextSize, + user_location: args.userLocation, + }) + break + } + case "openai.web_search": { + const args = webSearchArgsSchema.parse(tool.args) + openaiTools.push({ + type: "web_search", + filters: args.filters != null ? { allowed_domains: args.filters.allowedDomains } : undefined, + search_context_size: args.searchContextSize, + user_location: args.userLocation, + }) + break + } + case "openai.code_interpreter": { + const args = codeInterpreterArgsSchema.parse(tool.args) + openaiTools.push({ + type: "code_interpreter", + container: + args.container == null + ? { type: "auto", file_ids: undefined } + : typeof args.container === "string" + ? args.container + : { type: "auto", file_ids: args.container.fileIds }, + }) + break + } + case "openai.image_generation": { + const args = imageGenerationArgsSchema.parse(tool.args) + openaiTools.push({ + type: "image_generation", + background: args.background, + input_fidelity: args.inputFidelity, + input_image_mask: args.inputImageMask + ? { + file_id: args.inputImageMask.fileId, + image_url: args.inputImageMask.imageUrl, + } + : undefined, + model: args.model, + moderation: args.moderation, + partial_images: args.partialImages, + quality: args.quality, + output_compression: args.outputCompression, + output_format: args.outputFormat, + size: args.size, + }) + break + } + } + break + } + default: + toolWarnings.push({ type: "unsupported", feature: "tool type" }) + break + } + } + + if (toolChoice == null) { + return { tools: openaiTools, toolChoice: undefined, toolWarnings } + } + + const type = toolChoice.type + + switch (type) { + case "auto": + case "none": + case "required": + return { tools: openaiTools, toolChoice: type, toolWarnings } + case "tool": + return { + tools: openaiTools, + toolChoice: + toolChoice.toolName === "code_interpreter" || + toolChoice.toolName === "file_search" || + toolChoice.toolName === "image_generation" || + toolChoice.toolName === "web_search_preview" || + toolChoice.toolName === "web_search" + ? { type: toolChoice.toolName } + : { type: "function", name: toolChoice.toolName }, + toolWarnings, + } + default: { + const _exhaustiveCheck: never = type + throw new UnsupportedFunctionalityError({ + functionality: `tool choice type: ${_exhaustiveCheck}`, + }) + } + } +} diff --git a/packages/core/src/github-copilot/responses/openai-responses-settings.ts b/packages/core/src/github-copilot/responses/openai-responses-settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..76c97346fa24dfbb67c427d84444cbd839175b08 --- /dev/null +++ b/packages/core/src/github-copilot/responses/openai-responses-settings.ts @@ -0,0 +1 @@ +export type OpenAIResponsesModelId = string diff --git a/packages/core/src/github-copilot/responses/tool/code-interpreter.ts b/packages/core/src/github-copilot/responses/tool/code-interpreter.ts new file mode 100644 index 0000000000000000000000000000000000000000..909694ec7d69435293eb8ec1431f74453f19d4e3 --- /dev/null +++ b/packages/core/src/github-copilot/responses/tool/code-interpreter.ts @@ -0,0 +1,87 @@ +import { createProviderToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" +import { z } from "zod/v4" + +export const codeInterpreterInputSchema = z.object({ + code: z.string().nullish(), + containerId: z.string(), +}) + +export const codeInterpreterOutputSchema = z.object({ + outputs: z + .array( + z.discriminatedUnion("type", [ + z.object({ type: z.literal("logs"), logs: z.string() }), + z.object({ type: z.literal("image"), url: z.string() }), + ]), + ) + .nullish(), +}) + +export const codeInterpreterArgsSchema = z.object({ + container: z + .union([ + z.string(), + z.object({ + fileIds: z.array(z.string()).optional(), + }), + ]) + .optional(), +}) + +type CodeInterpreterArgs = { + /** + * The code interpreter container. + * Can be a container ID + * or an object that specifies uploaded file IDs to make available to your code. + */ + container?: string | { fileIds?: string[] } +} + +export const codeInterpreterToolFactory = createProviderToolFactoryWithOutputSchema< + { + /** + * The code to run, or null if not available. + */ + code?: string | null + + /** + * The ID of the container used to run the code. + */ + containerId: string + }, + { + /** + * The outputs generated by the code interpreter, such as logs or images. + * Can be null if no outputs are available. + */ + outputs?: Array< + | { + type: "logs" + + /** + * The logs output from the code interpreter. + */ + logs: string + } + | { + type: "image" + + /** + * The URL of the image output from the code interpreter. + */ + url: string + } + > | null + }, + CodeInterpreterArgs +>({ + id: "openai.code_interpreter", + inputSchema: codeInterpreterInputSchema, + outputSchema: codeInterpreterOutputSchema, +}) + +export const codeInterpreter = ( + args: CodeInterpreterArgs = {}, // default +) => { + return codeInterpreterToolFactory(args) +} diff --git a/packages/core/src/github-copilot/responses/tool/file-search.ts b/packages/core/src/github-copilot/responses/tool/file-search.ts new file mode 100644 index 0000000000000000000000000000000000000000..12a490e19dd608bc2be86e51085585ca60d4804a --- /dev/null +++ b/packages/core/src/github-copilot/responses/tool/file-search.ts @@ -0,0 +1,127 @@ +import { createProviderToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" +import type { + OpenAIResponsesFileSearchToolComparisonFilter, + OpenAIResponsesFileSearchToolCompoundFilter, +} from "../openai-responses-api-types" +import { z } from "zod/v4" + +const comparisonFilterSchema = z.object({ + key: z.string(), + type: z.enum(["eq", "ne", "gt", "gte", "lt", "lte"]), + value: z.union([z.string(), z.number(), z.boolean()]), +}) + +const compoundFilterSchema: z.ZodType = z.object({ + type: z.enum(["and", "or"]), + filters: z.array(z.union([comparisonFilterSchema, z.lazy(() => compoundFilterSchema)])), +}) + +export const fileSearchArgsSchema = z.object({ + vectorStoreIds: z.array(z.string()), + maxNumResults: z.number().optional(), + ranking: z + .object({ + ranker: z.string().optional(), + scoreThreshold: z.number().optional(), + }) + .optional(), + filters: z.union([comparisonFilterSchema, compoundFilterSchema]).optional(), +}) + +export const fileSearchOutputSchema = z.object({ + queries: z.array(z.string()), + results: z + .array( + z.object({ + attributes: z.record(z.string(), z.unknown()), + fileId: z.string(), + filename: z.string(), + score: z.number(), + text: z.string(), + }), + ) + .nullable(), +}) + +export const fileSearch = createProviderToolFactoryWithOutputSchema< + {}, + { + /** + * The search query to execute. + */ + queries: string[] + + /** + * The results of the file search tool call. + */ + results: + | null + | { + /** + * Set of 16 key-value pairs that can be attached to an object. + * This can be useful for storing additional information about the object + * in a structured format, and querying for objects via API or the dashboard. + * Keys are strings with a maximum length of 64 characters. + * Values are strings with a maximum length of 512 characters, booleans, or numbers. + */ + attributes: Record + + /** + * The unique ID of the file. + */ + fileId: string + + /** + * The name of the file. + */ + filename: string + + /** + * The relevance score of the file - a value between 0 and 1. + */ + score: number + + /** + * The text that was retrieved from the file. + */ + text: string + }[] + }, + { + /** + * List of vector store IDs to search through. + */ + vectorStoreIds: string[] + + /** + * Maximum number of search results to return. Defaults to 10. + */ + maxNumResults?: number + + /** + * Ranking options for the search. + */ + ranking?: { + /** + * The ranker to use for the file search. + */ + ranker?: string + + /** + * The score threshold for the file search, a number between 0 and 1. + * Numbers closer to 1 will attempt to return only the most relevant results, + * but may return fewer results. + */ + scoreThreshold?: number + } + + /** + * A filter to apply. + */ + filters?: OpenAIResponsesFileSearchToolComparisonFilter | OpenAIResponsesFileSearchToolCompoundFilter + } +>({ + id: "openai.file_search", + inputSchema: z.object({}), + outputSchema: fileSearchOutputSchema, +}) diff --git a/packages/core/src/github-copilot/responses/tool/image-generation.ts b/packages/core/src/github-copilot/responses/tool/image-generation.ts new file mode 100644 index 0000000000000000000000000000000000000000..b67bb76f9ce4a43c08dcfc41eb52feb7e962fc5c --- /dev/null +++ b/packages/core/src/github-copilot/responses/tool/image-generation.ts @@ -0,0 +1,114 @@ +import { createProviderToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" +import { z } from "zod/v4" + +export const imageGenerationArgsSchema = z + .object({ + background: z.enum(["auto", "opaque", "transparent"]).optional(), + inputFidelity: z.enum(["low", "high"]).optional(), + inputImageMask: z + .object({ + fileId: z.string().optional(), + imageUrl: z.string().optional(), + }) + .optional(), + model: z.string().optional(), + moderation: z.enum(["auto"]).optional(), + outputCompression: z.number().int().min(0).max(100).optional(), + outputFormat: z.enum(["png", "jpeg", "webp"]).optional(), + partialImages: z.number().int().min(0).max(3).optional(), + quality: z.enum(["auto", "low", "medium", "high"]).optional(), + size: z.enum(["1024x1024", "1024x1536", "1536x1024", "auto"]).optional(), + }) + .strict() + +export const imageGenerationOutputSchema = z.object({ + result: z.string(), +}) + +type ImageGenerationArgs = { + /** + * Background type for the generated image. Default is 'auto'. + */ + background?: "auto" | "opaque" | "transparent" + + /** + * Input fidelity for the generated image. Default is 'low'. + */ + inputFidelity?: "low" | "high" + + /** + * Optional mask for inpainting. + * Contains image_url (string, optional) and file_id (string, optional). + */ + inputImageMask?: { + /** + * File ID for the mask image. + */ + fileId?: string + + /** + * Base64-encoded mask image. + */ + imageUrl?: string + } + + /** + * The image generation model to use. Default: gpt-image-1. + */ + model?: string + + /** + * Moderation level for the generated image. Default: auto. + */ + moderation?: "auto" + + /** + * Compression level for the output image. Default: 100. + */ + outputCompression?: number + + /** + * The output format of the generated image. One of png, webp, or jpeg. + * Default: png + */ + outputFormat?: "png" | "jpeg" | "webp" + + /** + * Number of partial images to generate in streaming mode, from 0 (default value) to 3. + */ + partialImages?: number + + /** + * The quality of the generated image. + * One of low, medium, high, or auto. Default: auto. + */ + quality?: "auto" | "low" | "medium" | "high" + + /** + * The size of the generated image. + * One of 1024x1024, 1024x1536, 1536x1024, or auto. + * Default: auto. + */ + size?: "auto" | "1024x1024" | "1024x1536" | "1536x1024" +} + +const imageGenerationToolFactory = createProviderToolFactoryWithOutputSchema< + {}, + { + /** + * The generated image encoded in base64. + */ + result: string + }, + ImageGenerationArgs +>({ + id: "openai.image_generation", + inputSchema: z.object({}), + outputSchema: imageGenerationOutputSchema, +}) + +export const imageGeneration = ( + args: ImageGenerationArgs = {}, // default +) => { + return imageGenerationToolFactory(args) +} diff --git a/packages/core/src/github-copilot/responses/tool/local-shell.ts b/packages/core/src/github-copilot/responses/tool/local-shell.ts new file mode 100644 index 0000000000000000000000000000000000000000..45230d5ce55d8cf66bd1ab3d8e71a6f8d2364c7c --- /dev/null +++ b/packages/core/src/github-copilot/responses/tool/local-shell.ts @@ -0,0 +1,64 @@ +import { createProviderToolFactoryWithOutputSchema } from "@ai-sdk/provider-utils" +import { z } from "zod/v4" + +export const localShellInputSchema = z.object({ + action: z.object({ + type: z.literal("exec"), + command: z.array(z.string()), + timeoutMs: z.number().optional(), + user: z.string().optional(), + workingDirectory: z.string().optional(), + env: z.record(z.string(), z.string()).optional(), + }), +}) + +export const localShellOutputSchema = z.object({ + output: z.string(), +}) + +export const localShell = createProviderToolFactoryWithOutputSchema< + { + /** + * Execute a shell command on the server. + */ + action: { + type: "exec" + + /** + * The command to run. + */ + command: string[] + + /** + * Optional timeout in milliseconds for the command. + */ + timeoutMs?: number + + /** + * Optional user to run the command as. + */ + user?: string + + /** + * Optional working directory to run the command in. + */ + workingDirectory?: string + + /** + * Environment variables to set for the command. + */ + env?: Record + } + }, + { + /** + * The output of local shell tool call. + */ + output: string + }, + {} +>({ + id: "openai.local_shell", + inputSchema: localShellInputSchema, + outputSchema: localShellOutputSchema, +}) diff --git a/packages/core/src/github-copilot/responses/tool/web-search-preview.ts b/packages/core/src/github-copilot/responses/tool/web-search-preview.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d9a308d8ac02f774d8f340ce2acaec5e5863777 --- /dev/null +++ b/packages/core/src/github-copilot/responses/tool/web-search-preview.ts @@ -0,0 +1,103 @@ +import { createProviderToolFactory } from "@ai-sdk/provider-utils" +import { z } from "zod/v4" + +// Args validation schema +export const webSearchPreviewArgsSchema = z.object({ + /** + * Search context size to use for the web search. + * - high: Most comprehensive context, highest cost, slower response + * - medium: Balanced context, cost, and latency (default) + * - low: Least context, lowest cost, fastest response + */ + searchContextSize: z.enum(["low", "medium", "high"]).optional(), + + /** + * User location information to provide geographically relevant search results. + */ + userLocation: z + .object({ + /** + * Type of location (always 'approximate') + */ + type: z.literal("approximate"), + /** + * Two-letter ISO country code (e.g., 'US', 'GB') + */ + country: z.string().optional(), + /** + * City name (free text, e.g., 'Minneapolis') + */ + city: z.string().optional(), + /** + * Region name (free text, e.g., 'Minnesota') + */ + region: z.string().optional(), + /** + * IANA timezone (e.g., 'America/Chicago') + */ + timezone: z.string().optional(), + }) + .optional(), +}) + +export const webSearchPreview = createProviderToolFactory< + { + // Web search doesn't take input parameters - it's controlled by the prompt + }, + { + /** + * Search context size to use for the web search. + * - high: Most comprehensive context, highest cost, slower response + * - medium: Balanced context, cost, and latency (default) + * - low: Least context, lowest cost, fastest response + */ + searchContextSize?: "low" | "medium" | "high" + + /** + * User location information to provide geographically relevant search results. + */ + userLocation?: { + /** + * Type of location (always 'approximate') + */ + type: "approximate" + /** + * Two-letter ISO country code (e.g., 'US', 'GB') + */ + country?: string + /** + * City name (free text, e.g., 'Minneapolis') + */ + city?: string + /** + * Region name (free text, e.g., 'Minnesota') + */ + region?: string + /** + * IANA timezone (e.g., 'America/Chicago') + */ + timezone?: string + } + } +>({ + id: "openai.web_search_preview", + inputSchema: z.object({ + action: z + .discriminatedUnion("type", [ + z.object({ + type: z.literal("search"), + query: z.string().nullish(), + }), + z.object({ + type: z.literal("open_page"), + url: z.string(), + }), + z.object({ + type: z.literal("find"), + url: z.string(), + pattern: z.string(), + }), + ]) + .nullish(), + }), +}) diff --git a/packages/core/src/github-copilot/responses/tool/web-search.ts b/packages/core/src/github-copilot/responses/tool/web-search.ts new file mode 100644 index 0000000000000000000000000000000000000000..e380bb13b62ac109d1054e94188bb4204b91fa9a --- /dev/null +++ b/packages/core/src/github-copilot/responses/tool/web-search.ts @@ -0,0 +1,102 @@ +import { createProviderToolFactory } from "@ai-sdk/provider-utils" +import { z } from "zod/v4" + +export const webSearchArgsSchema = z.object({ + filters: z + .object({ + allowedDomains: z.array(z.string()).optional(), + }) + .optional(), + + searchContextSize: z.enum(["low", "medium", "high"]).optional(), + + userLocation: z + .object({ + type: z.literal("approximate"), + country: z.string().optional(), + city: z.string().optional(), + region: z.string().optional(), + timezone: z.string().optional(), + }) + .optional(), +}) + +export const webSearchToolFactory = createProviderToolFactory< + { + // Web search doesn't take input parameters - it's controlled by the prompt + }, + { + /** + * Filters for the search. + */ + filters?: { + /** + * Allowed domains for the search. + * If not provided, all domains are allowed. + * Subdomains of the provided domains are allowed as well. + */ + allowedDomains?: string[] + } + + /** + * Search context size to use for the web search. + * - high: Most comprehensive context, highest cost, slower response + * - medium: Balanced context, cost, and latency (default) + * - low: Least context, lowest cost, fastest response + */ + searchContextSize?: "low" | "medium" | "high" + + /** + * User location information to provide geographically relevant search results. + */ + userLocation?: { + /** + * Type of location (always 'approximate') + */ + type: "approximate" + /** + * Two-letter ISO country code (e.g., 'US', 'GB') + */ + country?: string + /** + * City name (free text, e.g., 'Minneapolis') + */ + city?: string + /** + * Region name (free text, e.g., 'Minnesota') + */ + region?: string + /** + * IANA timezone (e.g., 'America/Chicago') + */ + timezone?: string + } + } +>({ + id: "openai.web_search", + inputSchema: z.object({ + action: z + .discriminatedUnion("type", [ + z.object({ + type: z.literal("search"), + query: z.string().nullish(), + }), + z.object({ + type: z.literal("open_page"), + url: z.string(), + }), + z.object({ + type: z.literal("find"), + url: z.string(), + pattern: z.string(), + }), + ]) + .nullish(), + }), +}) + +export const webSearch = ( + args: Parameters[0] = {}, // default +) => { + return webSearchToolFactory(args) +} diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts new file mode 100644 index 0000000000000000000000000000000000000000..a192a4b4684fe2dc308665f3d3fa64c866bf8ccc --- /dev/null +++ b/packages/core/src/global.ts @@ -0,0 +1,87 @@ +import path from "path" +import fs from "fs/promises" +import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" +import os from "os" +import { Context, Effect, Layer } from "effect" +import { Flock } from "./util/flock" +import { Flag } from "./flag/flag" +import { makeGlobalNode } from "./effect/app-node" + +const app = "opencode" +const data = path.join(xdgData!, app) +const cache = path.join(xdgCache!, app) +const config = path.join(xdgConfig!, app) +const state = path.join(xdgState!, app) +const tmp = path.join(os.tmpdir(), app) + +const paths = { + get home() { + return process.env.OPENCODE_TEST_HOME ?? os.homedir() + }, + data, + bin: path.join(cache, "bin"), + log: path.join(data, "log"), + repos: path.join(data, "repos"), + cache, + config, + state, + tmp, +} + +export const Path = paths + +Flock.setGlobal({ state }) + +await Promise.all([ + fs.mkdir(Path.data, { recursive: true }), + fs.mkdir(Path.config, { recursive: true }), + fs.mkdir(Path.state, { recursive: true }), + fs.mkdir(Path.tmp, { recursive: true }), + fs.mkdir(Path.log, { recursive: true }), + fs.mkdir(Path.bin, { recursive: true }), + fs.mkdir(Path.repos, { recursive: true }), +]) + +export class Service extends Context.Service()("@opencode/Global") {} + +export interface Interface { + readonly home: string + readonly data: string + readonly cache: string + readonly config: string + readonly state: string + readonly tmp: string + readonly bin: string + readonly log: string + readonly repos: string +} + +export function make(input: Partial = {}): Interface { + return { + home: Path.home, + data: Path.data, + cache: Path.cache, + config: Flag.OPENCODE_CONFIG_DIR ?? Path.config, + state: Path.state, + tmp: Path.tmp, + bin: Path.bin, + log: Path.log, + repos: Path.repos, + ...input, + } +} + +const layer = Layer.effect( + Service, + Effect.sync(() => Service.of(make())), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] }) + +export const layerWith = (input: Partial) => + Layer.effect( + Service, + Effect.sync(() => Service.of(make(input))), + ) + +export * as Global from "./global" diff --git a/packages/core/src/id/id.ts b/packages/core/src/id/id.ts new file mode 100644 index 0000000000000000000000000000000000000000..be1efc446a6e17b6f92560fe3e0fa4222cbe2a66 --- /dev/null +++ b/packages/core/src/id/id.ts @@ -0,0 +1,47 @@ +import { create as createIdentifier } from "@opencode-ai/schema/identifier" + +const prefixes = { + job: "job", + event: "evt", + session: "ses", + message: "msg", + permission: "per", + question: "que", + part: "prt", + pty: "pty", + tool: "tool", + workspace: "wrk", +} as const + +export function ascending(prefix: keyof typeof prefixes, given?: string) { + return generateID(prefix, "ascending", given) +} + +export function descending(prefix: keyof typeof prefixes, given?: string) { + return generateID(prefix, "descending", given) +} + +function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string { + if (!given) { + return create(prefixes[prefix], direction) + } + + if (!given.startsWith(prefixes[prefix])) { + throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`) + } + return given +} + +export function create(prefix: string, direction: "descending" | "ascending", timestamp?: number): string { + return prefix + "_" + createIdentifier(direction === "descending", timestamp) +} + +/** Extract timestamp from an ascending ID. Does not work with descending IDs. */ +export function timestamp(id: string): number { + const prefix = id.split("_")[0] + const hex = id.slice(prefix.length + 1, prefix.length + 13) + const encoded = BigInt("0x" + hex) + return Number(encoded / BigInt(0x1000)) +} + +export * as Identifier from "./id" diff --git a/packages/core/src/image.ts b/packages/core/src/image.ts new file mode 100644 index 0000000000000000000000000000000000000000..304277669e8df7d3fc6472309eab8bb71b8ad712 --- /dev/null +++ b/packages/core/src/image.ts @@ -0,0 +1,81 @@ +export * as Image from "./image" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect, Layer, Schema } from "effect" +import { Config } from "./config" +import { FileSystem } from "./filesystem" + +export class ResizerUnavailableError extends Schema.TaggedErrorClass()( + "Image.ResizerUnavailableError", + {}, +) {} + +export class DecodeError extends Schema.TaggedErrorClass()("Image.DecodeError", { + resource: Schema.String, +}) { + override get message() { + return `Image could not be decoded: ${this.resource}` + } +} + +export class SizeError extends Schema.TaggedErrorClass()("Image.SizeError", { + resource: Schema.String, + width: Schema.Number, + height: Schema.Number, + bytes: Schema.Number, + maxWidth: Schema.Number, + maxHeight: Schema.Number, + maxBytes: Schema.Number, +}) { + override get message() { + return `Image ${this.resource} is ${this.width}x${this.height} with base64 size ${this.bytes}, exceeding configured limits ${this.maxWidth}x${this.maxHeight}/${this.maxBytes} bytes` + } +} + +export interface Interface { + readonly normalize: ( + resource: string, + content: FileSystem.Content & { readonly encoding: "base64" }, + ) => Effect.Effect< + FileSystem.Content & { readonly encoding: "base64" }, + ResizerUnavailableError | DecodeError | SizeError + > +} + +export class Service extends Context.Service()("@opencode/Image") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const config = yield* Config.Service + const loadAdapter = yield* Effect.cached( + Effect.tryPromise({ + try: () => import("./image/photon"), + catch: () => new ResizerUnavailableError(), + }).pipe(Effect.flatMap((adapter) => adapter.make)), + ) + const normalize = Effect.fn("Image.normalize")(function* ( + resource: string, + content: FileSystem.Content & { readonly encoding: "base64" }, + ) { + const image = Object.assign( + {}, + ...(yield* config.entries()).flatMap((entry) => + entry.type === "document" && entry.info.attachments?.image ? [entry.info.attachments.image] : [], + ), + ) + const normalize = yield* loadAdapter + return yield* normalize(resource, content, { + autoResize: image.auto_resize ?? true, + maxWidth: image.max_width ?? 2_000, + maxHeight: image.max_height ?? 2_000, + maxBase64Bytes: image.max_base64_bytes ?? 5 * 1024 * 1024, + }) + }) + return Service.of({ normalize }) + }), +) + +export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer)) + +export const node = makeLocationNode({ service: Service, layer, deps: [Config.node] }) diff --git a/packages/core/src/image/photon.ts b/packages/core/src/image/photon.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4a00ce5fc8ccc7c42bb20d3c39668540510f941 --- /dev/null +++ b/packages/core/src/image/photon.ts @@ -0,0 +1,94 @@ +// @ts-ignore Bun's static file import is embedded by `bun build --compile`; some consumers also declare *.wasm. +import photonWasm from "@silvia-odwyer/photon-node/photon_rs_bg.wasm" with { type: "file" } +import { Effect } from "effect" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { FileSystem } from "../filesystem" +import { DecodeError, ResizerUnavailableError, SizeError } from "../image" + +const JPEG_QUALITIES = [80, 85, 70, 55, 40] + +export const make = Effect.gen(function* () { + ;(globalThis as typeof globalThis & { __OPENCODE_PHOTON_WASM_PATH?: string }).__OPENCODE_PHOTON_WASM_PATH = + path.isAbsolute(photonWasm) ? photonWasm : fileURLToPath(new URL(photonWasm, import.meta.url)) + const loadPhoton = yield* Effect.cached( + Effect.tryPromise({ + try: () => import("@silvia-odwyer/photon-node"), + catch: () => new ResizerUnavailableError(), + }), + ) + return Effect.fn("Image.Photon.normalize")(function* ( + resource: string, + content: FileSystem.Content & { readonly encoding: "base64" }, + limits: { + readonly autoResize: boolean + readonly maxWidth: number + readonly maxHeight: number + readonly maxBase64Bytes: number + }, + ) { + const photon = yield* loadPhoton + const decoded = yield* Effect.try({ + try: () => photon.PhotonImage.new_from_byteslice(Buffer.from(content.content, "base64")), + catch: () => new DecodeError({ resource }), + }) + try { + const width = decoded.get_width() + const height = decoded.get_height() + const bytes = Buffer.byteLength(content.content, "utf-8") + if (width <= limits.maxWidth && height <= limits.maxHeight && bytes <= limits.maxBase64Bytes) return content + if (!limits.autoResize) + return yield* new SizeError({ + resource, + width, + height, + bytes, + maxWidth: limits.maxWidth, + maxHeight: limits.maxHeight, + maxBytes: limits.maxBase64Bytes, + }) + const scale = Math.min(1, limits.maxWidth / width, limits.maxHeight / height) + const sizes = Array.from({ length: 32 }).reduce>((acc) => { + const previous = acc.at(-1) ?? { + width: Math.max(1, Math.round(width * scale)), + height: Math.max(1, Math.round(height * scale)), + } + const next = + acc.length === 0 + ? previous + : { + width: previous.width === 1 ? 1 : Math.max(1, Math.floor(previous.width * 0.75)), + height: previous.height === 1 ? 1 : Math.max(1, Math.floor(previous.height * 0.75)), + } + return acc.some((item) => item.width === next.width && item.height === next.height) ? acc : [...acc, next] + }, []) + for (const size of sizes) { + const resized = photon.resize(decoded, size.width, size.height, photon.SamplingFilter.Lanczos3) + try { + const encoders: Array Uint8Array]> = [ + ["image/png", () => resized.get_bytes()], + ...JPEG_QUALITIES.map((quality) => ["image/jpeg", () => resized.get_bytes_jpeg(quality)] as const), + ] + for (const [mime, encode] of encoders) { + const candidate = Buffer.from(encode()).toString("base64") + if (Buffer.byteLength(candidate, "utf-8") <= limits.maxBase64Bytes) + return { ...content, content: candidate, encoding: "base64" as const, mime } + } + } finally { + resized.free() + } + } + return yield* new SizeError({ + resource, + width, + height, + bytes, + maxWidth: limits.maxWidth, + maxHeight: limits.maxHeight, + maxBytes: limits.maxBase64Bytes, + }) + } finally { + decoded.free() + } + }) +}) diff --git a/packages/core/src/installation/version.ts b/packages/core/src/installation/version.ts new file mode 100644 index 0000000000000000000000000000000000000000..25d9cd99aa6c0e4110a3efba8696c36bfcfaadb1 --- /dev/null +++ b/packages/core/src/installation/version.ts @@ -0,0 +1,8 @@ +declare global { + const OPENCODE_VERSION: string + const OPENCODE_CHANNEL: string +} + +export const InstallationVersion = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local" +export const InstallationChannel = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local" +export const InstallationLocal = InstallationChannel === "local" diff --git a/packages/core/src/instruction-context.ts b/packages/core/src/instruction-context.ts new file mode 100644 index 0000000000000000000000000000000000000000..492fa42094b7ee08355462b9b2dbcf43d3b08f14 --- /dev/null +++ b/packages/core/src/instruction-context.ts @@ -0,0 +1,101 @@ +export * as InstructionContext from "./instruction-context" + +import { Array, Effect, Layer, Schema } from "effect" +import { isAbsolute, join, relative, sep } from "path" +import { FSUtil } from "./fs-util" +import { Flag } from "./flag/flag" +import { Global } from "./global" +import { Location } from "./location" +import { AbsolutePath } from "./schema" +import { SystemContext } from "./system-context/index" +import { SystemContextRegistry } from "./system-context/registry" +import { makeLocationNode } from "./effect/app-node" + +class File extends Schema.Class("InstructionContext.File")({ + path: AbsolutePath, + content: Schema.String, +}) {} + +const Files = Schema.Array(File) +const key = SystemContext.Key.make("core/instructions") + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const global = yield* Global.Service + const location = yield* Location.Service + const registry = yield* SystemContextRegistry.Service + + const source = (value: ReadonlyArray | SystemContext.Unavailable) => + SystemContext.make({ + key, + codec: Schema.toCodecJson(Files), + load: Effect.succeed(value), + baseline: render, + update: (_previous, current) => + `These instructions replace all previously loaded ambient instructions.\n\n${render(current)}`, + removed: () => "Previously loaded instructions no longer apply.", + }) + + const observe = Effect.fn("InstructionContext.observe")(function* () { + const start = yield* fs.resolve(location.directory) + const stop = yield* fs.resolve(location.project.directory) + const fromProject = relative(stop, start) + const insideProject = + fromProject === "" || (fromProject !== ".." && !fromProject.startsWith(`..${sep}`) && !isAbsolute(fromProject)) + const discovered = new Set( + yield* Effect.forEach( + Flag.OPENCODE_DISABLE_PROJECT_CONFIG || !insideProject + ? [] + : yield* fs.up({ + targets: ["AGENTS.md"], + start, + stop, + }), + fs.resolve, + ), + ) + const paths = Array.dedupe([yield* fs.resolve(join(global.config, "AGENTS.md")), ...discovered]) + const files = yield* Effect.forEach( + paths, + (path) => + fs + .readFileStringSafe(path) + .pipe( + Effect.map((content) => + content === undefined ? undefined : new File({ path: AbsolutePath.make(path), content }), + ), + ), + { concurrency: "unbounded" }, + ) + if (files.some((file, index) => file === undefined && discovered.has(paths[index]))) + return SystemContext.unavailable + return files.filter((file): file is File => file !== undefined) + }) + + yield* registry.register({ + key, + load: observe().pipe( + Effect.map((files) => + files === SystemContext.unavailable + ? source(files) + : files.length === 0 + ? SystemContext.empty + : source(files), + ), + Effect.catch(() => Effect.succeed(source(SystemContext.unavailable))), + Effect.catchDefect(() => Effect.succeed(source(SystemContext.unavailable))), + ), + }) + }), +) + +export const node = makeLocationNode({ + name: "instruction-context", + layer, + deps: [FSUtil.node, Global.node, Location.node, SystemContextRegistry.node], +}) + +function render(files: ReadonlyArray) { + return files.map((file) => `Instructions from: ${file.path}\n${file.content}`).join("\n\n") +} diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts new file mode 100644 index 0000000000000000000000000000000000000000..f61bac393a826bcd7f64914abd6aac45b1b66c99 --- /dev/null +++ b/packages/core/src/integration.ts @@ -0,0 +1,520 @@ +export * as Integration from "./integration" + +import { makeLocationNode } from "./effect/app-node" +import { + Cause, + Clock, + Context, + Duration, + Effect, + Exit, + Layer, + Schedule, + Schema, + Scope, + SynchronizedRef, + Types, +} from "effect" +import { Integration } from "@opencode-ai/schema/integration" +import { Credential } from "./credential" +import { State } from "./state" +import { EventV2 } from "./event" +import { IntegrationConnection } from "./integration/connection" + +export const ID = Integration.ID +export type ID = Integration.ID + +export const MethodID = Integration.MethodID +export type MethodID = Integration.MethodID + +export const AttemptID = Integration.AttemptID +export type AttemptID = typeof AttemptID.Type + +export const When = Integration.When +export type When = Integration.When + +export const TextPrompt = Integration.TextPrompt +export type TextPrompt = Integration.TextPrompt + +export const SelectPrompt = Integration.SelectPrompt +export type SelectPrompt = Integration.SelectPrompt + +export const Prompt = Integration.Prompt +export type Prompt = Integration.Prompt + +export const OAuthMethod = Integration.OAuthMethod +export type OAuthMethod = Integration.OAuthMethod + +export const KeyMethod = Integration.KeyMethod +export type KeyMethod = Integration.KeyMethod + +export const EnvMethod = Integration.EnvMethod +export type EnvMethod = Integration.EnvMethod + +export const Method = Integration.Method +export type Method = Integration.Method + +export const Info = Integration.Info +export type Info = Integration.Info + +export const Inputs = Integration.Inputs +export type Inputs = Integration.Inputs + +export type OAuthAuthorization = { + readonly url: string + readonly instructions: string +} & ( + | { + readonly mode: "auto" + readonly callback: Effect.Effect + } + | { + readonly mode: "code" + readonly callback: (code: string) => Effect.Effect + } +) + +export interface OAuthImplementation { + readonly integrationID: ID + readonly method: OAuthMethod + readonly authorize: (inputs: Inputs) => Effect.Effect + readonly refresh?: (credential: Credential.OAuth) => Effect.Effect + readonly label?: (credential: Credential.OAuth) => string | undefined +} + +export interface KeyImplementation { + readonly integrationID: ID + readonly method: KeyMethod +} + +export interface EnvImplementation { + readonly integrationID: ID + readonly method: EnvMethod +} + +export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation + +export const Attempt = Integration.Attempt +export type Attempt = Integration.Attempt + +export const AttemptStatus = Integration.AttemptStatus +export type AttemptStatus = typeof AttemptStatus.Type + +export class CodeRequiredError extends Schema.TaggedErrorClass()("Integration.CodeRequired", { + attemptID: AttemptID, +}) {} + +export class AuthorizationError extends Schema.TaggedErrorClass()("Integration.Authorization", { + cause: Schema.Defect(), +}) {} + +export type Error = CodeRequiredError | AuthorizationError + +export const Event = Integration.Event + +export const Ref = Integration.Ref +export type Ref = Integration.Ref + +type Entry = { + ref: Types.DeepMutable + methods: Types.DeepMutable[] + implementations: Map> +} + +type Data = { + integrations: Map +} + +export type Draft = { + list: () => readonly Ref[] + get: (id: ID) => Ref | undefined + update: (id: ID, update: (integration: Types.DeepMutable) => void) => void + remove: (id: ID) => void + method: { + list: (integrationID: ID) => readonly Method[] + update: (implementation: Implementation) => void + remove: (integrationID: ID, method: Method) => void + } +} + +export interface Interface extends State.Transformable { + /** Registers a scoped transform over the integration registry. */ + /** Returns one integration with its methods and current connections. */ + readonly get: (id: ID) => Effect.Effect + /** Returns all integrations with their methods and current connections. */ + readonly list: () => Effect.Effect + readonly connection: { + /** Returns the active connection for one integration. */ + readonly active: (id: ID) => Effect.Effect + /** Resolves a connection into usable credential material. */ + readonly resolve: ( + connection: IntegrationConnection.Info, + ) => Effect.Effect + /** Runs a key method and stores the resulting credential. */ + readonly key: (input: { + /** Integration receiving the credential. */ + readonly integrationID: ID + /** Secret entered by the user. */ + readonly key: string + /** User-facing label for the stored credential. */ + readonly label?: string + }) => Effect.Effect + /** Starts a stateful OAuth attempt. */ + readonly oauth: (input: { + /** Integration being authenticated. */ + readonly integrationID: ID + /** OAuth method selected by the caller. */ + readonly methodID: MethodID + /** Answers to the method's optional prompts. */ + readonly inputs: Inputs + /** User-facing label for the credential created on completion. */ + readonly label?: string + }) => Effect.Effect + /** Updates a stored credential exposed as a connection. */ + readonly update: ( + credentialID: Credential.ID, + updates: Partial>, + ) => Effect.Effect + /** Removes a stored credential connection. */ + readonly remove: (credentialID: Credential.ID) => Effect.Effect + } + readonly attempt: { + /** Returns the current state of an OAuth attempt. */ + readonly status: (attemptID: AttemptID) => Effect.Effect + /** Completes the attempt and stores its credential. */ + readonly complete: (input: { + /** Opaque handle returned by `oauth`. */ + readonly attemptID: AttemptID + /** Authorization code required by attempts in code mode. */ + readonly code?: string + }) => Effect.Effect + /** Cancels an attempt and releases its resources. */ + readonly cancel: (attemptID: AttemptID) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/v2/Integration") {} + +const attemptLifetime = Duration.toMillis(Duration.minutes(10)) +const terminalRetention = Duration.toMillis(Duration.minutes(1)) +const scrubInterval = Duration.seconds(30) + +type AttemptTime = { created: number; expires: number } +type PendingAttempt = { + status: "pending" + completing: boolean + authorization: OAuthAuthorization + integrationID: ID + methodID: MethodID + label?: string + scope: Scope.Closeable + time: AttemptTime +} +type TerminalAttempt = { + status: "complete" | "failed" | "expired" + message?: string + removeAt: number + time: AttemptTime +} +type AttemptEntry = PendingAttempt | TerminalAttempt + +export const locationLayer = Layer.effect( + Service, + Effect.gen(function* () { + const credentials = yield* Credential.Service + const events = yield* EventV2.Service + const scope = yield* Scope.Scope + const attempts = SynchronizedRef.makeUnsafe(new Map()) + const state = State.create({ + initial: () => ({ integrations: new Map() }), + draft: (draft) => ({ + list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[], + get: (id) => draft.integrations.get(id)?.ref as Ref | undefined, + update: (id, update) => { + const current = draft.integrations.get(id) ?? { + ref: { id, name: id }, + methods: [], + implementations: new Map(), + } + if (!draft.integrations.has(id)) draft.integrations.set(id, current) + update(current.ref) + current.ref.id = id + }, + remove: (id) => draft.integrations.delete(id), + method: { + list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [], + update: (implementation) => { + const current = draft.integrations.get(implementation.integrationID) ?? { + ref: { + id: implementation.integrationID, + name: implementation.integrationID, + }, + methods: [], + implementations: new Map>(), + } + if (!draft.integrations.has(implementation.integrationID)) { + draft.integrations.set(implementation.integrationID, current) + } + const index = current.methods.findIndex((method) => { + if (method.type !== implementation.method.type) return false + if (method.type !== "oauth" || implementation.method.type !== "oauth") return true + return method.id === implementation.method.id + }) + if (index === -1) current.methods.push(implementation.method as Types.DeepMutable) + else current.methods[index] = implementation.method as Types.DeepMutable + if (implementation.method.type === "oauth") { + current.implementations.set( + implementation.method.id, + implementation as Types.DeepMutable, + ) + } + }, + remove: (integrationID, method) => { + const current = draft.integrations.get(integrationID) + if (!current) return + const index = current.methods.findIndex((candidate) => { + if (candidate.type !== method.type) return false + if (candidate.type !== "oauth" || method.type !== "oauth") return true + return candidate.id === method.id + }) + if (index !== -1) current.methods.splice(index, 1) + if (method.type === "oauth") current.implementations.delete(method.id) + }, + }, + }), + finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + }) + + const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => { + const credentials = saved + .map((credential) => ({ + type: "credential" as const, + id: credential.id, + label: credential.label, + })) + .toReversed() + const env = (entry?.methods ?? []) + .filter((method) => method.type === "env") + .flatMap((method) => method.names.filter((name) => process.env[name])) + .map((name) => ({ type: "env" as const, name })) + return [...credentials, ...env] + } + + const project = (entry: Entry, connections: IntegrationConnection.Info[]) => + new Info({ + id: entry.ref.id, + name: entry.ref.name, + methods: entry.methods, + connections, + }) + + const authorize = (effect: Effect.Effect) => + effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause }))) + + const close = (attemptScope: Scope.Closeable) => + Scope.close(attemptScope, Exit.void).pipe(Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid) + + const message = (cause: Cause.Cause) => { + const error = Cause.squash(cause) + return error instanceof Error ? error.message : String(error) + } + + const settle = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit) { + const now = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(attempts, (current) => { + const attempt = current.get(attemptID) + if (!attempt || attempt.status !== "pending") return [undefined, current] + const terminal: TerminalAttempt = Exit.isSuccess(exit) + ? { status: "complete", time: attempt.time, removeAt: now + terminalRetention } + : { status: "failed", message: message(exit.cause), time: attempt.time, removeAt: now + terminalRetention } + return [attempt, new Map(current).set(attemptID, terminal)] + }) + if (!result) return + if (Exit.isSuccess(exit)) { + const implementation = state.get().integrations.get(result.integrationID)?.implementations.get(result.methodID) + yield* credentials.create({ + integrationID: result.integrationID, + label: result.label ?? implementation?.label?.(exit.value), + value: exit.value, + }) + yield* events.publish(Event.ConnectionUpdated, { integrationID: result.integrationID }) + yield* events.publish(Event.Updated, {}) + } + yield* close(result.scope) + }) + + const scrub = Effect.fnUntraced(function* () { + const now = yield* Clock.currentTimeMillis + const expired = yield* SynchronizedRef.modify(attempts, (current) => { + const next = new Map(current) + const scopes: Scope.Closeable[] = [] + for (const [id, attempt] of current) { + if (attempt.status === "pending" && attempt.time.expires <= now) { + scopes.push(attempt.scope) + next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention }) + continue + } + if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id) + } + return [scopes, next] + }) + yield* Effect.forEach(expired, close, { discard: true }) + }) + + yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope)) + + return Service.of({ + transform: state.transform, + reload: state.reload, + get: Effect.fn("Integration.get")(function* (id) { + const entry = state.get().integrations.get(id) + if (!entry) return undefined + return project(entry, resolveConnections(entry, yield* credentials.list(id))) + }), + list: Effect.fn("Integration.list")(function* () { + const saved = Map.groupBy(yield* credentials.all(), (credential) => credential.integrationID) + return Array.from(state.get().integrations.values(), (entry) => + project(entry, resolveConnections(entry, saved.get(entry.ref.id) ?? [])), + ).toSorted((a, b) => a.name.localeCompare(b.name)) + }), + connection: { + active: Effect.fn("Integration.connection.active")(function* (id) { + const entry = state.get().integrations.get(id) + return resolveConnections(entry, yield* credentials.list(id))[0] + }), + resolve: Effect.fn("Integration.connection.resolve")(function* (connection) { + if (connection.type === "env") { + const key = process.env[connection.name] + return key ? Credential.Key.make({ type: "key", key }) : undefined + } + const credential = yield* credentials.get(connection.id) + if (!credential) return undefined + if (credential.value.type === "key") return credential.value + const implementation = state + .get() + .integrations.get(credential.integrationID) + ?.implementations.get(credential.value.methodID) + if (!implementation?.refresh) return credential.value + const now = yield* Clock.currentTimeMillis + if (credential.value.expires > now + Duration.toMillis(Duration.minutes(5))) return credential.value + const value = yield* authorize(implementation.refresh(credential.value)) + yield* credentials.update(credential.id, { value }) + return value + }), + key: Effect.fn("Integration.connection.key")(function* (input) { + const method = state + .get() + .integrations.get(input.integrationID) + ?.methods.some((method) => method.type === "key") + if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`) + yield* credentials.create({ + integrationID: input.integrationID, + label: input.label, + value: Credential.Key.make({ type: "key", key: input.key }), + }) + yield* events.publish(Event.ConnectionUpdated, { integrationID: input.integrationID }) + yield* events.publish(Event.Updated, {}) + }), + oauth: Effect.fn("Integration.connection.oauth")(function* (input) { + const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID) + if (!method) { + return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`) + } + const attemptScope = yield* Scope.fork(scope) + const authorization = yield* authorize(method.authorize(input.inputs)).pipe( + Scope.provide(attemptScope), + Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)), + ) + const id = AttemptID.create() + const created = yield* Clock.currentTimeMillis + const time = { created, expires: created + attemptLifetime } + yield* SynchronizedRef.update(attempts, (current) => + new Map(current).set(id, { + status: "pending", + completing: authorization.mode === "auto", + authorization, + integrationID: input.integrationID, + methodID: input.methodID, + label: input.label, + scope: attemptScope, + time, + }), + ) + if (authorization.mode === "auto") { + yield* authorization.callback.pipe( + Effect.exit, + Effect.flatMap((exit) => settle(id, exit)), + Effect.forkIn(attemptScope, { startImmediately: true }), + ) + } + return new Attempt({ + attemptID: id, + url: authorization.url, + instructions: authorization.instructions, + mode: authorization.mode, + time, + }) + }), + update: Effect.fn("Integration.connection.update")(function* (credentialID, updates) { + const credential = yield* credentials.get(credentialID) + yield* credentials.update(credentialID, updates) + if (credential) { + yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) + } + yield* events.publish(Event.Updated, {}) + }), + remove: Effect.fn("Integration.connection.remove")(function* (credentialID) { + const credential = yield* credentials.get(credentialID) + yield* credentials.remove(credentialID) + if (credential) { + yield* events.publish(Event.ConnectionUpdated, { integrationID: credential.integrationID }) + } + yield* events.publish(Event.Updated, {}) + }), + }, + attempt: { + status: Effect.fn("Integration.attempt.status")(function* (attemptID) { + const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID) + if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`) + if (attempt.status === "failed") { + return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time } + } + return { status: attempt.status, time: attempt.time } + }), + complete: Effect.fn("Integration.attempt.complete")(function* (input) { + const attempt = yield* SynchronizedRef.modify(attempts, (current) => { + const match = current.get(input.attemptID) + if (!match || match.status !== "pending" || match.completing) return [match, current] + if (match.authorization.mode === "code" && input.code === undefined) return [match, current] + return [match, new Map(current).set(input.attemptID, { ...match, completing: true })] + }) + if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`) + if (attempt.status !== "pending") return + if (attempt.authorization.mode === "code" && input.code === undefined) { + return yield* new CodeRequiredError({ attemptID: input.attemptID }) + } + if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`) + const callback = + attempt.authorization.mode === "auto" + ? attempt.authorization.callback + : attempt.authorization.callback(input.code as string) + const exit = yield* authorize(callback).pipe(Effect.exit) + yield* settle(input.attemptID, exit) + if (Exit.isFailure(exit)) return yield* exit + }), + cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) { + const attempt = yield* SynchronizedRef.modify(attempts, (current) => { + const match = current.get(attemptID) + if (!match || match.status !== "pending") return [undefined, current] + const next = new Map(current) + next.delete(attemptID) + return [match, next] + }) + if (attempt) yield* Scope.close(attempt.scope, Exit.void) + }), + }, + }) + }), +) + +export const node = makeLocationNode({ service: Service, layer: locationLayer, deps: [Credential.node, EventV2.node] }) diff --git a/packages/core/src/integration/connection.ts b/packages/core/src/integration/connection.ts new file mode 100644 index 0000000000000000000000000000000000000000..91ab7e9e4049b8f60530f46544ad61723ff0a04a --- /dev/null +++ b/packages/core/src/integration/connection.ts @@ -0,0 +1,12 @@ +export * as IntegrationConnection from "./connection" + +import { Connection } from "@opencode-ai/schema/connection" + +export const CredentialInfo = Connection.CredentialInfo +export type CredentialInfo = Connection.CredentialInfo + +export const EnvInfo = Connection.EnvInfo +export type EnvInfo = Connection.EnvInfo + +export const Info = Connection.Info +export type Info = Connection.Info diff --git a/packages/core/src/location-mutation.ts b/packages/core/src/location-mutation.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f410b95b1b07232941afe1ad70270acba33ad48 --- /dev/null +++ b/packages/core/src/location-mutation.ts @@ -0,0 +1,162 @@ +export * as LocationMutation from "./location-mutation" + +import { makeLocationNode } from "./effect/app-node" +import path from "path" +import { Context, Effect, Layer, Schema } from "effect" +import { FSUtil } from "./fs-util" +import { Location } from "./location" + +export const Kind = Schema.Literals(["file", "directory"]) +export type Kind = typeof Kind.Type + +/** + * Mutation paths do not accept project references. Relative paths must stay + * inside the active Location. Absolute paths outside it require separate + * `external_directory` approval. + */ +export const ResolveInput = Schema.Struct({ + path: Schema.String, + /** Selects the external approval boundary; it does not validate the target type. */ + kind: Kind.pipe(Schema.optional), +}) +export type ResolveInput = typeof ResolveInput.Type + +export class PathError extends Schema.TaggedErrorClass()("LocationMutation.PathError", { + path: Schema.String, + reason: Schema.Literals(["relative_escape", "location_escape", "non_directory_ancestor"]), +}) {} + +export interface ExternalDirectoryAuthorization { + readonly action: "external_directory" + /** Canonical existing directory used as the external approval boundary. */ + readonly directory: string + /** `external_directory` permission resource. */ + readonly resource: string + readonly save: string +} + +export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({ + action: input.action, + resources: [input.resource], + save: [input.save], +}) + +export interface Target { + /** Canonical existing path, or missing path below a canonical directory. */ + readonly canonical: string + /** Permission resource: Location-relative for internal paths, canonical for external paths. */ + readonly resource: string + readonly externalDirectory?: ExternalDirectoryAuthorization +} + +export interface Interface { + /** + * Resolve a path and derive its permission resources. Relative paths must + * stay inside the Location. Absolute paths outside it require separate + * `external_directory` approval. This does not approve the mutation. + */ + readonly resolve: (input: ResolveInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/LocationMutation") {} + +interface ResolvedPath { + readonly canonical: string + readonly type?: + | "File" + | "Directory" + | "SymbolicLink" + | "BlockDevice" + | "CharacterDevice" + | "FIFO" + | "Socket" + | "Unknown" + readonly directory: string +} + +const slash = (value: string) => value.replaceAll("\\", "/") + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const location = yield* Location.Service + const locationRoot = yield* fs.realPath(location.directory) + + function notFound(effect: Effect.Effect) { + return effect.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined))) + } + + const resolvePath = Effect.fnUntraced(function* (absolute: string) { + const existing = yield* notFound(fs.realPath(absolute)) + if (existing !== undefined) { + const info = yield* fs.stat(existing) + return { + canonical: existing, + type: info.type, + directory: info.type === "Directory" ? existing : path.dirname(existing), + } satisfies ResolvedPath + } + + let anchor = path.dirname(absolute) + while (true) { + const canonical = yield* notFound(fs.realPath(anchor)) + if (canonical !== undefined) { + const info = yield* fs.stat(canonical) + if (info.type !== "Directory") { + return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" }) + } + return { + canonical: path.resolve(canonical, path.relative(anchor, absolute)), + directory: canonical, + } satisfies ResolvedPath + } + const parent = path.dirname(anchor) + if (parent === anchor) return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" }) + anchor = parent + } + }) + + const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) { + const relative = !path.isAbsolute(input.path) + const absolute = path.resolve(location.directory, input.path) + const lexicallyInternal = FSUtil.contains(location.directory, absolute) + if (relative && !lexicallyInternal) return yield* new PathError({ path: input.path, reason: "relative_escape" }) + + const resolved = yield* resolvePath(absolute) + if (lexicallyInternal && !FSUtil.contains(locationRoot, resolved.canonical)) { + return yield* new PathError({ path: input.path, reason: "location_escape" }) + } + + const external = !lexicallyInternal + const resource = external + ? slash(resolved.canonical) + : slash(path.relative(locationRoot, resolved.canonical) || ".") + const externalDirectory = + input.kind === "directory" && resolved.type === "Directory" ? resolved.canonical : resolved.directory + const externalResource = slash(path.join(externalDirectory, "*")) + return { + canonical: resolved.canonical, + resource, + externalDirectory: external + ? { + action: "external_directory", + directory: externalDirectory, + resource: externalResource, + save: externalResource, + } + : undefined, + } satisfies Target + }) + + return Service.of({ resolve }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ + service: Service, + layer: layer.pipe(Layer.orDie), + deps: [FSUtil.node, Location.node], +}) diff --git a/packages/core/src/location-service-map.ts b/packages/core/src/location-service-map.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a6c26737dd5d0142cbb1c5aafa8b97c135f32fa --- /dev/null +++ b/packages/core/src/location-service-map.ts @@ -0,0 +1,18 @@ +import { Context, Effect, Layer, LayerMap } from "effect" +import { LayerNode } from "./effect/layer-node" +import { Node } from "./effect/app-node" +import { Location } from "./location" +import type { LocationError, LocationServices } from "./location-services" + +export class Service extends Context.Service< + Service, + LayerMap.LayerMap +>()("@opencode/example/LocationServiceMap") { + static get(ref: Location.Ref) { + return Layer.unwrap(Effect.map(Service, (locations) => locations.get(ref))) + } +} + +export const node = LayerNode.unbound(Service, Node.tags.values.global) + +export * as LocationServiceMap from "./location-service-map" diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts new file mode 100644 index 0000000000000000000000000000000000000000..7da67673c31982abafc856f34fb52a2c82891525 --- /dev/null +++ b/packages/core/src/location-services.ts @@ -0,0 +1,115 @@ +import { Effect, Layer, LayerMap } from "effect" +import { AgentV2 } from "./agent" +import { AISDK } from "./aisdk" +import { Catalog } from "./catalog" +import { CommandV2 } from "./command" +import { Config } from "./config" +import { LayerNode } from "./effect/layer-node" +import { Node } from "./effect/app-node" +import { FileMutation } from "./file-mutation" +import { FileSystem } from "./filesystem" +import { FileSystemSearch } from "./filesystem/search" +import { Watcher } from "./filesystem/watcher" +import { Image } from "./image" +import { Integration } from "./integration" +import { Location } from "./location" +import { LocationMutation } from "./location-mutation" +import { LocationServiceMap } from "./location-service-map" +import { PermissionV2 } from "./permission" +import { PluginV2 } from "./plugin" +import { PluginInternal } from "./plugin/internal" +import { Policy } from "./policy" +import { ProjectCopy } from "./project/copy" +import { Pty } from "./pty" +import { QuestionV2 } from "./question" +import { Reference } from "./reference" +import { ReferenceGuidance } from "./reference/guidance" +import * as SessionRunnerLLM from "./session/runner/llm" +import { SessionRunnerModel } from "./session/runner/model" +import { SessionTodo } from "./session/todo" +import { SkillV2 } from "./skill" +import { SkillGuidance } from "./skill/guidance" +import { Snapshot } from "./snapshot" +import { SystemContextBuiltIns } from "./system-context/builtins" +import { SystemContextRegistry } from "./system-context/registry" +import { BuiltInTools } from "./tool/builtins" +import { ReadToolFileSystem } from "./tool/read-filesystem" +import { ToolRegistry } from "./tool/registry" +import { ToolOutputStore } from "./tool-output-store" + +export { LocationServiceMap } from "./location-service-map" + +export const locationServices = LayerNode.group([ + Location.node, + Policy.node, + Config.node, + AgentV2.node, + CommandV2.node, + Reference.node, + Integration.node, + Catalog.node, + AISDK.node, + PluginV2.node, + PluginInternal.node, + ProjectCopy.node, + ProjectCopy.refreshNode, + FileSystemSearch.node, + FileSystem.node, + Watcher.node, + Pty.node, + SkillV2.node, + SystemContextRegistry.node, + SystemContextBuiltIns.node, + LocationMutation.node, + FileMutation.node, + PermissionV2.node, + ToolOutputStore.node, + ToolRegistry.node, + ToolRegistry.toolsNode, + Image.node, + SkillGuidance.node, + ReferenceGuidance.node, + SessionTodo.node, + QuestionV2.node, + ReadToolFileSystem.node, + BuiltInTools.node, + SessionRunnerModel.node, + Snapshot.node, + SessionRunnerLLM.node, +]) + +export type LocationServices = LayerNode.Output +export type LocationError = LayerNode.Error + +export function buildLocationServiceMap( + replacements: LayerNode.Replacements = [], +): Layer.Layer { + return Layer.effect( + LocationServiceMap.Service, + LayerMap.make( + (ref: Location.Ref) => { + const allReplacements = replacements.concat([[Location.node, Location.boundNode(ref)]]) + // Apply replacements during hoist, not afterward: replacements can + // introduce new tagged dependencies (Location.boundNode depends on + // Project), and the hoist walk is the only pass that can still slice + // those back out. + const location = LayerNode.hoist(locationServices, Node.tags.values.global, allReplacements) + + return LayerNode.compile(location.node).pipe( + Layer.fresh, + Layer.tap(() => + Effect.logInfo("booting location services", { + directory: ref.directory, + workspaceID: ref.workspaceID, + }), + ), + Layer.provide(LayerNode.compile(location.hoisted)), + ) + }, + { idleTimeToLive: "60 minutes" }, + ), + ) +} + +// This is temporary for backwards compatibility +export const locationServiceMapLayer = buildLocationServiceMap() diff --git a/packages/core/src/location.ts b/packages/core/src/location.ts new file mode 100644 index 0000000000000000000000000000000000000000..8228b8599ea3b26a7212ab576dab0f43d3b80a78 --- /dev/null +++ b/packages/core/src/location.ts @@ -0,0 +1,39 @@ +import { Context, Effect, Layer } from "effect" +import { Info, Ref, response } from "@opencode-ai/schema/location" +import { Project } from "./project" +import { LayerNode } from "./effect/layer-node" +import { makeLocationNode, tags } from "./effect/app-node" + +export * as Location from "./location" + +export { Info, Ref, response } + +export interface Interface extends Info { + readonly vcs?: Project.Vcs +} + +export class Service extends Context.Service()("@opencode/Location") {} + +export const node = LayerNode.unbound(Service, tags.values.location) + +const layer = (ref: Ref) => + Layer.effect( + Service, + Effect.gen(function* () { + const project = yield* Project.Service + const resolved = yield* project.resolve(ref.directory) + return Service.of({ + directory: ref.directory, + workspaceID: ref.workspaceID, + project: { id: resolved.id, directory: resolved.directory }, + vcs: resolved.vcs, + }) + }), + ) + +export const boundNode = (ref: Ref) => + makeLocationNode({ + service: Service, + layer: layer(ref), + deps: [Project.node], + }) diff --git a/packages/core/src/markdown.d.ts b/packages/core/src/markdown.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb3e3b92d663efa9ff0c4b1ff21f15a33e7b67ab --- /dev/null +++ b/packages/core/src/markdown.d.ts @@ -0,0 +1,4 @@ +declare module "*.md" { + const content: string + export default content +} diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts new file mode 100644 index 0000000000000000000000000000000000000000..52fff98733b11acdd292115bd4dabe320ebc592f --- /dev/null +++ b/packages/core/src/model.ts @@ -0,0 +1,41 @@ +import { Types } from "effect" +import { Model } from "@opencode-ai/schema/model" +import { ProviderV2 } from "./provider" + +export const ID = Model.ID +export type ID = typeof ID.Type + +export const VariantID = Model.VariantID +export type VariantID = typeof VariantID.Type + +// Grouping of models, eg claude opus, claude sonnet +export const Family = Model.Family +export type Family = Model.Family + +export const Capabilities = Model.Capabilities +export type Capabilities = Model.Capabilities + +export const Cost = Model.Cost + +export const Ref = Model.Ref +export type Ref = typeof Ref.Type + +export const Api = Model.Api +export type Api = Model.Api + +export const Info = Model.Info +export type Info = Model.Info + +export type MutableInfo = Omit, "api"> & { + api: ProviderV2.MutableApi +} + +export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } { + const [providerID, ...modelID] = input.split("/") + return { + providerID: ProviderV2.ID.make(providerID), + modelID: ID.make(modelID.join("/")), + } +} + +export * as ModelV2 from "./model" diff --git a/packages/core/src/models-dev.ts b/packages/core/src/models-dev.ts new file mode 100644 index 0000000000000000000000000000000000000000..e577935f067ac5462022a912554279310cfdaa4d --- /dev/null +++ b/packages/core/src/models-dev.ts @@ -0,0 +1,266 @@ +import path from "path" +import { Context, Duration, Effect, Layer, Option, Schedule, Schema } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ModelsDev } from "@opencode-ai/schema/models-dev" +import { Global } from "./global" +import { Flag } from "./flag/flag" +import { Flock } from "./util/flock" +import { Hash } from "./util/hash" +import { FSUtil } from "./fs-util" +import { InstallationChannel, InstallationVersion } from "./installation/version" +import { EventV2 } from "./event" +import { makeGlobalNode } from "./effect/app-node" +import { httpClient } from "./effect/app-node-platform" + +export const CatalogModelStatus = Schema.Literals(["alpha", "beta", "deprecated"]) +export type CatalogModelStatus = typeof CatalogModelStatus.Type + +const InterleavedField = Schema.Union([ + Schema.Literals(["reasoning", "reasoning_content", "reasoning_text"]), + Schema.String, +]) + +const USER_AGENT = `opencode/${InstallationChannel}/${InstallationVersion}/${Flag.OPENCODE_CLIENT}` + +const CostTier = Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + cache_read: Schema.optional(Schema.Finite), + cache_write: Schema.optional(Schema.Finite), + tier: Schema.Struct({ + type: Schema.Literal("context"), + size: Schema.Finite, + }), +}) + +const Cost = Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + cache_read: Schema.optional(Schema.Finite), + cache_write: Schema.optional(Schema.Finite), + tiers: Schema.optional(Schema.Array(CostTier)), + context_over_200k: Schema.optional( + Schema.Struct({ + input: Schema.Finite, + output: Schema.Finite, + cache_read: Schema.optional(Schema.Finite), + cache_write: Schema.optional(Schema.Finite), + }), + ), +}) + +const ReasoningOption = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("effort"), + values: Schema.Array(Schema.NullOr(Schema.String)), + }), + Schema.Struct({ + type: Schema.Literal("toggle"), + }), + Schema.Struct({ + type: Schema.Literal("budget_tokens"), + min: Schema.optional(Schema.Finite), + max: Schema.optional(Schema.Finite), + }), +]) + +export const Model = Schema.Struct({ + id: Schema.String, + name: Schema.String, + family: Schema.optional(Schema.String), + release_date: Schema.String, + attachment: Schema.Boolean, + reasoning: Schema.Boolean, + temperature: Schema.Boolean, + tool_call: Schema.Boolean, + reasoning_options: Schema.optional(Schema.Array(ReasoningOption)), + interleaved: Schema.optional( + Schema.Union([ + Schema.Boolean, + InterleavedField, + Schema.Struct({ + field: InterleavedField, + }), + ]), + ), + cost: Schema.optional(Cost), + limit: Schema.Struct({ + context: Schema.Finite, + input: Schema.optional(Schema.Finite), + output: Schema.Finite, + }), + modalities: Schema.optional( + Schema.Struct({ + input: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])), + output: Schema.Array(Schema.Literals(["text", "audio", "image", "video", "pdf"])), + }), + ), + experimental: Schema.optional( + Schema.Struct({ + modes: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + cost: Schema.optional(Cost), + provider: Schema.optional( + Schema.Struct({ + body: Schema.optional(Schema.Record(Schema.String, Schema.MutableJson)), + headers: Schema.optional(Schema.Record(Schema.String, Schema.String)), + }), + ), + }), + ), + ), + }), + ), + status: Schema.optional(CatalogModelStatus), + provider: Schema.optional( + Schema.Struct({ npm: Schema.optional(Schema.String), api: Schema.optional(Schema.String) }), + ), +}) +export type Model = Schema.Schema.Type + +export const Provider = Schema.Struct({ + api: Schema.optional(Schema.String), + name: Schema.String, + env: Schema.Array(Schema.String), + id: Schema.String, + npm: Schema.optional(Schema.String), + models: Schema.Record(Schema.String, Model), +}) + +export type Provider = Schema.Schema.Type + +export const Event = ModelsDev.Event + +declare const OPENCODE_MODELS_DEV: Record | undefined + +export interface Interface { + readonly get: () => Effect.Effect> + readonly refresh: (force?: boolean) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ModelsDev") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const events = yield* EventV2.Service + const http = HttpClient.filterStatusOk( + (yield* HttpClient.HttpClient).pipe( + HttpClient.retryTransient({ + retryOn: "errors-and-responses", + times: 2, + schedule: Schedule.exponential(200).pipe(Schedule.jittered), + }), + ), + ) + + const source = Flag.OPENCODE_MODELS_URL || "https://models.opencode.ai" + const filepath = path.join( + Global.Path.cache, + source === "https://models.opencode.ai" ? "models.json" : `models-${Hash.fast(source)}.json`, + ) + const ttl = Duration.minutes(5) + const lockKey = `models-dev:${filepath}` + + const fresh = Effect.fnUntraced(function* () { + const stat = yield* fs.stat(filepath).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (!stat) return false + const mtime = Option.getOrElse(stat.mtime, () => new Date(0)).getTime() + return Date.now() - mtime < Duration.toMillis(ttl) + }) + + const fetchApi = Effect.fn("ModelsDev.fetchApi")(function* () { + return yield* HttpClientRequest.get(`${source}/api.json`).pipe( + HttpClientRequest.setHeader("User-Agent", USER_AGENT), + http.execute, + Effect.flatMap((res) => res.text), + Effect.timeout("10 seconds"), + ) + }) + + const loadFromDisk = fs.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).pipe( + Effect.catch((error) => { + if ( + Flag.OPENCODE_MODELS_PATH === undefined && + error._tag === "FileSystemError" && + error.method === "readJson" + ) { + return fs.remove(filepath, { force: true }).pipe(Effect.ignore, Effect.as(undefined)) + } + return Effect.succeed(undefined) + }), + Effect.map((v) => v as Record | undefined), + ) + + const loadSnapshot = Effect.sync(() => + typeof OPENCODE_MODELS_DEV === "undefined" ? undefined : OPENCODE_MODELS_DEV, + ) + + const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () { + const text = yield* fetchApi() + const tempfile = `${filepath}.${process.pid}.${Date.now()}.tmp` + yield* fs.writeWithDirs(tempfile, text).pipe( + Effect.andThen(fs.rename(tempfile, filepath)), + Effect.catch((error) => + Effect.gen(function* () { + yield* fs.remove(tempfile, { force: true }).pipe(Effect.ignore) + return yield* Effect.fail(error) + }), + ), + ) + return text + }) + + const populate = Effect.gen(function* () { + const fromDisk = yield* loadFromDisk + if (fromDisk) return fromDisk + const snapshot = yield* loadSnapshot + if (snapshot) return snapshot + if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {} + // Flock is cross-process: concurrent opencode CLIs can race on this cache file. + const text = yield* Effect.scoped( + Effect.gen(function* () { + yield* Flock.effect(lockKey) + return yield* fetchAndWrite() + }), + ) + return JSON.parse(text) as Record + }).pipe(Effect.withSpan("ModelsDev.populate"), Effect.orDie) + + const [cachedGet, invalidate] = yield* Effect.cachedInvalidateWithTTL(populate, Duration.infinity) + + const get = (): Effect.Effect> => cachedGet + + const refresh = Effect.fn("ModelsDev.refresh")(function* (force = false) { + if (!force && (yield* fresh())) return + yield* Effect.scoped( + Effect.gen(function* () { + yield* Flock.effect(lockKey) + // Re-check under the lock: another process may have refreshed between + // our outer check and lock acquisition. + if (!force && (yield* fresh())) return + yield* fetchAndWrite() + yield* invalidate + yield* events.publish(Event.Refreshed, {}) + }), + ).pipe( + Effect.tapCause((cause) => Effect.logError("Failed to fetch models.dev", { cause: cause })), + Effect.ignore, + ) + }) + + if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) { + // Schedule.spaced runs the effect once, then waits between completions. + yield* Effect.forkScoped(refresh().pipe(Effect.repeat(Schedule.spaced("60 minutes")), Effect.ignore)) + } + + return Service.of({ get, refresh }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [FSUtil.node, EventV2.node, httpClient] }) + +export * as ModelsDev from "./models-dev" diff --git a/packages/core/src/npm-config.ts b/packages/core/src/npm-config.ts new file mode 100644 index 0000000000000000000000000000000000000000..896bb84872c1ffce95a81d6562214468769dcab3 --- /dev/null +++ b/packages/core/src/npm-config.ts @@ -0,0 +1,40 @@ +export * as NpmConfig from "./npm-config" + +import { fileURLToPath } from "url" +// @ts-expect-error npm does not publish types for this internal config API. +import Config from "@npmcli/config" +// @ts-expect-error npm does not publish types for this internal config API. +import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" +import { Effect } from "effect" + +const npmPath = fileURLToPath(new URL("..", import.meta.url)) + +export const load = (dir: string) => + Effect.tryPromise({ + try: async () => { + const config = new Config({ + npmPath, + cwd: dir, + env: { ...process.env }, + argv: [process.execPath, process.execPath], + execPath: process.execPath, + platform: process.platform, + definitions, + flatten, + nerfDarts, + shorthands, + warn: false, + }) + await config.load() + return config.flat as Record + }, + catch: (cause) => cause, + }).pipe(Effect.orElseSucceed(() => ({}) as Record)) + +export const registry = (dir: string) => + load(dir).pipe( + Effect.map((config) => { + const registry = typeof config.registry === "string" ? config.registry : "https://registry.npmjs.org" + return registry.endsWith("/") ? registry.slice(0, -1) : registry + }), + ) diff --git a/packages/core/src/npm.ts b/packages/core/src/npm.ts new file mode 100644 index 0000000000000000000000000000000000000000..30e12cff1206c7c9fa4e7279d4aaab67c72187d7 --- /dev/null +++ b/packages/core/src/npm.ts @@ -0,0 +1,269 @@ +export * as Npm from "./npm" + +import path from "path" +import npa from "npm-package-arg" +import { Effect, Schema, Context, Layer, Option, FileSystem } from "effect" +import { NodeFileSystem } from "@effect/platform-node" +import { FSUtil } from "./fs-util" +import { Global } from "./global" +import { EffectFlock } from "./util/effect-flock" +import { makeGlobalNode } from "./effect/app-node" +import { filesystem } from "./effect/app-node-platform" +import { LayerNode } from "./effect/layer-node" +import { makeRuntime } from "./effect/runtime" +import { NpmConfig } from "./npm-config" + +export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { + add: Schema.Array(Schema.String).pipe(Schema.optional), + dir: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} + +export interface EntryPoint { + readonly directory: string + readonly entrypoint?: string +} + +export interface Interface { + readonly add: (pkg: string) => Effect.Effect + readonly install: ( + dir: string, + input?: { + add: { + name: string + version?: string + }[] + }, + ) => Effect.Effect + readonly which: (pkg: string, bin?: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/Npm") {} + +const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined + +export function sanitize(pkg: string) { + if (!illegal) return pkg + return Array.from(pkg, (char) => (illegal.has(char) || char.charCodeAt(0) < 32 ? "_" : char)).join("") +} + +const resolveEntryPoint = (name: string, dir: string): EntryPoint => { + let entrypoint: string | undefined + try { + entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir) + } catch { + entrypoint = undefined + } + return { + directory: dir, + entrypoint, + } +} + +interface ArboristNode { + name: string + path: string +} + +interface ArboristTree { + edgesOut: Map +} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const afs = yield* FSUtil.Service + const global = yield* Global.Service + const fs = yield* FileSystem.FileSystem + const flock = yield* EffectFlock.Service + const directory = (pkg: string) => path.join(global.cache, "packages", sanitize(pkg)) + const reify = (input: { dir: string; add?: string[] }) => + Effect.gen(function* () { + yield* flock.acquire(`npm-install:${input.dir}`) + const { Arborist } = yield* Effect.promise(() => import("@npmcli/arborist")) + const add = input.add ?? [] + const npmOptions = yield* NpmConfig.load(input.dir) + const arborist = new Arborist({ + ...npmOptions, + path: input.dir, + binLinks: true, + progress: false, + savePrefix: "", + ignoreScripts: true, + }) + return yield* Effect.tryPromise({ + try: () => + arborist.reify({ + ...npmOptions, + add, + save: true, + saveType: "prod", + }), + catch: (cause) => + new InstallFailedError({ + cause, + add, + dir: input.dir, + }), + }) as Effect.Effect + }).pipe( + Effect.withSpan("Npm.reify", { + attributes: input, + }), + ) + + const add = Effect.fn("Npm.add")(function* (pkg: string) { + const dir = directory(pkg) + const name = (() => { + try { + return npa(pkg).name ?? pkg + } catch { + return pkg + } + })() + + if (yield* afs.existsSafe(path.join(dir, "node_modules", name))) { + return resolveEntryPoint(name, path.join(dir, "node_modules", name)) + } + + const tree = yield* reify({ dir, add: [pkg] }) + const first = tree.edgesOut.values().next().value?.to + if (!first) { + const result = resolveEntryPoint(name, path.join(dir, "node_modules", name)) + if (result.entrypoint) return result + return yield* new InstallFailedError({ add: [pkg], dir }) + } + return resolveEntryPoint(first.name, first.path) + }, Effect.scoped) + + const install: Interface["install"] = Effect.fn("Npm.install")(function* (dir, input) { + const canWrite = yield* afs.access(dir, { writable: true }).pipe( + Effect.as(true), + Effect.orElseSucceed(() => false), + ) + if (!canWrite) return + + const add = input?.add.map((pkg) => [pkg.name, pkg.version].filter(Boolean).join("@")) ?? [] + if ( + yield* Effect.gen(function* () { + const nodeModulesExists = yield* afs.existsSafe(path.join(dir, "node_modules")) + if (!nodeModulesExists) { + yield* reify({ add, dir }) + return true + } + return false + }).pipe(Effect.withSpan("Npm.checkNodeModules")) + ) + return + + yield* Effect.gen(function* () { + const pkg = yield* afs.readJson(path.join(dir, "package.json")).pipe(Effect.orElseSucceed(() => ({}))) + const lock = yield* afs.readJson(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => ({}))) + + const pkgAny = pkg as any + const lockAny = lock as any + const declared = new Set([ + ...Object.keys(pkgAny?.dependencies || {}), + ...Object.keys(pkgAny?.devDependencies || {}), + ...Object.keys(pkgAny?.peerDependencies || {}), + ...Object.keys(pkgAny?.optionalDependencies || {}), + ...(input?.add || []).map((pkg) => pkg.name), + ]) + + const root = lockAny?.packages?.[""] || {} + const locked = new Set([ + ...Object.keys(root?.dependencies || {}), + ...Object.keys(root?.devDependencies || {}), + ...Object.keys(root?.peerDependencies || {}), + ...Object.keys(root?.optionalDependencies || {}), + ]) + + for (const name of declared) { + if (!locked.has(name)) { + yield* reify({ dir, add }) + return + } + } + }).pipe(Effect.withSpan("Npm.checkDirty")) + + return + }, Effect.scoped) + + const which = Effect.fn("Npm.which")(function* (pkg: string, bin?: string) { + const dir = directory(pkg) + const binDir = path.join(dir, "node_modules", ".bin") + + const pick = Effect.fnUntraced(function* () { + const files = yield* fs.readDirectory(binDir).pipe(Effect.catch(() => Effect.succeed([] as string[]))) + + if (files.length === 0) return Option.none() + // Caller picked a specific bin (e.g. pyright exposes both `pyright` and + // `pyright-langserver`); trust the hint if the package provides it. + if (bin) return files.includes(bin) ? Option.some(bin) : Option.none() + if (files.length === 1) return Option.some(files[0]) + + const pkgJson = yield* afs.readJson(path.join(dir, "node_modules", pkg, "package.json")).pipe(Effect.option) + + if (Option.isSome(pkgJson)) { + const parsed = pkgJson.value as { bin?: string | Record } + if (parsed?.bin) { + const unscoped = pkg.startsWith("@") ? pkg.split("/")[1] : pkg + const parsedBin = parsed.bin + if (typeof parsedBin === "string") return Option.some(unscoped) + const keys = Object.keys(parsedBin) + if (keys.length === 1) return Option.some(keys[0]) + return parsedBin[unscoped] ? Option.some(unscoped) : Option.some(keys[0]) + } + } + + return Option.some(files[0]) + }) + + return Option.getOrUndefined( + yield* Effect.gen(function* () { + const bin = yield* pick() + if (Option.isSome(bin)) { + return Option.some(path.join(binDir, bin.value)) + } + + yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {})) + + yield* add(pkg) + + const resolved = yield* pick() + if (Option.isNone(resolved)) return Option.none() + return Option.some(path.join(binDir, resolved.value)) + }).pipe( + Effect.scoped, + Effect.orElseSucceed(() => Option.none()), + ), + ) + }) + + return Service.of({ + add, + install, + which, + }) + }), +) + +export const node = makeGlobalNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, Global.node, filesystem, EffectFlock.node], +}) + +const { runPromise } = makeRuntime(Service, LayerNode.compile(node)) + +export async function install(...args: Parameters) { + return runPromise((svc) => svc.install(...args)) +} + +export async function add(...args: Parameters) { + return runPromise((svc) => svc.add(...args)) +} + +export async function which(...args: Parameters) { + return runPromise((svc) => svc.which(...args)) +} diff --git a/packages/core/src/oauth/page.ts b/packages/core/src/oauth/page.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d3e29f67bdbc11b8cad267eeced904b34903a5d --- /dev/null +++ b/packages/core/src/oauth/page.ts @@ -0,0 +1,276 @@ +// Branded HTML pages for local OAuth callback servers. +// +// These are served by the loopback HTTP servers that finish an OAuth exchange +// (MCP, Codex/ChatGPT, xAI, Snowflake, DigitalOcean, ...). The functions return +// a fully self-contained HTML string with no external assets, so they work +// offline and drop into any transport (`res.end(...)`, Effect `response.end`, +// etc.). +// +// The visual language mirrors the OpenCode app: the design tokens are a curated +// subset of the OC-2 semantic tokens in `packages/ui/src/styles/theme.css`, and +// the wordmark is the same geometry as `packages/ui/src/components/logo.tsx`. +// Keep this file in sync with those sources when the brand changes. + +export interface CallbackPageOptions { + /** Friendly integration name shown as a subtitle, e.g. "xAI", "Snowflake", "MCP". */ + provider?: string + /** Attempt to close the window shortly after success. Defaults to true. */ + autoClose?: boolean +} + +export function success(options?: CallbackPageOptions) { + const provider = options?.provider + return renderDocument({ + title: "Authorization successful", + body: renderCard({ + status: "success", + headline: "Authorization successful", + message: provider ? `OpenCode is now connected to ${escapeHtml(provider)}.` : "OpenCode is now authorized.", + footnote: "You can close this window.", + }), + script: options?.autoClose === false ? undefined : AUTO_CLOSE_SCRIPT, + }) +} + +export function error(detail: string, options?: CallbackPageOptions) { + const provider = options?.provider + return renderDocument({ + title: "Authorization failed", + body: renderCard({ + status: "error", + headline: "Authorization failed", + message: provider + ? `OpenCode couldn't finish connecting to ${escapeHtml(provider)}.` + : "OpenCode couldn't complete authorization.", + detail, + footnote: "Close this window and try again from OpenCode.", + }), + }) +} + +export interface BootstrapOptions { + /** Same-origin path the in-browser script POSTs the parsed callback to. */ + tokenPath: string + provider?: string +} + +// For flows where the credential arrives in the URL fragment (implicit grant), +// the browser must relay it back to the loopback server. This renders a pending +// page whose script reads the fragment, POSTs it to `tokenPath`, then resolves +// to the success or error state in place. +export function bootstrap(options: BootstrapOptions) { + return renderDocument({ + title: "Finishing sign-in", + body: renderCard({ + status: "pending", + headline: "Finishing sign-in", + message: options.provider + ? `Completing your ${escapeHtml(options.provider)} authorization.` + : "Completing authorization.", + footnote: "You can close this window once sign-in finishes.", + }), + script: bootstrapScript(options), + }) +} + +export * as OauthCallbackPage from "./page" + +type Status = "pending" | "success" | "error" + +function renderCard(input: { status: Status; headline: string; message: string; detail?: string; footnote: string }) { + const detail = input.detail?.trim() + return `
+
${WORDMARK}
+ +

${escapeHtml(input.headline)}

+

${input.message}

+
${detail ? escapeHtml(detail) : ""}
+

${escapeHtml(input.footnote)}

+
` +} + +function renderDocument(input: { title: string; body: string; script?: string }) { + return ` + + + + + + ${escapeHtml(input.title)} · OpenCode + + + + ${input.body}${input.script ? `\n ` : ""} + +` +} + +const AUTO_CLOSE_SCRIPT = `setTimeout(function(){try{window.close()}catch(e){}},2500)` + +function bootstrapScript(options: BootstrapOptions) { + return `var PROVIDER=${scriptString(options.provider ?? "")}; +var TOKEN_URL=new URL(${scriptString(options.tokenPath)},window.location.origin).href; +(function(){ + var card=document.getElementById("oc-card"),headline=document.getElementById("oc-headline"),message=document.getElementById("oc-message"),detail=document.getElementById("oc-detail"),footnote=document.getElementById("oc-footnote"); + function fail(text){card.dataset.status="error";headline.textContent="Authorization failed";message.textContent=PROVIDER?("OpenCode couldn't finish connecting to "+PROVIDER+"."):"OpenCode couldn't complete authorization.";if(text){detail.textContent=text;detail.hidden=false}footnote.textContent="Close this window and try again from OpenCode."} + function ok(){card.dataset.status="success";headline.textContent="Authorization successful";message.textContent=PROVIDER?("OpenCode is now connected to "+PROVIDER+"."):"OpenCode is now authorized.";detail.hidden=true;footnote.textContent="You can close this window.";setTimeout(function(){try{window.close()}catch(e){}},2500)} + try{ + var hash=new URLSearchParams((window.location.hash||"").slice(1)); + var search=new URLSearchParams(window.location.search||""); + var err=hash.get("error")||search.get("error"); + var errDescription=hash.get("error_description")||search.get("error_description"); + var body=err?{error:err,error_description:errDescription||""}:{access_token:hash.get("access_token")||"",expires_in:hash.get("expires_in")||"0",state:hash.get("state")||""}; + fetch(TOKEN_URL,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}).then(function(res){ + if(!res.ok)return res.text().catch(function(){return""}).then(function(t){throw new Error(t||("callback failed ("+res.status+")"))}); + if(err){fail(errDescription||err);return} + ok(); + }).catch(function(e){fail(String(e&&e.message?e.message:e))}); + }catch(e){fail(String(e&&e.message?e.message:e))} +})()` +} + +function scriptString(value: string) { + return JSON.stringify(value).replaceAll("<", "\\u003c") +} + +function escapeHtml(value: string) { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'") +} + +// Curated subset of OC-2 tokens (packages/ui/src/styles/theme.css). Default is +// light; dark applies via prefers-color-scheme. The [data-theme] selectors let a +// host force a scheme without changing the default. +const LIGHT_VARS = ` + --oc-bg: #f8f8f8; + --oc-card: #fcfcfc; + --oc-text-strong: #171717; + --oc-text-base: #6f6f6f; + --oc-text-weak: #8f8f8f; + --oc-border-weak: #e5e5e5; + --oc-icon-strong: #171717; + --oc-icon-base: #8f8f8f; + --oc-icon-weak: #dbdbdb; + --oc-success: #2dba26; + --oc-error: #ed4831; + --oc-detail-bg: #fff8f6; + --oc-detail-border: #fdc3b7; + --oc-shadow: 0 16px 48px -6px rgba(0,0,0,.10), 0 6px 12px -2px rgba(0,0,0,.05), 0 1px 2px rgba(0,0,0,.06);` + +const DARK_VARS = ` + --oc-bg: #101010; + --oc-card: #161616; + --oc-text-strong: rgba(255,255,255,.936); + --oc-text-base: rgba(255,255,255,.618); + --oc-text-weak: rgba(255,255,255,.422); + --oc-border-weak: #282828; + --oc-icon-strong: #ededed; + --oc-icon-base: #7e7e7e; + --oc-icon-weak: #343434; + --oc-success: #12c905; + --oc-error: #fc533a; + --oc-detail-bg: #28110c; + --oc-detail-border: #6a1206; + --oc-shadow: 0 16px 48px -6px rgba(0,0,0,.55), 0 6px 12px -2px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.4);` + +const STYLES = ` + :root { color-scheme: light dark;${LIGHT_VARS} + --oc-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --oc-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + } + @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) {${DARK_VARS} } } + :root[data-theme="dark"] {${DARK_VARS} } + :root[data-theme="light"] {${LIGHT_VARS} } + + * { box-sizing: border-box; } + html, body { margin: 0; height: 100%; } + body { + min-height: 100vh; + display: grid; + place-items: center; + padding: 24px; + background: var(--oc-bg); + color: var(--oc-text-base); + font-family: var(--oc-font-sans); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + } + .card { + width: min(100%, 28rem); + padding: 2.25rem 2rem 1.75rem; + background: var(--oc-card); + border: 1px solid var(--oc-border-weak); + border-radius: 14px; + box-shadow: var(--oc-shadow); + text-align: center; + } + .brand { display: flex; justify-content: center; margin-bottom: 1.75rem; } + .brand svg { height: 19px; width: auto; } + .status { display: flex; justify-content: center; margin-bottom: 1.125rem; } + .icon { display: none; line-height: 0; } + .icon svg { display: block; } + .card[data-status="pending"] .icon-pending, + .card[data-status="success"] .icon-success, + .card[data-status="error"] .icon-error { display: block; } + .icon-success { color: var(--oc-success); } + .icon-error { color: var(--oc-error); } + .icon-pending { color: var(--oc-text-weak); } + .headline { margin: 0; font-size: 1.1875rem; font-weight: 500; line-height: 1.3; letter-spacing: -0.012em; color: var(--oc-text-strong); } + .message { margin: 0.5rem 0 0; font-size: 0.9375rem; color: var(--oc-text-base); } + .detail { + margin: 1.25rem 0 0; + padding: 0.75rem 0.875rem; + text-align: left; + font-family: var(--oc-font-mono); + font-size: 0.8125rem; + line-height: 1.55; + color: var(--oc-text-strong); + background: var(--oc-detail-bg); + border: 1px solid var(--oc-detail-border); + border-radius: 8px; + white-space: pre-wrap; + word-break: break-word; + max-height: 9.5rem; + overflow: auto; + } + .detail[hidden] { display: none; } + .footnote { margin: 1.5rem 0 0; font-size: 0.8125rem; color: var(--oc-text-weak); } + .spinner { animation: oc-spin 0.8s linear infinite; transform-origin: center; } + @keyframes oc-spin { to { transform: rotate(360deg); } } + @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } } +` + +// OpenCode wordmark — same path geometry as packages/ui/src/components/logo.tsx (Logo). +const WORDMARK = ` + + + + + + + + + + + + + + + + + ` + +const ICON_CHECK = `` + +const ICON_CROSS = `` + +const ICON_SPINNER = `` diff --git a/packages/core/src/observability.ts b/packages/core/src/observability.ts new file mode 100644 index 0000000000000000000000000000000000000000..22285974d83ab0517fbba1c30c788f1279106aa8 --- /dev/null +++ b/packages/core/src/observability.ts @@ -0,0 +1,24 @@ +export * as Observability from "./observability" + +import { NodeFileSystem } from "@effect/platform-node" +import { LayerNode } from "./effect/layer-node" +import { Effect, Layer, Logger, References } from "effect" +import { FetchHttpClient } from "effect/unstable/http" +import { OtlpSerialization } from "effect/unstable/observability" +import { Logging } from "./observability/logging" +import { Otlp } from "./observability/otlp" + +export const layer = Layer.unwrap( + Effect.gen(function* () { + const logs = Logger.layer([...Logging.loggers(), ...Otlp.loggers()], { mergeWithExisting: false }).pipe( + Layer.provide(NodeFileSystem.layer), + Layer.provide(OtlpSerialization.layerJson), + Layer.provide(FetchHttpClient.layer), + Layer.orDie, + Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())), + ) + return Layer.merge(logs, yield* Effect.promise(Otlp.tracingLayer)) + }), +) + +export const node = LayerNode.make({ name: "observability", layer, deps: [] }) diff --git a/packages/core/src/observability/logging.ts b/packages/core/src/observability/logging.ts new file mode 100644 index 0000000000000000000000000000000000000000..0047d8d5e3fd4b5cae6e80c63787dbafe340721b --- /dev/null +++ b/packages/core/src/observability/logging.ts @@ -0,0 +1,71 @@ +import { Formatter, Logger, type LogLevel } from "effect" +import path from "path" +import { Global } from "../global" +import { runID } from "./shared" + +function formatter(id: string = runID) { + return Logger.map(Logger.formatStructured, (output) => { + const messages = Array.isArray(output.message) ? output.message : [output.message] + return [ + ["timestamp", output.timestamp], + ["level", output.level], + ["run", id], + ...messages.flatMap((value) => (plain(value) ? flatten(value) : [["message", value] as const])), + ...(output.cause === undefined ? [] : [["cause", output.cause] as const]), + ...flatten(output.spans), + ...flatten(output.annotations), + ] + .map(([key, value]) => `${key}=${format(value)}`) + .join(" ") + }) +} + +function flatten( + input: Record, + prefix = "", + seen = new WeakSet(), +): Array { + if (seen.has(input)) return [[prefix, "[Circular]"]] + seen.add(input) + const entries = Object.entries(input) + if (entries.length === 0 && prefix) return [[prefix, input]] + return entries.flatMap(([key, value]) => { + const path = prefix ? `${prefix}.${key}` : key + return plain(value) ? flatten(value, path, seen) : [[path, value] as const] + }) +} + +function plain(input: unknown): input is Record { + if (input === null || typeof input !== "object" || Array.isArray(input)) return false + const prototype = Object.getPrototypeOf(input) + return prototype === Object.prototype || prototype === null +} + +function format(input: unknown) { + const value = typeof input === "string" ? input : Formatter.format(input) + return /^[^\s="\\]+$/.test(value) ? value : JSON.stringify(value) +} + +export function fileLogger(file = path.join(Global.Path.log, "opencode.log"), id: string = runID) { + // Do not set batchWindow to 0; it causes high idle CPU usage. + return Logger.toFile(formatter(id), file, { flag: "a" }) +} + +const stderrLogger = Logger.make((options) => process.stderr.write(formatter().log(options) + "\n")) + +export function minimumLogLevel() { + const value = process.env.OPENCODE_LOG_LEVEL?.toUpperCase() + const levels = { + DEBUG: "Debug", + INFO: "Info", + WARN: "Warn", + ERROR: "Error", + } as const satisfies Record + return value && value in levels ? levels[value as keyof typeof levels] : levels.INFO +} + +export function loggers() { + return process.env.OPENCODE_PRINT_LOGS === "1" ? [fileLogger(), stderrLogger] : [fileLogger()] +} + +export * as Logging from "./logging" diff --git a/packages/core/src/observability/otlp.ts b/packages/core/src/observability/otlp.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd99ebc1436b81bdac7d86f6e82d5f3846157ef7 --- /dev/null +++ b/packages/core/src/observability/otlp.ts @@ -0,0 +1,79 @@ +import { Layer } from "effect" +import { OtlpLogger } from "effect/unstable/observability" +import { Flag } from "../flag/flag" +import { InstallationChannel, InstallationVersion } from "../installation/version" +import { runID } from "./shared" + +const endpoint = Flag.OTEL_EXPORTER_OTLP_ENDPOINT + +const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS + ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce( + (acc, entry) => { + const [key, ...value] = entry.split("=") + acc[key] = value.join("=") + return acc + }, + {} as Record, + ) + : undefined + +function resourceAttributes() { + const value = process.env.OTEL_RESOURCE_ATTRIBUTES + if (!value) return {} + try { + return Object.fromEntries( + value.split(",").map((entry) => { + const index = entry.indexOf("=") + if (index < 1) throw new Error("Invalid OTEL_RESOURCE_ATTRIBUTES entry") + return [decodeURIComponent(entry.slice(0, index)), decodeURIComponent(entry.slice(index + 1))] + }), + ) + } catch { + return {} + } +} + +export function resource(): { serviceName: string; serviceVersion: string; attributes: Record } { + return { + serviceName: "opencode", + serviceVersion: InstallationVersion, + attributes: { + ...resourceAttributes(), + "deployment.environment.name": InstallationChannel, + "opencode.client": Flag.OPENCODE_CLIENT, + "opencode.run": runID, + "service.instance.id": runID, + }, + } +} + +export function loggers() { + if (!endpoint) return [] + return [OtlpLogger.make({ url: `${endpoint}/v1/logs`, resource: resource(), headers })] +} + +export async function tracingLayer() { + if (!endpoint) return Layer.empty + const NodeSdk = await import("@effect/opentelemetry/NodeSdk") + const OTLP = await import("@opentelemetry/exporter-trace-otlp-http") + const SdkBase = await import("@opentelemetry/sdk-trace-base") + const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") + const { context } = await import("@opentelemetry/api") + + // The Effect Node SDK does not register a global context manager, but the AI SDK uses it to parent spans. + const manager = new AsyncLocalStorageContextManager() + manager.enable() + context.setGlobalContextManager(manager) + + return NodeSdk.layer(() => ({ + resource: resource(), + spanProcessor: new SdkBase.BatchSpanProcessor( + new OTLP.OTLPTraceExporter({ + url: `${endpoint}/v1/traces`, + headers, + }), + ), + })) +} + +export * as Otlp from "./otlp" diff --git a/packages/core/src/observability/shared.ts b/packages/core/src/observability/shared.ts new file mode 100644 index 0000000000000000000000000000000000000000..76393aacce5a28ab22e9496ea1afb4b5333b32c8 --- /dev/null +++ b/packages/core/src/observability/shared.ts @@ -0,0 +1 @@ +export const runID = crypto.randomUUID().slice(0, 8) diff --git a/packages/core/src/patch.ts b/packages/core/src/patch.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4370d44aac12180cb76b0b4df05afcd05c71e8e --- /dev/null +++ b/packages/core/src/patch.ts @@ -0,0 +1,197 @@ +export * as Patch from "./patch" + +export type Hunk = + | { readonly type: "add"; readonly path: string; readonly contents: string } + | { readonly type: "delete"; readonly path: string } + | { + readonly type: "update" + readonly path: string + readonly movePath?: string + readonly chunks: ReadonlyArray + } + +export interface UpdateFileChunk { + readonly oldLines: ReadonlyArray + readonly newLines: ReadonlyArray + readonly changeContext?: string + readonly endOfFile?: boolean +} + +export interface FileUpdate { + readonly content: string + readonly bom: boolean +} + +export function parse(patchText: string): ReadonlyArray { + const lines = stripHeredoc(patchText.trim()).split("\n") + const begin = lines.findIndex((line) => line.trim() === "*** Begin Patch") + const end = lines.findIndex((line) => line.trim() === "*** End Patch") + if (begin === -1 || end === -1 || begin >= end) throw new Error("Invalid patch format: missing Begin/End markers") + + const hunks: Hunk[] = [] + let index = begin + 1 + while (index < end) { + const line = lines[index]! + if (line.startsWith("*** Add File:")) { + const path = line.slice("*** Add File:".length).trim() + if (!path) throw new Error("Invalid add file path") + const parsed = parseAdd(lines, index + 1) + hunks.push({ type: "add", path, contents: parsed.content }) + index = parsed.next + continue + } + if (line.startsWith("*** Delete File:")) { + const path = line.slice("*** Delete File:".length).trim() + if (!path) throw new Error("Invalid delete file path") + hunks.push({ type: "delete", path }) + index++ + continue + } + if (line.startsWith("*** Update File:")) { + const path = line.slice("*** Update File:".length).trim() + if (!path) throw new Error("Invalid update file path") + let next = index + 1 + let movePath: string | undefined + if (lines[next]?.startsWith("*** Move to:")) { + movePath = lines[next]!.slice("*** Move to:".length).trim() + if (!movePath) throw new Error("Invalid move file path") + next++ + } + const parsed = parseUpdate(lines, next) + if (parsed.chunks.length === 0) throw new Error(`Invalid update hunk for ${path}: expected at least one @@ chunk`) + hunks.push({ type: "update", path, movePath, chunks: parsed.chunks }) + index = parsed.next + continue + } + throw new Error(`Invalid patch line: ${line}`) + } + return hunks +} + +export function derive(path: string, chunks: ReadonlyArray, original: string): FileUpdate { + const source = splitBom(original) + const lines = source.text.split("\n") + if (lines.at(-1) === "") lines.pop() + const replacements = computeReplacements(lines, path, chunks) + const updated = [...lines] + for (const [start, remove, insert] of replacements.toReversed()) updated.splice(start, remove, ...insert) + if (updated.at(-1) !== "") updated.push("") + const next = splitBom(updated.join("\n")) + return { content: next.text, bom: source.bom || next.bom } +} + +export function joinBom(text: string, bom: boolean) { + const stripped = splitBom(text).text + return bom ? `\uFEFF${stripped}` : stripped +} + +function parseAdd(lines: ReadonlyArray, start: number) { + const content: string[] = [] + let index = start + while (index < lines.length && !lines[index]!.startsWith("***")) { + if (!lines[index]!.startsWith("+")) throw new Error(`Invalid add file line: ${lines[index]}`) + content.push(lines[index]!.slice(1)) + index++ + } + return { content: content.join("\n"), next: index } +} + +function parseUpdate(lines: ReadonlyArray, start: number) { + const chunks: UpdateFileChunk[] = [] + let index = start + while (index < lines.length && !lines[index]!.startsWith("***")) { + if (!lines[index]!.startsWith("@@")) { + throw new Error(`Invalid update file line: ${lines[index]}`) + } + const changeContext = lines[index]!.slice(2).trim() || undefined + const oldLines: string[] = [] + const newLines: string[] = [] + let endOfFile = false + index++ + while (index < lines.length && !lines[index]!.startsWith("@@")) { + const line = lines[index]! + if (line === "*** End of File") { + endOfFile = true + index++ + break + } + if (line.startsWith("***")) break + if (line.startsWith(" ")) { + oldLines.push(line.slice(1)) + newLines.push(line.slice(1)) + } else if (line.startsWith("-")) oldLines.push(line.slice(1)) + else if (line.startsWith("+")) newLines.push(line.slice(1)) + else throw new Error(`Invalid update chunk line: ${line}`) + index++ + } + chunks.push({ oldLines, newLines, changeContext, endOfFile: endOfFile || undefined }) + } + return { chunks, next: index } +} + +function computeReplacements(lines: ReadonlyArray, path: string, chunks: ReadonlyArray) { + const replacements: Array]> = [] + let lineIndex = 0 + for (const chunk of chunks) { + if (chunk.changeContext) { + const context = seek(lines, [chunk.changeContext], lineIndex) + if (context === -1) throw new Error(`Failed to find context '${chunk.changeContext}' in ${path}`) + lineIndex = context + 1 + } + if (chunk.oldLines.length === 0) { + replacements.push([lines.length, 0, chunk.newLines]) + continue + } + let oldLines = chunk.oldLines + let newLines = chunk.newLines + let found = seek(lines, oldLines, lineIndex, chunk.endOfFile) + if (found === -1 && oldLines.at(-1) === "") { + oldLines = oldLines.slice(0, -1) + if (newLines.at(-1) === "") newLines = newLines.slice(0, -1) + found = seek(lines, oldLines, lineIndex, chunk.endOfFile) + } + if (found === -1) throw new Error(`Failed to find expected lines in ${path}:\n${chunk.oldLines.join("\n")}`) + replacements.push([found, oldLines.length, newLines]) + lineIndex = found + oldLines.length + } + return replacements.toSorted((left, right) => left[0] - right[0]) +} + +function seek(lines: ReadonlyArray, pattern: ReadonlyArray, start: number, eof = false) { + if (pattern.length === 0) return -1 + for (const compare of [exact, rstrip, trim, normalized]) { + if (eof) { + const offset = lines.length - pattern.length + if (offset >= start && matches(lines, pattern, offset, compare)) return offset + } + for (let offset = start; offset <= lines.length - pattern.length; offset++) { + if (matches(lines, pattern, offset, compare)) return offset + } + } + return -1 +} + +function matches( + lines: ReadonlyArray, + pattern: ReadonlyArray, + offset: number, + compare: (left: string, right: string) => boolean, +) { + return pattern.every((line, index) => compare(lines[offset + index]!, line)) +} + +const exact = (left: string, right: string) => left === right +const rstrip = (left: string, right: string) => left.trimEnd() === right.trimEnd() +const trim = (left: string, right: string) => left.trim() === right.trim() +const normalized = (left: string, right: string) => normalize(left.trim()) === normalize(right.trim()) +const normalize = (value: string) => + value + .replace(/[‘’‚‛]/g, "'") + .replace(/[“”„‟]/g, '"') + .replace(/[‐‑‒–—―]/g, "-") + .replace(/…/g, "...") + .replace(/ /g, " ") +const splitBom = (text: string) => + text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text } +const stripHeredoc = (input: string) => + input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f28632a034d05b4ab833f22fdeb85d23d1b2888 --- /dev/null +++ b/packages/core/src/permission.ts @@ -0,0 +1,310 @@ +export * as PermissionV2 from "./permission" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Deferred, Effect as EffectRuntime, Layer, Schema } from "effect" +import { Permission } from "@opencode-ai/schema/permission" +import { EventV2 } from "./event" +import { Location } from "./location" +import { AgentV2 } from "./agent" +import { SessionV2 } from "./session" +import { SessionStore } from "./session/store" +import { Wildcard } from "./util/wildcard" +import { PermissionSaved } from "./permission/saved" + +export { Effect, Rule, Ruleset } from "@opencode-ai/schema/permission" +const missingAgentPermissions: Permission.Ruleset = [{ action: "*", resource: "*", effect: "deny" }] + +export const ID = Permission.ID +export type ID = typeof ID.Type + +export const Source = Permission.Source +export type Source = typeof Source.Type + +const RequestFields = { + sessionID: Permission.Request.fields.sessionID, + action: Permission.Request.fields.action, + resources: Permission.Request.fields.resources, + save: Permission.Request.fields.save, + metadata: Permission.Request.fields.metadata, + source: Permission.Request.fields.source, +} + +export const Request = Permission.Request +export type Request = typeof Request.Type + +export const Reply = Permission.Reply +export type Reply = typeof Reply.Type + +export const AssertInput = Schema.Struct({ + id: ID.pipe(Schema.optional), + ...RequestFields, + agent: AgentV2.ID.pipe(Schema.optional), +}).annotate({ identifier: "PermissionV2.AssertInput" }) +export type AssertInput = typeof AssertInput.Type + +export const ReplyInput = Schema.Struct({ + requestID: ID, + reply: Reply, + message: Schema.String.pipe(Schema.optional), +}).annotate({ identifier: "PermissionV2.ReplyInput" }) +export type ReplyInput = typeof ReplyInput.Type + +export const AskResult = Schema.Struct({ + id: ID, + effect: Permission.Effect, +}).annotate({ identifier: "PermissionV2.AskResult" }) +export type AskResult = typeof AskResult.Type + +export const Event = Permission.Event + +export class DeclinedError extends Schema.TaggedErrorClass()("PermissionV2.DeclinedError", {}) {} + +export class CorrectedError extends Schema.TaggedErrorClass()("PermissionV2.CorrectedError", { + feedback: Schema.String, +}) {} + +export class BlockedError extends Schema.TaggedErrorClass()("PermissionV2.BlockedError", { + rules: Permission.Ruleset, +}) {} + +export class NotFoundError extends Schema.TaggedErrorClass()("PermissionV2.NotFoundError", { + requestID: ID, +}) {} + +export type Error = BlockedError | CorrectedError + +export function evaluate(action: string, resource: string, ...rulesets: Permission.Ruleset[]): Permission.Rule { + return ( + rulesets + .flat() + .findLast((rule) => Wildcard.match(action, rule.action) && Wildcard.match(resource, rule.resource)) ?? { + action, + resource: "*", + effect: "ask", + } + ) +} + +export function merge(...rulesets: Permission.Ruleset[]): Permission.Ruleset { + return rulesets.flat() +} + +export interface Interface { + readonly ask: (input: AssertInput) => EffectRuntime.Effect + readonly assert: (input: AssertInput) => EffectRuntime.Effect + readonly reply: (input: ReplyInput) => EffectRuntime.Effect + readonly get: (id: ID) => EffectRuntime.Effect + readonly forSession: (sessionID: SessionV2.ID) => EffectRuntime.Effect> + readonly list: () => EffectRuntime.Effect> +} + +export class Service extends Context.Service()("@opencode/v2/Permission") {} + +interface Pending { + readonly request: Request + readonly agent?: AgentV2.ID + readonly deferred: Deferred.Deferred +} + +const layer = Layer.effect( + Service, + EffectRuntime.gen(function* () { + const events = yield* EventV2.Service + const location = yield* Location.Service + const agents = yield* AgentV2.Service + const sessions = yield* SessionStore.Service + const saved = yield* PermissionSaved.Service + const pending = new Map() + + yield* EffectRuntime.addFinalizer(() => + EffectRuntime.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new DeclinedError()), { + discard: true, + }).pipe( + EffectRuntime.ensuring( + EffectRuntime.sync(() => { + pending.clear() + }), + ), + ), + ) + + const savedRules = EffectRuntime.fnUntraced(function* () { + return (yield* saved.list({ projectID: location.project.id })).map( + (item): Permission.Rule => ({ action: item.action, resource: item.resource, effect: "allow" }), + ) + }) + + const configured = EffectRuntime.fn("PermissionV2.configured")(function* ( + sessionID: SessionV2.ID, + agentID?: AgentV2.ID, + ) { + const session = yield* sessions.get(sessionID) + if (!session) return yield* new SessionV2.NotFoundError({ sessionID }) + const agent = yield* agents.resolve(agentID ?? session.agent) + return agent?.permissions ?? missingAgentPermissions + }) + + function denied(input: AssertInput, rules: Permission.Ruleset) { + return input.resources.some((resource) => evaluate(input.action, resource, rules).effect === "deny") + } + + function relevant(input: AssertInput, rules: Permission.Ruleset) { + return rules.filter((rule) => Wildcard.match(input.action, rule.action)) + } + + const evaluateInput = EffectRuntime.fnUntraced(function* (input: AssertInput) { + const rules = yield* configured(input.sessionID, input.agent) + if (denied(input, rules)) return { effect: "deny" as const, rules } + const all = [...rules, ...(yield* savedRules())] + const effects = input.resources.map((resource) => evaluate(input.action, resource, all).effect) + const effect: Permission.Effect = effects.includes("deny") ? "deny" : effects.includes("ask") ? "ask" : "allow" + return { effect, rules: all } + }) + + function request(input: AssertInput): Request { + return { + id: input.id ?? ID.create(), + sessionID: input.sessionID, + action: input.action, + resources: input.resources, + save: input.save, + metadata: input.metadata, + source: input.source, + } + } + + const create = (request: Request, agent?: AgentV2.ID) => + EffectRuntime.uninterruptible( + EffectRuntime.gen(function* () { + const deferred = yield* Deferred.make() + const item = { request, agent, deferred } + if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`) + pending.set(request.id, item) + yield* events + .publish(Event.Asked, request) + .pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id)))) + return item + }), + ) + + const ask = EffectRuntime.fn("PermissionV2.ask")(function* (input: AssertInput) { + const result = yield* evaluateInput(input) + const value = request(input) + if (result.effect === "ask") yield* create(value, input.agent) + return { id: value.id, effect: result.effect } + }) + + const assert = EffectRuntime.fn("PermissionV2.assert")((input: AssertInput) => + EffectRuntime.uninterruptibleMask((restore) => + EffectRuntime.gen(function* () { + const result = yield* evaluateInput(input) + if (result.effect === "deny") { + return yield* new BlockedError({ + rules: relevant(input, result.rules), + }) + } + if (result.effect === "allow") return + const item = yield* create(request(input), input.agent) + return yield* restore(Deferred.await(item.deferred)).pipe( + EffectRuntime.catchTag("PermissionV2.DeclinedError", (error) => EffectRuntime.die(error)), + EffectRuntime.ensuring( + EffectRuntime.sync(() => { + pending.delete(item.request.id) + }), + ), + ) + }), + ), + ) + + const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) => + EffectRuntime.uninterruptible( + EffectRuntime.gen(function* () { + const existing = pending.get(input.requestID) + if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) + yield* events.publish(Event.Replied, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + reply: input.reply, + }) + + if (input.reply === "reject") { + yield* Deferred.fail( + existing.deferred, + input.message ? new CorrectedError({ feedback: input.message }) : new DeclinedError(), + ) + pending.delete(input.requestID) + for (const [id, item] of pending) { + if (item.request.sessionID !== existing.request.sessionID) continue + yield* events.publish(Event.Replied, { + sessionID: item.request.sessionID, + requestID: item.request.id, + reply: "reject", + }) + yield* Deferred.fail(item.deferred, new DeclinedError()) + pending.delete(id) + } + return + } + + if (input.reply === "always" && existing.request.save?.length) { + yield* saved.add({ + projectID: location.project.id, + action: existing.request.action, + resources: existing.request.save, + }) + } + yield* Deferred.succeed(existing.deferred, undefined) + pending.delete(input.requestID) + if (input.reply !== "always" || !existing.request.save?.length) return + + const rememberedRules = yield* savedRules() + for (const [id, item] of pending) { + const input = { ...item.request } + const rules = yield* configured(item.request.sessionID, item.agent).pipe( + EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)), + ) + if (!rules) continue + if (denied(input, rules)) continue + const effective = [...rules, ...rememberedRules] + if ( + !item.request.resources.every( + (resource) => evaluate(item.request.action, resource, effective).effect === "allow", + ) + ) + continue + yield* events.publish(Event.Replied, { + sessionID: item.request.sessionID, + requestID: item.request.id, + reply: "always", + }) + yield* Deferred.succeed(item.deferred, undefined) + pending.delete(id) + } + }), + ), + ) + + const list = EffectRuntime.fn("PermissionV2.list")(function* () { + return Array.from(pending.values(), (item) => item.request) + }) + + const get = EffectRuntime.fn("PermissionV2.get")(function* (id: ID) { + return pending.get(id)?.request + }) + + const forSession = EffectRuntime.fn("PermissionV2.forSession")(function* (sessionID: SessionV2.ID) { + return Array.from(pending.values(), (item) => item.request).filter((request) => request.sessionID === sessionID) + }) + + return Service.of({ ask, assert, reply, get, forSession, list }) + }), +) + +export const locationLayer = layer.pipe(Layer.provideMerge(AgentV2.locationLayer)) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [EventV2.node, Location.node, AgentV2.node, SessionStore.node, PermissionSaved.node], +}) diff --git a/packages/core/src/permission/saved.ts b/packages/core/src/permission/saved.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffc4559afee039e52a9ea65ed3d1b1349e963a02 --- /dev/null +++ b/packages/core/src/permission/saved.ts @@ -0,0 +1,79 @@ +export * as PermissionSaved from "./saved" + +import { eq } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Database } from "../database/database" +import { makeGlobalNode } from "../effect/app-node" +import { ProjectV2 } from "../project" +import { PermissionTable } from "./sql" +import { PermissionSaved } from "@opencode-ai/schema/permission-saved" + +export const ID = PermissionSaved.ID +export type ID = typeof ID.Type + +export const Info = PermissionSaved.Info +export type Info = typeof Info.Type + +export const ListInput = Schema.Struct({ + projectID: ProjectV2.ID.pipe(Schema.optional), +}).annotate({ identifier: "PermissionSaved.ListInput" }) +export type ListInput = typeof ListInput.Type + +export const AddInput = Schema.Struct({ + projectID: ProjectV2.ID, + action: Schema.String, + resources: Schema.Array(Schema.String), +}).annotate({ identifier: "PermissionSaved.AddInput" }) +export type AddInput = typeof AddInput.Type + +export interface Interface { + readonly list: (input?: ListInput) => Effect.Effect> + readonly add: (input: AddInput) => Effect.Effect + readonly remove: (id: ID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/PermissionSaved") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + + const list = Effect.fn("PermissionSaved.list")(function* (input?: ListInput) { + const rows = yield* db + .select() + .from(PermissionTable) + .where(input?.projectID ? eq(PermissionTable.project_id, input.projectID) : undefined) + .all() + .pipe(Effect.orDie) + return rows.map( + (row): Info => ({ id: row.id, projectID: row.project_id, action: row.action, resource: row.resource }), + ) + }) + + const add = Effect.fn("PermissionSaved.add")(function* (input: AddInput) { + if (!input.resources.length) return + yield* db + .insert(PermissionTable) + .values( + input.resources.map((resource) => ({ + id: ID.create(), + project_id: input.projectID, + action: input.action, + resource, + })), + ) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + }) + + const remove = Effect.fn("PermissionSaved.remove")(function* (id: ID) { + yield* db.delete(PermissionTable).where(eq(PermissionTable.id, id)).run().pipe(Effect.orDie) + }) + + return Service.of({ list, add, remove }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] }) diff --git a/packages/core/src/permission/sql.ts b/packages/core/src/permission/sql.ts new file mode 100644 index 0000000000000000000000000000000000000000..c395555d79508dd0cfdfb142e7d402f05133a1a3 --- /dev/null +++ b/packages/core/src/permission/sql.ts @@ -0,0 +1,20 @@ +import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" +import { Timestamps } from "../database/schema.sql" +import { ProjectV2 } from "../project" +import { ProjectTable } from "../project/sql" +import type { PermissionSaved } from "./saved" + +export const PermissionTable = sqliteTable( + "permission", + { + id: text().$type().primaryKey(), + project_id: text() + .$type() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + action: text().notNull(), + resource: text().notNull(), + ...Timestamps, + }, + (table) => [uniqueIndex("permission_project_action_resource_idx").on(table.project_id, table.action, table.resource)], +) diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts new file mode 100644 index 0000000000000000000000000000000000000000..a89cdfda5f0a58e674fec3641911abd6fe3d977b --- /dev/null +++ b/packages/core/src/plugin.ts @@ -0,0 +1,167 @@ +export * as PluginV2 from "./plugin" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Deferred, Effect, Exit, Layer, Scope } from "effect" +import type { Plugin as PluginRuntime } from "@opencode-ai/plugin/v2/effect" +import { Plugin } from "@opencode-ai/schema/plugin" +import { AgentV2 } from "./agent" +import { AISDK } from "./aisdk" +import { Catalog } from "./catalog" +import { CommandV2 } from "./command" +import { EventV2 } from "./event" +import { Integration } from "./integration" +import { KeyedMutex } from "./effect/keyed-mutex" +import { PluginHost } from "./plugin/host" +import { Reference } from "./reference" +import { SkillV2 } from "./skill" +import { State } from "./state" + +export const ID = Plugin.ID +export type ID = typeof ID.Type +export const Event = Plugin.Event + +export interface Interface { + readonly add: (id: ID, effect: PluginRuntime["effect"]) => Effect.Effect + readonly remove: (id: ID) => Effect.Effect + readonly wait: (id: ID) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Plugin") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const locks = KeyedMutex.makeUnsafe() + const scope = yield* Scope.make() + const active = new Map() + const loading = new Set() + const waiters = new Map>>() + const failures = new Map>() + let host: Parameters[0] + + const add = Effect.fn("Plugin.add")(function* (id: ID, effect: PluginRuntime["effect"]) { + if (loading.has(id)) return yield* Effect.die(`Plugin load cycle detected for ${id}`) + + yield* locks.withLock(id)( + Effect.sync(() => { + loading.add(id) + failures.delete(id) + }).pipe( + Effect.andThen( + State.batch( + Effect.gen(function* () { + const existing = active.get(id) + active.delete(id) + if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore) + + const child = yield* Scope.fork(scope) + yield* effect(host).pipe( + Scope.provide(child), + Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }), + Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)), + ) + yield* events.publish(Event.Added, { id }) + active.set(id, child) + yield* Effect.forEach(waiters.get(id) ?? [], (waiter) => Deferred.succeed(waiter, undefined), { + discard: true, + }) + waiters.delete(id) + }), + ), + ), + Effect.onExit((exit) => { + if (Exit.isSuccess(exit)) return Effect.void + failures.set(id, exit) + return Effect.forEach(waiters.get(id) ?? [], (waiter) => Deferred.done(waiter, exit), { + discard: true, + }).pipe(Effect.ensuring(Effect.sync(() => waiters.delete(id)))) + }), + Effect.ensuring(Effect.sync(() => loading.delete(id))), + ), + ) + }) + + const remove = Effect.fn("Plugin.remove")(function* (id: ID) { + if (loading.has(id)) return yield* Effect.die(`Cannot remove plugin ${id} while it is loading`) + + yield* locks.withLock(id)( + State.batch( + Effect.gen(function* () { + const current = active.get(id) + active.delete(id) + failures.delete(id) + if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore) + }), + ), + ) + }) + + const wait = Effect.fn("Plugin.wait")(function* (id: ID) { + const waiter = yield* Deferred.make() + const pending = yield* locks.withLock(id)( + Effect.sync(() => { + if (active.has(id)) return false + const failure = failures.get(id) + if (failure) return failure + const current = waiters.get(id) ?? new Set() + current.add(waiter) + waiters.set(id, current) + return true + }), + ) + if (!pending) return + if (typeof pending !== "boolean") return yield* pending + yield* Deferred.await(waiter).pipe( + Effect.ensuring( + locks.withLock(id)( + Effect.sync(() => { + const current = waiters.get(id) + current?.delete(waiter) + if (current?.size === 0) waiters.delete(id) + }), + ), + ), + ) + }) + + yield* Effect.addFinalizer((exit) => + Effect.gen(function* () { + active.clear() + yield* State.batch(Scope.close(scope, exit)) + }), + ) + + const service = Service.of({ + add, + remove, + wait, + }) + host = yield* PluginHost.make(service) + return service + }), +) + +export const locationLayer = layer.pipe( + Layer.provideMerge(AgentV2.locationLayer), + Layer.provideMerge(AISDK.locationLayer), + Layer.provideMerge(Catalog.locationLayer), + Layer.provideMerge(CommandV2.locationLayer), + Layer.provideMerge(Integration.locationLayer), + Layer.provideMerge(Reference.locationLayer), +) + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [ + EventV2.node, + AgentV2.node, + AISDK.node, + Catalog.node, + CommandV2.node, + Integration.node, + Reference.node, + SkillV2.node, + ], +}) diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts new file mode 100644 index 0000000000000000000000000000000000000000..915df79d5be5afd6a045df867f54b361a52b6b33 --- /dev/null +++ b/packages/core/src/plugin/agent.ts @@ -0,0 +1,202 @@ +export * as AgentPlugin from "./agent" + +import path from "path" +import { define } from "./internal" +import { Effect } from "effect" +import { AgentV2 } from "../agent" +import { Global } from "../global" +import { Location } from "../location" +import { PermissionV2 } from "../permission" + +const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*") +const BUILD_SYSTEM = + "You are an AI coding agent. Help the user accomplish software engineering tasks by inspecting the workspace, making targeted changes, and using tools according to the configured permissions." + +const PROMPT_EXPLORE = `You are a file search specialist. You excel at thoroughly navigating and exploring codebases. + +Your strengths: +- Rapidly finding files using glob patterns +- Searching code and text with powerful regex patterns +- Reading and analyzing file contents + +Guidelines: +- Use Glob for broad file pattern matching +- Use Grep for searching file contents with regex +- Use Read when you know the specific file path you need to read +- Adapt your search approach based on the thoroughness level specified by the caller +- Return file paths as absolute paths in your final response +- For clear communication, avoid using emojis +- Do not create any files, or run bash commands that modify the user's system state in any way + +Complete the user's search request efficiently and report your findings clearly.` + +const PROMPT_COMPACTION = `You are a context summarization agent. You are given a conversation between a user and an agent. Your goal is to produce a structured summary matching the format specified so another coding agent can continue the work. + +Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs. + +Do not continue the conversation. Do not respond to any questions in the conversation. Only output the structured summary in the exact format requested by the user prompt. Respond in the same language as the conversation.` + +const PROMPT_TITLE = `You are a title generator. You output ONLY a thread title. Nothing else. + + +Generate a brief title that would help the user find this conversation later. + +Follow all rules in +Use the so you know what a good title looks like. +Your output must be: +- A single line +- <=50 characters +- No explanations + + + +- you MUST use the same language as the user message you are summarizing +- Title must be grammatically correct and read naturally - no word salad +- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool") +- Focus on the main topic or question the user needs to retrieve +- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing" +- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it +- Keep exact: technical terms, numbers, filenames, HTTP codes +- Remove: the, this, my, a, an +- Never assume tech stack +- Never use tools +- NEVER respond to questions, just generate a title for the conversation +- The title should NEVER include "summarizing" or "generating" when generating a title +- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT +- Always output something meaningful, even if the input is minimal. +- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"): + -> create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.) + + + +"debug 500 errors in production" -> Debugging production 500 errors +"refactor user service" -> Refactoring user service +"why is app.js failing" -> app.js failure investigation +"implement rate limiting" -> Rate limiting implementation +"how do I connect postgres to my API" -> Postgres API connection +"best practices for React hooks" -> React hooks best practices +"@src/credential.ts can you add refresh token support" -> Credential refresh token support +"@utils/parser.ts this is broken" -> Parser bug fix +"look at @config.json" -> Config review +"@App.tsx add dark mode toggle" -> Dark mode toggle in App +` + +const PROMPT_SUMMARY = `Summarize what was done in this conversation. Write like a pull request description. + +Rules: +- 2-3 sentences max +- Describe the changes made, not the process +- Do not mention running tests, builds, or other validation steps +- Do not explain what the user asked for +- Write in first person (I added..., I fixed...) +- Never ask questions or add new questions +- If the conversation ends with an unanswered question to the user, preserve that exact question +- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary` + +export const Plugin = define({ + id: "agent", + effect: Effect.fn(function* (ctx) { + const location = yield* Location.Service + const worktree = location.directory + const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")] + const readonlyExternalDirectory: PermissionV2.Ruleset = [ + { action: "external_directory", resource: "*", effect: "ask" }, + ...whitelistedDirs.map( + (resource): PermissionV2.Rule => ({ action: "external_directory", resource, effect: "allow" }), + ), + ] + const defaults: PermissionV2.Ruleset = [ + { action: "*", resource: "*", effect: "allow" }, + ...readonlyExternalDirectory, + { action: "question", resource: "*", effect: "deny" }, + { action: "plan_enter", resource: "*", effect: "deny" }, + { action: "plan_exit", resource: "*", effect: "deny" }, + { action: "read", resource: "*", effect: "allow" }, + { action: "read", resource: "*.env", effect: "ask" }, + { action: "read", resource: "*.env.*", effect: "ask" }, + { action: "read", resource: "*.env.example", effect: "allow" }, + ] + + yield* ctx.agent.transform((draft) => { + draft.update(AgentV2.defaultID, (item) => { + item.description = "The default agent. Executes tools based on configured permissions." + item.system ??= BUILD_SYSTEM + item.mode = "primary" + item.permissions.push( + ...PermissionV2.merge(defaults, [ + { action: "question", resource: "*", effect: "allow" }, + { action: "plan_enter", resource: "*", effect: "allow" }, + ]), + ) + }) + + draft.update(AgentV2.ID.make("plan"), (item) => { + item.description = "Plan mode. Disallows all edit tools." + item.mode = "primary" + item.permissions.push( + ...PermissionV2.merge(defaults, [ + { action: "question", resource: "*", effect: "allow" }, + { action: "plan_exit", resource: "*", effect: "allow" }, + { action: "external_directory", resource: path.join(Global.Path.data, "plans", "*"), effect: "allow" }, + { action: "edit", resource: "*", effect: "deny" }, + { action: "edit", resource: path.join(".opencode", "plans", "*.md"), effect: "allow" }, + { + action: "edit", + resource: path.relative(worktree, path.join(Global.Path.data, "plans", "*.md")), + effect: "allow", + }, + ]), + ) + }) + + draft.update(AgentV2.ID.make("general"), (item) => { + item.description = + "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel." + item.mode = "subagent" + item.permissions.push(...PermissionV2.merge(defaults, [{ action: "todowrite", resource: "*", effect: "deny" }])) + }) + + draft.update(AgentV2.ID.make("explore"), (item) => { + item.description = + 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.' + item.system = PROMPT_EXPLORE + item.mode = "subagent" + item.permissions.push( + ...PermissionV2.merge( + defaults, + [ + { action: "*", resource: "*", effect: "deny" }, + { action: "grep", resource: "*", effect: "allow" }, + { action: "glob", resource: "*", effect: "allow" }, + { action: "webfetch", resource: "*", effect: "allow" }, + { action: "websearch", resource: "*", effect: "allow" }, + { action: "read", resource: "*", effect: "allow" }, + ], + readonlyExternalDirectory, + ), + ) + }) + + draft.update(AgentV2.ID.make("compaction"), (item) => { + item.mode = "primary" + item.hidden = true + item.system = PROMPT_COMPACTION + item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + }) + + draft.update(AgentV2.ID.make("title"), (item) => { + item.mode = "primary" + item.hidden = true + item.system = PROMPT_TITLE + item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + }) + + draft.update(AgentV2.ID.make("summary"), (item) => { + item.mode = "primary" + item.hidden = true + item.system = PROMPT_SUMMARY + item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }])) + }) + }) + }), +}) diff --git a/packages/core/src/plugin/command.ts b/packages/core/src/plugin/command.ts new file mode 100644 index 0000000000000000000000000000000000000000..cbafd68b5025c161d76b5eec1fa5ee82bb5e6a04 --- /dev/null +++ b/packages/core/src/plugin/command.ts @@ -0,0 +1,25 @@ +export * as CommandPlugin from "./command" + +import { define } from "./internal" +import { Effect } from "effect" +import { Location } from "../location" +import PROMPT_INITIALIZE from "./command/initialize.txt" +import PROMPT_REVIEW from "./command/review.txt" + +export const Plugin = define({ + id: "command", + effect: Effect.fn(function* (ctx) { + const location = yield* Location.Service + yield* ctx.command.transform((draft) => { + draft.update("init", (command) => { + command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory) + command.description = "guided AGENTS.md setup" + }) + draft.update("review", (command) => { + command.template = PROMPT_REVIEW.replace("${path}", location.project.directory) + command.description = "review changes [commit|branch|pr], defaults to uncommitted" + command.subtask = true + }) + }) + }), +}) diff --git a/packages/core/src/plugin/command/initialize.txt b/packages/core/src/plugin/command/initialize.txt new file mode 100644 index 0000000000000000000000000000000000000000..5fc073d61cd3c1631195ff629e2a7319291ab822 --- /dev/null +++ b/packages/core/src/plugin/command/initialize.txt @@ -0,0 +1,65 @@ +Create or update `AGENTS.md` for this repository. + +The goal is a compact instruction file that helps future OpenCode sessions avoid mistakes and ramp up quickly. Every line should answer: "Would an agent likely miss this without help?" If not, leave it out. + +User-provided focus or constraints (honor these): +$ARGUMENTS + +## How to investigate + +Read the highest-value sources first: +- `README*`, root manifests, workspace config, lockfiles +- build, test, lint, formatter, typecheck, and codegen config +- CI workflows and pre-commit / task runner config +- existing instruction files (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/`, `.cursorrules`, `.github/copilot-instructions.md`) +- repo-local OpenCode config such as `opencode.json` + +If architecture is still unclear after reading config and docs, inspect a small number of representative code files to find the real entrypoints, package boundaries, and execution flow. Prefer reading the files that explain how the system is wired together over random leaf files. + +Prefer executable sources of truth over prose. If docs conflict with config or scripts, trust the executable source and only keep what you can verify. + +## What to extract + +Look for the highest-signal facts for an agent working in this repo: +- exact developer commands, especially non-obvious ones +- how to run a single test, a single package, or a focused verification step +- required command order when it matters, such as `lint -> typecheck -> test` +- monorepo or multi-package boundaries, ownership of major directories, and the real app/library entrypoints +- framework or toolchain quirks: generated code, migrations, codegen, build artifacts, special env loading, dev servers, infra deploy flow +- testing quirks: fixtures, integration test prerequisites, snapshot workflows, required services, flaky or expensive suites +- important constraints from existing instruction files worth preserving + +Good `AGENTS.md` content is usually hard-earned context that took reading multiple files to infer. + +## Questions + +Only ask the user questions if the repo cannot answer something important. Use the `question` tool for one short batch at most. + +Good questions: +- undocumented team conventions +- branch / PR / release expectations +- missing setup or test prerequisites that are known but not written down + +Do not ask about anything the repo already makes clear. + +## Writing rules + +Include only high-signal, repo-specific guidance such as: +- exact commands and shortcuts the agent would otherwise guess wrong +- architecture notes that are not obvious from filenames +- conventions that differ from language or framework defaults +- setup requirements, environment quirks, and operational gotchas +- references to existing instruction sources that matter + +Exclude: +- generic software advice +- long tutorials or exhaustive file trees +- obvious language conventions +- speculative claims or anything you could not verify +- content better stored in another file referenced via `opencode.json` `instructions` + +When in doubt, omit. + +Prefer short sections and bullets. If the repo is simple, keep the file simple. If the repo is large, summarize the few structural facts that actually change how an agent should work. + +If `AGENTS.md` already exists at `${path}`, improve it in place rather than rewriting blindly. Preserve verified useful guidance, delete fluff or stale claims, and reconcile it with the current codebase. diff --git a/packages/core/src/plugin/command/review.txt b/packages/core/src/plugin/command/review.txt new file mode 100644 index 0000000000000000000000000000000000000000..071807ec87405603bdb4441b512bd110502bc8d3 --- /dev/null +++ b/packages/core/src/plugin/command/review.txt @@ -0,0 +1,100 @@ +You are a code reviewer. Your job is to review code changes and provide actionable feedback. + +--- + +Input: $ARGUMENTS + +--- + +## Determining What to Review + +Based on the input provided, determine which type of review to perform: + +1. **No arguments (default)**: Review all uncommitted changes + - Run: `git diff` for unstaged changes + - Run: `git diff --cached` for staged changes + - Run: `git status --short` to identify untracked (net new) files + +2. **Commit hash** (40-char SHA or short hash): Review that specific commit + - Run: `git show $ARGUMENTS` + +3. **Branch name**: Compare current branch to the specified branch + - Run: `git diff $ARGUMENTS...HEAD` + +4. **PR URL or number** (contains "github.com" or "pull" or looks like a PR number): Review the pull request + - Run: `gh pr view $ARGUMENTS` to get PR context + - Run: `gh pr diff $ARGUMENTS` to get the diff + +Use best judgement when processing input. + +--- + +## Gathering Context + +**Diffs alone are not enough.** After getting the diff, read the entire file(s) being modified to understand the full context. Code that looks wrong in isolation may be correct given surrounding logic—and vice versa. + +- Use the diff to identify which files changed +- Use `git status --short` to identify untracked files, then read their full contents +- Read the full file to understand existing patterns, control flow, and error handling +- Check for existing style guide or conventions files (CONVENTIONS.md, AGENTS.md, .editorconfig, etc.) + +--- + +## What to Look For + +**Bugs** - Your primary focus. +- Logic errors, off-by-one mistakes, incorrect conditionals +- If-else guards: missing guards, incorrect branching, unreachable code paths +- Edge cases: null/empty/undefined inputs, error conditions, race conditions +- Security issues: injection, auth bypass, data exposure +- Broken error handling that swallows failures, throws unexpectedly or returns error types that are not caught. + +**Structure** - Does the code fit the codebase? +- Does it follow existing patterns and conventions? +- Are there established abstractions it should use but doesn't? +- Excessive nesting that could be flattened with early returns or extraction + +**Performance** - Only flag if obviously problematic. +- O(n²) on unbounded data, N+1 queries, blocking I/O on hot paths + +**Behavior Changes** - If a behavioral change is introduced, raise it (especially if it's possibly unintentional). + +--- + +## Before You Flag Something + +**Be certain.** If you're going to call something a bug, you need to be confident it actually is one. + +- Only review the changes - do not review pre-existing code that wasn't modified +- Don't flag something as a bug if you're unsure - investigate first +- Don't invent hypothetical problems - if an edge case matters, explain the realistic scenario where it breaks +- If you need more context to be sure, use the tools below to get it + +**Don't be a zealot about style.** When checking code against conventions: + +- Verify the code is *actually* in violation. Don't complain about else statements if early returns are already being used correctly. +- Some "violations" are acceptable when they're the simplest option. A `let` statement is fine if the alternative is convoluted. +- Excessive nesting is a legitimate concern regardless of other style choices. + +--- + +## Tools + +Use these to inform your review: + +- **Explore agent** - Find how existing code handles similar problems. Check patterns, conventions, and prior art before claiming something doesn't fit. +- **Exa Code Context** - Verify correct usage of libraries/APIs before flagging something as wrong. +- **Web Search** - Research best practices if you're unsure about a pattern. + +If you're uncertain about something and can't verify it with these tools, say "I'm not sure about X" rather than flagging it as a definite issue. + +--- + +## Output + +1. If there is a bug, be direct and clear about why it is a bug. +2. Clearly communicate severity of issues. Do not overstate severity. +3. Critiques should clearly and explicitly communicate the scenarios, environments, or inputs that are necessary for the bug to arise. The comment should immediately indicate that the issue's severity depends on these factors. +4. Your tone should be matter-of-fact and not accusatory or overly positive. It should read as a helpful AI assistant suggestion without sounding too much like a human reviewer. +5. Write so the reader can quickly understand the issue without reading too closely. +6. AVOID flattery, do not give any comments that are not helpful to the reader. diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9d084709efc69d5c376c227eea122c46bffc306 --- /dev/null +++ b/packages/core/src/plugin/host.ts @@ -0,0 +1,219 @@ +export * as PluginHost from "./host" + +import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect" +import { Effect, Schema } from "effect" +import { AgentV2 } from "../agent" +import { AISDK } from "../aisdk" +import { Catalog } from "../catalog" +import { CommandV2 } from "../command" +import { Credential } from "../credential" +import { Integration } from "../integration" +import { ModelV2 } from "../model" +import { PluginV2 } from "../plugin" +import { ProviderV2 } from "../provider" +import { Reference } from "../reference" +import type { DeepMutable } from "../schema" +import { SkillV2 } from "../skill" + +const mutable = (value: T) => value as DeepMutable + +export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) { + const agents = yield* AgentV2.Service + const aisdk = yield* AISDK.Service + const catalog = yield* Catalog.Service + const commands = yield* CommandV2.Service + const integration = yield* Integration.Service + const reference = yield* Reference.Service + const skill = yield* SkillV2.Service + + return { + options: {}, + agent: { + reload: agents.reload, + transform: (callback) => + agents.transform((draft) => + callback({ + list: () => mutable(draft.list()), + get: (id) => mutable(draft.get(AgentV2.ID.make(id))), + default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)), + update: (id, update) => draft.update(AgentV2.ID.make(id), update), + remove: (id) => draft.remove(AgentV2.ID.make(id)), + }), + ), + }, + aisdk: { + sdk: (callback) => + aisdk.hook.sdk((event) => { + const output = { + model: mutable(event.model), + package: event.package, + options: event.options, + sdk: event.sdk, + } + const result = callback(output) + return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( + Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))), + ) + }), + language: (callback) => + aisdk.hook.language((event) => { + const output = { + model: mutable(event.model), + sdk: event.sdk, + options: event.options, + language: event.language, + } + const result = callback(output) + return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( + Effect.tap(() => Effect.sync(() => (event.language = output.language))), + ) + }), + }, + catalog: { + reload: catalog.reload, + transform: (callback) => + catalog.transform((draft) => + callback({ + provider: { + list: () => mutable(draft.provider.list()), + get: (id) => mutable(draft.provider.get(ProviderV2.ID.make(id))), + update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update), + remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)), + }, + model: { + get: (providerID, modelID) => + mutable(draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))), + update: (providerID, modelID, update) => + draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update), + remove: (providerID, modelID) => + draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + default: { + get: draft.model.default.get, + set: (providerID, modelID) => + draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)), + }, + }, + }), + ), + }, + command: { + reload: commands.reload, + transform: commands.transform, + }, + integration: { + reload: integration.reload, + connection: { + active: (id) => integration.connection.active(Integration.ID.make(id)), + resolve: (connection) => + integration.connection.resolve( + connection.type === "credential" ? { ...connection, id: Credential.ID.make(connection.id) } : connection, + ), + }, + transform: (callback) => + integration.transform((draft) => + callback({ + list: () => mutable(draft.list()), + get: (id) => mutable(draft.get(Integration.ID.make(id))), + update: (id, update) => draft.update(Integration.ID.make(id), update), + remove: (id) => draft.remove(Integration.ID.make(id)), + method: { + list: (id) => mutable(draft.method.list(Integration.ID.make(id))), + update: (input) => { + if ("authorize" in input) { + const methodID = Integration.MethodID.make(input.method.id) + const refresh = input.refresh + draft.method.update({ + integrationID: Integration.ID.make(input.integrationID), + method: { ...input.method, id: methodID }, + authorize: (inputs) => + input.authorize(inputs).pipe( + Effect.map((authorization) => { + if (authorization.mode === "auto") { + return { + ...authorization, + callback: authorization.callback.pipe( + Effect.map((credential) => + Credential.OAuth.make({ + ...credential, + methodID: Integration.MethodID.make(credential.methodID), + }), + ), + ), + } + } + return { + ...authorization, + callback: (code: string) => + authorization.callback(code).pipe( + Effect.map((credential) => + Credential.OAuth.make({ + ...credential, + methodID: Integration.MethodID.make(credential.methodID), + }), + ), + ), + } + }), + ), + ...(refresh + ? { + refresh: (value: Credential.OAuth) => + refresh(value).pipe( + Effect.map((next) => + Credential.OAuth.make({ + ...next, + methodID: Integration.MethodID.make(next.methodID), + }), + ), + ), + } + : {}), + ...(input.label ? { label: input.label } : {}), + }) + return + } + if (input.method.type === "env") { + draft.method.update({ + integrationID: Integration.ID.make(input.integrationID), + method: { type: "env", names: input.method.names }, + }) + return + } + draft.method.update({ + integrationID: Integration.ID.make(input.integrationID), + method: { type: "key", label: input.method.label }, + }) + }, + remove: (id, method) => + draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)), + }, + }), + ), + }, + plugin: { + add: (input) => plugin.add(PluginV2.ID.make(input.id), input.effect), + remove: (id) => plugin.remove(PluginV2.ID.make(id)), + }, + reference: { + reload: reference.reload, + transform: (callback) => + reference.transform((draft) => + callback({ + add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)), + remove: draft.remove, + list: draft.list, + }), + ), + }, + skill: { + reload: skill.reload, + transform: (callback) => + skill.transform((draft) => + callback({ + source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)), + list: draft.list, + }), + ), + }, + } satisfies Interface +}) diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts new file mode 100644 index 0000000000000000000000000000000000000000..d4ab71cb6b04d3c696b58bc865245b746cec5741 --- /dev/null +++ b/packages/core/src/plugin/internal.ts @@ -0,0 +1,153 @@ +export * as PluginInternal from "./internal" + +import { makeLocationNode } from "../effect/app-node" +import { httpClient } from "../effect/app-node-platform" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" +import { Effect, Layer, Scope } from "effect" +import { AgentV2 } from "../agent" +import { Catalog } from "../catalog" +import { CommandV2 } from "../command" +import { Config } from "../config" +import { ConfigAgentPlugin } from "../config/plugin/agent" +import { ConfigCommandPlugin } from "../config/plugin/command" +import { ConfigExternalPlugin } from "../config/plugin/external" +import { ConfigProviderPlugin } from "../config/plugin/provider" +import { ConfigReferencePlugin } from "../config/plugin/reference" +import { ConfigSkillPlugin } from "../config/plugin/skill" +import { EventV2 } from "../event" +import { FileSystem } from "../filesystem" +import { FSUtil } from "../fs-util" +import { Global } from "../global" +import { Integration } from "../integration" +import { Location } from "../location" +import { ModelsDev } from "../models-dev" +import { Npm } from "../npm" +import { PluginV2 } from "../plugin" +import { Reference } from "../reference" +import { SkillV2 } from "../skill" +import { State } from "../state" +import { FetchHttpClient, HttpClient } from "effect/unstable/http" +import { AgentPlugin } from "./agent" +import { CommandPlugin } from "./command" +import { ModelsDevPlugin } from "./models-dev" +import { ProviderPlugins } from "./provider" +import { SkillPlugin } from "./skill" +import { VariantPlugin } from "./variant" + +export type Requirements = + | AgentV2.Service + | Catalog.Service + | CommandV2.Service + | Config.Service + | EventV2.Service + | FileSystem.Service + | FSUtil.Service + | Global.Service + | HttpClient.HttpClient + | Integration.Service + | Location.Service + | ModelsDev.Service + | Npm.Service + | Reference.Service + | SkillV2.Service + +export interface Plugin { + readonly id: string + readonly effect: (context: PluginContext) => Effect.Effect +} + +export function define(plugin: Plugin) { + return plugin +} + +const layer = Layer.effectDiscard( + Effect.gen(function* () { + const catalog = yield* Catalog.Service + const commands = yield* CommandV2.Service + const plugin = yield* PluginV2.Service + const integration = yield* Integration.Service + const agents = yield* AgentV2.Service + const config = yield* Config.Service + const location = yield* Location.Service + const modelsDev = yield* ModelsDev.Service + const npm = yield* Npm.Service + const events = yield* EventV2.Service + const fs = yield* FSUtil.Service + const filesystem = yield* FileSystem.Service + const global = yield* Global.Service + const http = yield* HttpClient.HttpClient + const skill = yield* SkillV2.Service + const reference = yield* Reference.Service + const add = (input: Plugin) => { + const loaded = { + id: input.id, + effect: (context: PluginContext) => + input + .effect(context) + .pipe( + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(CommandV2.Service, commands), + Effect.provideService(Integration.Service, integration), + Effect.provideService(AgentV2.Service, agents), + Effect.provideService(Config.Service, config), + Effect.provideService(Location.Service, location), + Effect.provideService(ModelsDev.Service, modelsDev), + Effect.provideService(Npm.Service, npm), + Effect.provideService(EventV2.Service, events), + Effect.provideService(FSUtil.Service, fs), + Effect.provideService(FileSystem.Service, filesystem), + Effect.provideService(Global.Service, global), + Effect.provideService(HttpClient.HttpClient, http), + Effect.provideService(SkillV2.Service, skill), + Effect.provideService(Reference.Service, reference), + ), + } + return plugin.add(PluginV2.ID.make(loaded.id), loaded.effect) + } + + yield* State.batch( + Effect.gen(function* () { + yield* add(ConfigReferencePlugin.Plugin) + yield* add(AgentPlugin.Plugin) + yield* add(CommandPlugin.Plugin) + yield* add(SkillPlugin.Plugin) + yield* add(ModelsDevPlugin) + yield* add(ConfigAgentPlugin.Plugin) + yield* add(ConfigCommandPlugin.Plugin) + yield* add(ConfigSkillPlugin.Plugin) + for (const item of ProviderPlugins) yield* add(item) + yield* add(ConfigExternalPlugin.Plugin) + yield* add(ConfigProviderPlugin.Plugin) + yield* add(VariantPlugin.Plugin) + }), + ).pipe(Effect.withSpan("PluginInternal.boot"), Effect.forkScoped({ startImmediately: true })) + }), +) + +export const locationLayer = layer.pipe( + Layer.provideMerge(Config.locationLayer), + Layer.provideMerge(FetchHttpClient.layer), +) + +export const node = makeLocationNode({ + name: "plugin-internal", + layer, + deps: [ + Catalog.node, + CommandV2.node, + PluginV2.node, + Integration.node, + AgentV2.node, + Config.node, + Location.node, + ModelsDev.node, + Npm.node, + EventV2.node, + FSUtil.node, + FileSystem.node, + Global.node, + httpClient, + SkillV2.node, + Reference.node, + ], +}) diff --git a/packages/core/src/plugin/layer-map.example.ts b/packages/core/src/plugin/layer-map.example.ts new file mode 100644 index 0000000000000000000000000000000000000000..63e33d3f0c46901f5d6b76d5e656f544770482ba --- /dev/null +++ b/packages/core/src/plugin/layer-map.example.ts @@ -0,0 +1,94 @@ +export * as LayerMapExample from "./layer-map.example" + +import { Context, Effect, Layer, LayerMap } from "effect" +import { Npm } from "../npm" + +/** + * Tutorial: split global services from context-specific services. + * + * Use this pattern when part of the app should be constructed once at the app edge, + * while another part should be cached per request/project/workspace key. + * + * In this example: + * - Npm.Service is the global service. It is not keyed by request context and should + * be provided once by the application runtime. + * - ConfigService is context-specific. It is built from a RequestContext key and is + * cached by LayerMap for that key. + * - ConfigServiceMap.layer owns the cache. Provide it once globally, then each + * request can provide ConfigServiceMap.get(context) to select the right instance. + * + * Lifetime model: + * - ConfigServiceMap.layer has the app/global lifetime and depends on Npm.Service. + * - ConfigServiceMap.get(context) has the request/context lifetime and provides + * ConfigService for exactly that context key. + * - The cached ConfigService entry stays alive while something is using it. Once idle, + * it remains cached for idleTimeToLive, then its scope is finalized. + * - invalidate(context) removes the cache entry for future lookups. Active users keep + * running on the old instance; the next lookup can create a fresh instance. + * + * Key model: + * - Keys can be strings, structs, classes, arrays, etc. + * - Prefer primitive or immutable keys. Effect uses Hash / Equal semantics for cache + * lookup, so mutating an object after it has been used as a key is a bug. + */ + +export type RequestContext = { + readonly directory: string + readonly workspace: string +} + +export class RequestContextRef extends Context.Service()( + "@opencode/example/RequestContextRef", +) {} + +export interface ConfigServiceShape { + readonly directory: string + readonly workspace: string + readonly nextUse: () => Effect.Effect + readonly which: Npm.Interface["which"] +} + +export class ConfigService extends Context.Service()( + "@opencode/example/ConfigService", +) {} + +const configServiceLayer = Layer.effect( + ConfigService, + Effect.gen(function* () { + const context = yield* RequestContextRef + const npm = yield* Npm.Service + + let useCount = 0 + + return ConfigService.of({ + directory: context.directory, + workspace: context.workspace, + nextUse: () => Effect.succeed(++useCount), + which: npm.which, + }) + }), +) + +export class ConfigServiceMap extends LayerMap.Service()("@opencode/example/ConfigServiceMap", { + lookup: (context: RequestContext) => + configServiceLayer.pipe(Layer.provide(Layer.succeed(RequestContextRef, RequestContextRef.of(context)))), + idleTimeToLive: "5 minutes", +}) {} + +export const appLayer = ConfigServiceMap.layer + +export const readConfig = Effect.fn("LayerMapExample.readConfig")(function* () { + const config = yield* ConfigService + + return { + directory: config.directory, + workspace: config.workspace, + useCount: yield* config.nextUse(), + } +}) + +export const handleRequest = Effect.fn("LayerMapExample.handleRequest")(function* (context: RequestContext) { + return yield* readConfig().pipe(Effect.provide(ConfigServiceMap.get(context))) +}) + +export const invalidateContext = (context: RequestContext) => ConfigServiceMap.invalidate(context) diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts new file mode 100644 index 0000000000000000000000000000000000000000..075a6ed093c6f0fc2f6036b8d2a0957b33b75ebe --- /dev/null +++ b/packages/core/src/plugin/models-dev.ts @@ -0,0 +1,183 @@ +import { define } from "./internal" +import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" +import { Effect, Stream } from "effect" +import { EventV2 } from "../event" +import { ModelsDev } from "../models-dev" +import { ProviderV2 } from "../provider" + +function released(date: string) { + const time = Date.parse(date) + return Number.isFinite(time) ? time : 0 +} + +function cost(input: ModelsDev.Model["cost"]): ModelV2Info["cost"] { + const base = { + input: input?.input ?? 0, + output: input?.output ?? 0, + cache: { + read: input?.cache_read ?? 0, + write: input?.cache_write ?? 0, + }, + } + return [ + base, + ...(input?.tiers?.map((item) => ({ + tier: item.tier, + input: item.input, + output: item.output, + cache: { + read: item.cache_read ?? 0, + write: item.cache_write ?? 0, + }, + })) ?? []), + ...(input?.context_over_200k + ? [ + { + tier: { + type: "context" as const, + size: 200_000, + }, + input: input.context_over_200k.input, + output: input.context_over_200k.output, + cache: { + read: input.context_over_200k.cache_read ?? 0, + write: input.context_over_200k.cache_write ?? 0, + }, + }, + ] + : []), + ] +} + +function mergeCost(base: ModelV2Info["cost"], override: ModelsDev.Model["cost"] | undefined) { + if (!override) return base + const next = cost(override) + const [baseDefault, ...baseTiers] = base + const [nextDefault, ...nextTiers] = next + const tierKey = (item: ModelV2Info["cost"][number]) => `${item.tier?.type ?? "base"}:${item.tier?.size ?? 0}` + const merge = (left: ModelV2Info["cost"][number], right: ModelV2Info["cost"][number]) => ({ + ...left, + ...right, + tier: right.tier ?? left.tier, + cache: { ...left.cache, ...right.cache }, + }) + const tiers = new Map(baseTiers.map((item) => [tierKey(item), item])) + for (const item of nextTiers) { + const current = tiers.get(tierKey(item)) + tiers.set(tierKey(item), current ? merge(current, item) : item) + } + return [merge(baseDefault ?? { input: 0, output: 0, cache: { read: 0, write: 0 } }, nextDefault), ...tiers.values()] +} + +function modeName(model: ModelsDev.Model, mode: string) { + return `${model.name} ${mode.charAt(0).toUpperCase()}${mode.slice(1)}` +} + +function applyModel( + draft: ModelV2Info, + model: ModelsDev.Model, + input: { + readonly name?: string + readonly cost?: ModelV2Info["cost"] + readonly request?: NonNullable["modes"]>[string]["provider"] + } = {}, +) { + draft.name = input.name ?? model.name + draft.family = model.family + draft.api = model.provider?.npm + ? { + id: model.id, + type: "aisdk", + package: model.provider.npm, + url: model.provider.api, + } + : { + id: model.id, + type: "native", + url: model.provider?.api, + settings: {}, + } + draft.capabilities = { + tools: model.tool_call, + input: [...(model.modalities?.input ?? [])], + output: [...(model.modalities?.output ?? [])], + } + draft.variants = [] + draft.time.released = released(model.release_date) + draft.cost = input.cost ?? cost(model.cost) + draft.status = model.status ?? "active" + draft.enabled = true + draft.limit = { + context: model.limit.context, + input: model.limit.input, + output: model.limit.output, + } + Object.assign(draft.request.headers, input.request?.headers ?? {}) + Object.assign(draft.request.body, input.request?.body ?? {}) +} + +export const ModelsDevPlugin = define({ + id: "models-dev", + effect: Effect.fn(function* (ctx) { + const modelsDev = yield* ModelsDev.Service + const events = yield* EventV2.Service + yield* ctx.integration.transform( + Effect.fn(function* (integrations) { + const data = yield* modelsDev.get() + for (const item of Object.values(data)) { + if (item.env.length === 0) continue + const integrationID = item.id + integrations.update(integrationID, (integration) => (integration.name = item.name)) + integrations.method.update({ + integrationID, + method: { type: "key" }, + }) + integrations.method.update({ + integrationID, + method: { type: "env", names: [...item.env] }, + }) + } + }), + ) + yield* ctx.catalog.transform( + Effect.fn(function* (catalog) { + const data = yield* modelsDev.get() + for (const item of Object.values(data)) { + const providerID = ProviderV2.ID.make(item.id) + catalog.provider.update(providerID, (provider) => { + provider.name = item.name + provider.api = item.npm + ? { + type: "aisdk", + package: item.npm, + url: item.api, + } + : { + type: "native", + url: item.api, + settings: {}, + } + }) + + for (const model of Object.values(item.models)) { + const baseCost = cost(model.cost) + catalog.model.update(providerID, model.id, (draft) => applyModel(draft, model, { cost: baseCost })) + for (const [mode, options] of Object.entries(model.experimental?.modes ?? {})) { + catalog.model.update(providerID, `${model.id}-${mode}`, (draft) => + applyModel(draft, model, { + name: modeName(model, mode), + cost: mergeCost(baseCost, options.cost), + request: options.provider, + }), + ) + } + } + } + }), + ) + yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( + Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))), + Effect.forkScoped({ startImmediately: true }), + ) + }), +}) diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts new file mode 100644 index 0000000000000000000000000000000000000000..d0bf82c2a0c08fdf125a1d0300469f45fda9b284 --- /dev/null +++ b/packages/core/src/plugin/promise.ts @@ -0,0 +1,93 @@ +export * as PluginPromise from "./promise" + +import { define } from "@opencode-ai/plugin/v2/effect" +import type { Plugin, PluginContext, Registration } from "@opencode-ai/plugin/v2/promise" +import { Effect, Scope } from "effect" + +// The Effect host hands back this registration shape; mirror it structurally so +// we do not have to alias the Effect package's `Registration` against the Promise one. +type HostRegistration = { readonly dispose: Effect.Effect } + +/** + * Adapts a Promise plugin into an Effect plugin so the existing Effect-only + * loader (`PluginV2` / `PluginInternal`) can run it unchanged. + * + * Hook registrations created during the async `setup` attach to the plugin's + * scope, so unloading the plugin disposes them. The captured fiber context + * preserves boot-time batching, so Promise-plugin transforms still coalesce + * into one reload per domain. + */ +export function fromPromise(plugin: Plugin) { + return define({ + id: plugin.id, + effect: (host) => + Effect.gen(function* () { + const scope = yield* Scope.Scope + const context = yield* Effect.context() + + // Run a hook registration on the plugin scope and resolve once it is registered. + const register = (effect: Effect.Effect): Promise => + Effect.runPromiseWith(context)(Scope.provide(scope)(effect)).then((registration) => ({ + dispose: () => Effect.runPromiseWith(context)(registration.dispose), + })) + + const run = (effect: Effect.Effect) => Effect.runPromiseWith(context)(effect) + + const transform = + (domain: { + transform: ( + callback: (draft: Draft) => Effect.Effect | void, + ) => Effect.Effect + }) => + (callback: (draft: Draft) => Promise | void) => + register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft))))) + + const context2: PluginContext = { + options: host.options, + agent: { + transform: transform(host.agent), + reload: () => run(host.agent.reload()), + }, + aisdk: { + sdk: (callback) => + register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))), + language: (callback) => + register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))), + }, + catalog: { + transform: transform(host.catalog), + reload: () => run(host.catalog.reload()), + }, + command: { + transform: transform(host.command), + reload: () => run(host.command.reload()), + }, + integration: { + transform: transform(host.integration), + reload: () => run(host.integration.reload()), + connection: { + active: (id) => Effect.runPromiseWith(context)(host.integration.connection.active(id)), + resolve: (connection) => Effect.runPromiseWith(context)(host.integration.connection.resolve(connection)), + }, + }, + plugin: { + add: (input) => { + const child = fromPromise(input) + return run(host.plugin.add(child)) + }, + remove: (id) => run(host.plugin.remove(id)), + }, + reference: { + transform: transform(host.reference), + reload: () => run(host.reference.reload()), + }, + skill: { + transform: transform(host.skill), + reload: () => run(host.skill.reload()), + }, + } + + yield* Effect.promise(() => Promise.resolve(plugin.setup(context2))) + }), + }) +} diff --git a/packages/core/src/plugin/provider.ts b/packages/core/src/plugin/provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..1749b474ed338579cef78d6f86bb8b98068f93d2 --- /dev/null +++ b/packages/core/src/plugin/provider.ts @@ -0,0 +1,71 @@ +import { AlibabaPlugin } from "./provider/alibaba" +import { AmazonBedrockPlugin } from "./provider/amazon-bedrock" +import { AnthropicPlugin } from "./provider/anthropic" +import { AzureCognitiveServicesPlugin, AzurePlugin } from "./provider/azure" +import { CerebrasPlugin } from "./provider/cerebras" +import { CloudflareAIGatewayPlugin } from "./provider/cloudflare-ai-gateway" +import { CloudflareWorkersAIPlugin } from "./provider/cloudflare-workers-ai" +import { CoherePlugin } from "./provider/cohere" +import { DeepInfraPlugin } from "./provider/deepinfra" +import { DynamicProviderPlugin } from "./provider/dynamic" +import { GatewayPlugin } from "./provider/gateway" +import { GithubCopilotPlugin } from "./provider/github-copilot" +import { GitLabPlugin } from "./provider/gitlab" +import { GooglePlugin } from "./provider/google" +import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./provider/google-vertex" +import { GroqPlugin } from "./provider/groq" +import { KiloPlugin } from "./provider/kilo" +import { LLMGatewayPlugin } from "./provider/llmgateway" +import { MistralPlugin } from "./provider/mistral" +import { NvidiaPlugin } from "./provider/nvidia" +import { OpenAIPlugin } from "./provider/openai" +import { SnowflakeCortexPlugin } from "./provider/snowflake-cortex" +import { OpenAICompatiblePlugin } from "./provider/openai-compatible" +import { OpencodePlugin } from "./provider/opencode" +import { OpenRouterPlugin } from "./provider/openrouter" +import { PerplexityPlugin } from "./provider/perplexity" +import { SapAICorePlugin } from "./provider/sap-ai-core" +import { TogetherAIPlugin } from "./provider/togetherai" +import { VercelPlugin } from "./provider/vercel" +import { VenicePlugin } from "./provider/venice" +import { XAIPlugin } from "./provider/xai" +import { ZenmuxPlugin } from "./provider/zenmux" +import type { PluginInternal } from "./internal" +import type { Scope } from "effect" + +export const ProviderPlugins: PluginInternal.Plugin[] = [ + AlibabaPlugin, + AmazonBedrockPlugin, + AnthropicPlugin, + AzureCognitiveServicesPlugin, + AzurePlugin, + CerebrasPlugin, + CloudflareAIGatewayPlugin, + CloudflareWorkersAIPlugin, + CoherePlugin, + DeepInfraPlugin, + GatewayPlugin, + GithubCopilotPlugin, + GitLabPlugin, + GooglePlugin, + GoogleVertexAnthropicPlugin, + GoogleVertexPlugin, + GroqPlugin, + KiloPlugin, + LLMGatewayPlugin, + MistralPlugin, + NvidiaPlugin, + OpencodePlugin, + SnowflakeCortexPlugin, + OpenAICompatiblePlugin, + OpenAIPlugin, + OpenRouterPlugin, + PerplexityPlugin, + SapAICorePlugin, + TogetherAIPlugin, + VercelPlugin, + VenicePlugin, + XAIPlugin, + ZenmuxPlugin, + DynamicProviderPlugin, +] diff --git a/packages/core/src/plugin/provider/alibaba.ts b/packages/core/src/plugin/provider/alibaba.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5c4be0d0becf6feb8d98e676b334105e1a8ed53 --- /dev/null +++ b/packages/core/src/plugin/provider/alibaba.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const AlibabaPlugin = define({ + id: "alibaba", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/alibaba") return + const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba")) + evt.sdk = mod.createAlibaba(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b830796d69f1e25f452cbf7539bd561474409ef --- /dev/null +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -0,0 +1,134 @@ +import { Effect } from "effect" +import type { LanguageModelV3 } from "@ai-sdk/provider" +import { define } from "../internal" +import { ProviderV2 } from "../../provider" + +type MantleSDK = { + languageModel: (modelID: string) => LanguageModelV3 + chat: (modelID: string) => LanguageModelV3 + responses: (modelID: string) => LanguageModelV3 +} + +// Bedrock cross-region inference profiles require regional prefixes only for +// specific model/region combinations. Keep the mapping narrow and avoid +// double-prefixing model IDs that models.dev already marks as global/us/eu/etc. +function resolveModelID(modelID: string, region: string | undefined) { + if (modelID.startsWith("arn:")) return modelID + + const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] + if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) return modelID + + const resolvedRegion = region ?? "us-east-1" + const regionPrefix = resolvedRegion.split("-")[0] + if (regionPrefix === "us") { + const requiresPrefix = [ + "nova-micro", + "nova-lite", + "nova-pro", + "nova-premier", + "nova-2", + "claude", + "deepseek.r1", + ].some((item) => modelID.includes(item)) + if (requiresPrefix && !resolvedRegion.startsWith("us-gov")) return `${regionPrefix}.${modelID}` + return modelID + } + if (regionPrefix === "eu") { + const regionRequiresPrefix = [ + "eu-west-1", + "eu-west-2", + "eu-west-3", + "eu-north-1", + "eu-central-1", + "eu-south-1", + "eu-south-2", + ].some((item) => resolvedRegion.includes(item)) + const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((item) => + modelID.includes(item), + ) + return regionRequiresPrefix && modelRequiresPrefix ? `${regionPrefix}.${modelID}` : modelID + } + if (regionPrefix !== "ap") return modelID + + const australia = ["ap-southeast-2", "ap-southeast-4"].includes(resolvedRegion) + if (australia && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((item) => modelID.includes(item))) { + return `au.${modelID}` + } + + const prefix = resolvedRegion === "ap-northeast-1" ? "jp" : "apac" + return ["claude", "nova-lite", "nova-micro", "nova-pro"].some((item) => modelID.includes(item)) + ? `${prefix}.${modelID}` + : modelID +} + +function selectMantleModel(sdk: MantleSDK, modelID: string) { + if (modelID === "openai.gpt-oss-safeguard-20b" || modelID === "openai.gpt-oss-safeguard-120b") + return sdk.chat(modelID) + return sdk.responses(modelID) +} + +export const AmazonBedrockPlugin = define({ + id: "amazon-bedrock", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue + evt.provider.update(item.provider.id, (provider) => { + if (provider.api.type !== "aisdk") return + if (typeof provider.request.body.endpoint !== "string") return + // The AI SDK expects a base URL, but users configure Bedrock private/VPC + // endpoints as `endpoint`; move it into the catalog endpoint URL once. + provider.api.url = provider.request.body.endpoint + delete provider.request.body.endpoint + }) + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return + const options = { ...evt.options } + const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE + const region = typeof options.region === "string" ? options.region : (process.env.AWS_REGION ?? "us-east-1") + const bearerToken = + process.env.AWS_BEARER_TOKEN_BEDROCK ?? + (typeof options.bearerToken === "string" ? options.bearerToken : undefined) + if (bearerToken && !process.env.AWS_BEARER_TOKEN_BEDROCK) process.env.AWS_BEARER_TOKEN_BEDROCK = bearerToken + const containerCreds = Boolean( + process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI, + ) + + options.region = region + if (typeof options.endpoint === "string") options.baseURL = options.endpoint + if (!bearerToken && options.credentialProvider === undefined) { + // Do not gate SDK creation on explicit AWS env vars. The default chain + // also handles ~/.aws/credentials, SSO, process creds, and instance roles. + const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers")) + options.credentialProvider = fromNodeProviderChain(profile ? { profile } : {}) + } + + if (evt.package === "@ai-sdk/amazon-bedrock/mantle") { + const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock/mantle")) + evt.sdk = mod.createBedrockMantle(options) + return + } + + const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock")) + evt.sdk = mod.createAmazonBedrock(options) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return + if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") { + evt.language = selectMantleModel(evt.sdk, evt.model.api.id) + return + } + const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION + evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region)) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/anthropic.ts b/packages/core/src/plugin/provider/anthropic.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf883a0687f3eef99572505d265ed02ee89ee6f4 --- /dev/null +++ b/packages/core/src/plugin/provider/anthropic.ts @@ -0,0 +1,27 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const AnthropicPlugin = define({ + id: "anthropic", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/anthropic") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["anthropic-beta"] = + "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" + }) + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/anthropic") return + const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic")) + evt.sdk = mod.createAnthropic(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e1f9d9b48f4cee81ca6fa2310ee015155886b1d --- /dev/null +++ b/packages/core/src/plugin/provider/azure.ts @@ -0,0 +1,82 @@ +import { Effect } from "effect" +import { define } from "../internal" +import { ProviderV2 } from "../../provider" + +function selectLanguage(sdk: any, modelID: string, useChat: boolean) { + if (useChat && sdk.chat) return sdk.chat(modelID) + if (sdk.responses) return sdk.responses(modelID) + if (sdk.messages) return sdk.messages(modelID) + if (sdk.chat) return sdk.chat(modelID) + return sdk.languageModel(modelID) +} + +export const AzurePlugin = define({ + id: "azure", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/azure") continue + const configured = item.provider.request.body.resourceName + const resourceName = + typeof configured === "string" && configured.trim() !== "" ? configured : process.env.AZURE_RESOURCE_NAME + if (!resourceName) continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.body.resourceName = resourceName + }) + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/azure") return + if (evt.model.providerID === ProviderV2.ID.azure) { + if ( + !evt.options.resourceName && + !evt.options.baseURL && + (evt.model.api.type !== "aisdk" || !evt.model.api.url) + ) { + throw new Error( + "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it", + ) + } + } + const mod = yield* Effect.promise(() => import("@ai-sdk/azure")) + evt.sdk = mod.createAzure(evt.options) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.azure) return + evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) + }), + ) + }), +}) + +export const AzureCognitiveServicesPlugin = define({ + id: "azure-cognitive-services", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME + if (!resourceName) return + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (!item.provider.id.includes("azure-cognitive-services")) continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.body.baseURL = `https://${resourceName}.cognitiveservices.azure.com/openai` + }) + } + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return + evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts new file mode 100644 index 0000000000000000000000000000000000000000..0fd651160fe2ff3b78a22f7b8108351d80841c33 --- /dev/null +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -0,0 +1,26 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const CerebrasPlugin = define({ + id: "cerebras", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/cerebras") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode" + }) + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/cerebras") return + const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras")) + evt.sdk = mod.createCerebras(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts new file mode 100644 index 0000000000000000000000000000000000000000..2803cb7a8e8fb11a38ce8173d44f16e75caede04 --- /dev/null +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -0,0 +1,87 @@ +import os from "os" +import { InstallationVersion } from "../../installation/version" +import { Effect, Option, Schema } from "effect" +import { define } from "../internal" + +export const CloudflareAIGatewayPlugin = define({ + id: "cloudflare-ai-gateway", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "ai-gateway-provider") return + if (evt.options.baseURL) return + + const config = gatewayConfig(evt.options) + if (!config) return + const metadata = gatewayMetadata(evt.options) + const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")).pipe(Effect.orDie) + const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")).pipe( + Effect.orDie, + ) + const gateway = createAiGateway({ + accountId: config.accountId, + gateway: config.gatewayId, + apiKey: config.apiKey, + options: gatewayOptions(evt.options, metadata), + } as any) + evt.sdk = { + languageModel(modelID: string) { + // Workers AI is the only first-party provider whose upstream is Cloudflare itself, so it is + // the only one that should receive the Cloudflare token as its upstream Authorization header. + // The Unified API addresses Workers AI both with the explicit "workers-ai/" prefix and as + // bare "@cf/..." ids. Third-party providers must not receive the token; they rely on the + // gateway's stored/BYOK keys instead. + const isWorkersAi = modelID.startsWith("workers-ai/") || modelID.startsWith("@cf/") + const unified = createUnified(isWorkersAi ? { apiKey: config.apiKey } : {}) + return gateway(unified(modelID)) + }, + } + }), + ) + }), +}) + +type GatewayConfig = { + accountId: string + gatewayId: string + apiKey: string +} + +const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) + +function gatewayConfig(options: Record): GatewayConfig | undefined { + const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") + // Credential projection copies key metadata into options. The prompt stores the + // gateway as gatewayId, while older config examples may use gateway. + const gatewayId = + process.env.CLOUDFLARE_GATEWAY_ID ?? stringOption(options, "gatewayId") ?? stringOption(options, "gateway") + const apiKey = process.env.CLOUDFLARE_API_TOKEN ?? process.env.CF_AIG_TOKEN ?? stringOption(options, "apiKey") + if (!accountId || !gatewayId || !apiKey) return undefined + + return { accountId, gatewayId, apiKey } +} + +function gatewayMetadata(options: Record) { + // Preserve the legacy cf-aig-metadata header escape hatch for gateway logging + // metadata, but prefer the typed metadata option when present. + if (options.metadata !== undefined) return options.metadata + const raw = (options.headers as Record | undefined)?.["cf-aig-metadata"] + return raw ? Option.getOrUndefined(decodeJson(raw)) : undefined +} + +function gatewayOptions(options: Record, metadata: unknown) { + return { + metadata, + cacheTtl: options.cacheTtl, + cacheKey: options.cacheKey, + skipCache: options.skipCache, + collectLog: options.collectLog, + headers: { + "User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`, + }, + } +} + +function stringOption(options: Record, key: string) { + return typeof options[key] === "string" ? options[key] : undefined +} diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a1c533eb552186a44120ff0388bae185c73cca2 --- /dev/null +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -0,0 +1,81 @@ +import os from "os" +import { InstallationVersion } from "../../installation/version" +import { Effect } from "effect" +import { define } from "../internal" +import { ProviderV2 } from "../../provider" + +const providerID = ProviderV2.ID.make("cloudflare-workers-ai") + +export const CloudflareWorkersAIPlugin = define({ + id: "cloudflare-workers-ai", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + const item = evt.provider.get(providerID) + if (!item) return + evt.provider.update(item.provider.id, (provider) => { + if (provider.api.type !== "aisdk") return + if (provider.api.url) return + const accountId = resolveAccountId(provider.request.body) + if (accountId) provider.api.url = workersEndpoint(accountId) + }) + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.model.providerID !== providerID) return + if (evt.package !== "@ai-sdk/openai-compatible") return + + const accountId = resolveAccountId(evt.options) + if (!hasWorkersEndpoint(evt.model.api) && !accountId) return + const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) + evt.sdk = mod.createOpenAICompatible( + sdkOptions({ + ...evt.options, + baseURL: evt.options.baseURL ?? (accountId ? workersEndpoint(accountId) : undefined), + }) as any, + ) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== providerID) return + evt.language = evt.sdk.languageModel(evt.model.api.id) + }), + ) + }), +}) + +function resolveAccountId(options: Record) { + return process.env.CLOUDFLARE_ACCOUNT_ID ?? stringOption(options, "accountId") +} + +function workersEndpoint(accountId: string) { + return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1` +} + +function hasWorkersEndpoint(api: ProviderV2.Api) { + return api.type === "aisdk" && Boolean(api.url) +} + +function sdkOptions(options: Record) { + return { + ...options, + baseURL: expandAccountId(options.baseURL), + apiKey: process.env.CLOUDFLARE_API_KEY ?? options.apiKey, + headers: { + "User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`, + ...options.headers, + }, + name: providerID, + } +} + +function expandAccountId(baseURL: unknown) { + if (typeof baseURL !== "string") return baseURL + return baseURL.replaceAll("${CLOUDFLARE_ACCOUNT_ID}", process.env.CLOUDFLARE_ACCOUNT_ID ?? "${CLOUDFLARE_ACCOUNT_ID}") +} + +function stringOption(options: Record, key: string) { + return typeof options[key] === "string" ? options[key] : undefined +} diff --git a/packages/core/src/plugin/provider/cohere.ts b/packages/core/src/plugin/provider/cohere.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ca0708577aafdf59928abebd6fc96bf38f45497 --- /dev/null +++ b/packages/core/src/plugin/provider/cohere.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const CoherePlugin = define({ + id: "cohere", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/cohere") return + const mod = yield* Effect.promise(() => import("@ai-sdk/cohere")) + evt.sdk = mod.createCohere(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/deepinfra.ts b/packages/core/src/plugin/provider/deepinfra.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b23e08ba4a34aed50b628e1be027336e06434c7 --- /dev/null +++ b/packages/core/src/plugin/provider/deepinfra.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const DeepInfraPlugin = define({ + id: "deepinfra", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/deepinfra") return + const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra")) + evt.sdk = mod.createDeepInfra(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/dynamic.ts b/packages/core/src/plugin/provider/dynamic.ts new file mode 100644 index 0000000000000000000000000000000000000000..c84a6ed51f895f7972b907a2f32113c4f412ce5e --- /dev/null +++ b/packages/core/src/plugin/provider/dynamic.ts @@ -0,0 +1,31 @@ +import { Effect } from "effect" +import { pathToFileURL } from "url" +import { define } from "../internal" +import { Npm } from "../../npm" + +export const DynamicProviderPlugin = define({ + id: "dynamic-provider", + effect: Effect.fn(function* (ctx) { + const npm = yield* Npm.Service + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.sdk) return + + const installedPath = evt.package.startsWith("file://") + ? evt.package + : (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint + if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) + + const mod = yield* Effect.promise(async () => { + return (await import( + installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href + )) as Record any> + }).pipe(Effect.orDie) + const match = Object.keys(mod).find((name) => name.startsWith("create")) + if (!match) throw new Error(`Package ${evt.package} has no provider factory export`) + + evt.sdk = mod[match](evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/gateway.ts b/packages/core/src/plugin/provider/gateway.ts new file mode 100644 index 0000000000000000000000000000000000000000..f097dcaca3f6500e70990dc2f429ee4715156cd9 --- /dev/null +++ b/packages/core/src/plugin/provider/gateway.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const GatewayPlugin = define({ + id: "gateway", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/gateway") return + const mod = yield* Effect.promise(() => import("@ai-sdk/gateway")) + evt.sdk = mod.createGateway(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts new file mode 100644 index 0000000000000000000000000000000000000000..6fd5b50fb4626956f0aa18f8e58c33e563426c2d --- /dev/null +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -0,0 +1,52 @@ +import { Effect } from "effect" +import { ModelV2 } from "../../model" +import { ProviderV2 } from "../../provider" +import type { PluginContext } from "@opencode-ai/plugin/v2/effect" + +export const GithubCopilotPlugin = { + id: "github-copilot", + effect: Effect.fn(function* (ctx: PluginContext) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + const item = evt.provider.get(ProviderV2.ID.githubCopilot) + if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return + evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + // This chat-only alias conflicts with the Copilot GPT-5 Responses route, + // so hide it only for Copilot rather than for every provider catalog. + model.enabled = false + }) + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/github-copilot") return + const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider")) + evt.sdk = mod.createOpenaiCompatible(evt.options) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return + if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { + evt.language = evt.sdk.languageModel(evt.model.api.id) + return + } + if (evt.options.endpoint === "responses" && evt.sdk.responses) { + evt.language = evt.sdk.responses(evt.model.api.id) + return + } + if (evt.options.endpoint === "chat" && evt.sdk.chat) { + evt.language = evt.sdk.chat(evt.model.api.id) + return + } + const match = /^gpt-(\d+)/.exec(evt.model.api.id) + // Copilot supports Responses for GPT-5 class models, except mini variants + // which still need the chat-completions endpoint. + evt.language = + match && Number(match[1]) >= 5 && !evt.model.api.id.startsWith("gpt-5-mini") && evt.sdk.responses + ? evt.sdk.responses(evt.model.api.id) + : evt.sdk.chat(evt.model.api.id) + }), + ) + }), +} diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts new file mode 100644 index 0000000000000000000000000000000000000000..8723cdaac2e37dd6b5f7d2b2cfb5c32bed422c9b --- /dev/null +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -0,0 +1,65 @@ +import os from "os" +import { InstallationVersion } from "../../installation/version" +import { Effect } from "effect" +import { define } from "../internal" +import { ProviderV2 } from "../../provider" + +export const GitLabPlugin = define({ + id: "gitlab", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "gitlab-ai-provider") return + const mod = yield* Effect.promise(() => import("gitlab-ai-provider")) + evt.sdk = mod.createGitLab({ + ...evt.options, + instanceUrl: + typeof evt.options.instanceUrl === "string" + ? evt.options.instanceUrl + : (process.env.GITLAB_INSTANCE_URL ?? "https://gitlab.com"), + apiKey: typeof evt.options.apiKey === "string" ? evt.options.apiKey : process.env.GITLAB_TOKEN, + aiGatewayHeaders: { + "User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${mod.VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, + "anthropic-beta": "context-1m-2025-08-07", + ...evt.options.aiGatewayHeaders, + }, + featureFlags: { + duo_agent_platform_agentic_chat: true, + duo_agent_platform: true, + ...evt.options.featureFlags, + }, + }) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.gitlab) return + const featureFlags = + typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {} + if (evt.model.api.id.startsWith("duo-workflow-")) { + const gitlab = yield* Effect.promise(() => import("gitlab-ai-provider")).pipe(Effect.orDie) + const workflowRef = + typeof evt.model.request.body.workflowRef === "string" ? evt.model.request.body.workflowRef : undefined + const workflowDefinition = + typeof evt.model.request.body.workflowDefinition === "string" + ? evt.model.request.body.workflowDefinition + : undefined + const language = evt.sdk.workflowChat( + gitlab.isWorkflowModel(evt.model.api.id) ? evt.model.api.id : "duo-workflow", + { + featureFlags, + workflowDefinition, + }, + ) + if (workflowRef) language.selectedModelRef = workflowRef + evt.language = language + return + } + evt.language = evt.sdk.agenticChat(evt.model.api.id, { + aiGatewayHeaders: evt.options.aiGatewayHeaders, + featureFlags, + }) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e643c9f519e32f253eb4f9dafa0573d47ac175d --- /dev/null +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -0,0 +1,173 @@ +import { Effect } from "effect" +import { define } from "../internal" +import { ProviderV2 } from "../../provider" + +function resolveProject(options: Record) { + // models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex, while Google SDKs + // and ADC examples commonly use the broader Google Cloud project aliases. + return ( + options.project ?? + process.env.GOOGLE_VERTEX_PROJECT ?? + process.env.GOOGLE_CLOUD_PROJECT ?? + process.env.GCP_PROJECT ?? + process.env.GCLOUD_PROJECT + ) +} + +function resolveLocation(options: Record) { + return ( + options.location ?? + process.env.GOOGLE_VERTEX_LOCATION ?? + process.env.GOOGLE_CLOUD_LOCATION ?? + process.env.VERTEX_LOCATION ?? + "us-central1" + ) +} + +function vertexEndpoint(location: string) { + if (location === "global") return "aiplatform.googleapis.com" + return `${location}-aiplatform.googleapis.com` +} + +function replaceVertexVars(value: string, project: string | undefined, location: string) { + // Vertex OpenAI-compatible endpoints are stored as templates in the catalog; + // expand them after provider config/env project and location have been resolved. + return value + .replaceAll("${GOOGLE_VERTEX_PROJECT}", project ?? "${GOOGLE_VERTEX_PROJECT}") + .replaceAll("${GOOGLE_VERTEX_LOCATION}", location) + .replaceAll("${GOOGLE_VERTEX_ENDPOINT}", vertexEndpoint(location)) +} + +function authFetch(fetchWithRuntimeOptions?: unknown) { + // Native Vertex SDKs handle ADC internally. OpenAI-compatible Vertex endpoints + // do not, so inject a Google access token into their fetch path. + return async (input: Parameters[0], init?: RequestInit) => { + const { GoogleAuth } = await import("google-auth-library") + const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }) + const client = await auth.getClient() + const token = await client.getAccessToken() + const headers = new Headers(init?.headers) + headers.set("Authorization", `Bearer ${token.token}`) + return typeof fetchWithRuntimeOptions === "function" + ? fetchWithRuntimeOptions(input, { ...init, headers }) + : fetch(input, { ...init, headers }) + } +} + +export const GoogleVertexPlugin = define({ + id: "google-vertex", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if ( + item.provider.api.package !== "@ai-sdk/google-vertex" && + !( + item.provider.id === ProviderV2.ID.googleVertex && + item.provider.api.package.includes("@ai-sdk/openai-compatible") + ) + ) + continue + const project = resolveProject(item.provider.request.body) + const location = String(resolveLocation(item.provider.request.body)) + evt.provider.update(item.provider.id, (provider) => { + if (project) provider.request.body.project = project + provider.request.body.location = location + if (provider.api.type === "aisdk" && provider.api.url) { + provider.api.url = replaceVertexVars(provider.api.url, project, location) + } + if (provider.api.type === "aisdk" && provider.api.package.includes("@ai-sdk/openai-compatible")) { + provider.request.body.fetch = authFetch(provider.request.body.fetch) + } + }) + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { + evt.options.fetch = authFetch(evt.options.fetch) + return + } + if (evt.package !== "@ai-sdk/google-vertex") return + const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex")) + const project = resolveProject(evt.options) + const location = resolveLocation(evt.options) + const options = { ...evt.options } + delete options.fetch + evt.sdk = mod.createVertex({ + ...options, + project, + location, + }) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.googleVertex) return + evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) + }), + ) + }), +}) + +export const GoogleVertexAnthropicPlugin = define({ + id: "google-vertex-anthropic", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue + const project = + item.provider.request.body.project ?? + process.env.GOOGLE_CLOUD_PROJECT ?? + process.env.GCP_PROJECT ?? + process.env.GCLOUD_PROJECT + const location = + item.provider.request.body.location ?? + process.env.GOOGLE_CLOUD_LOCATION ?? + process.env.VERTEX_LOCATION ?? + "global" + evt.provider.update(item.provider.id, (provider) => { + if (project) provider.request.body.project = project + provider.request.body.location = location + }) + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/google-vertex/anthropic") return + const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) + const project = + typeof evt.options.project === "string" + ? evt.options.project + : (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT) + const location = + typeof evt.options.location === "string" + ? evt.options.location + : (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global") + evt.sdk = mod.createVertexAnthropic({ + ...evt.options, + project, + location, + // Continental multi-regions (eu, us) require Regional Endpoint Platform + // domains; the default {region}-aiplatform.googleapis.com does not resolve. + ...((location === "eu" || location === "us") && project && !evt.options.baseURL + ? { + baseURL: `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models`, + } + : {}), + }) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return + evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/google.ts b/packages/core/src/plugin/provider/google.ts new file mode 100644 index 0000000000000000000000000000000000000000..476af5b912134189e794163c2d48ddd15f31582e --- /dev/null +++ b/packages/core/src/plugin/provider/google.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const GooglePlugin = define({ + id: "google", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/google") return + const mod = yield* Effect.promise(() => import("@ai-sdk/google")) + evt.sdk = mod.createGoogleGenerativeAI(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/groq.ts b/packages/core/src/plugin/provider/groq.ts new file mode 100644 index 0000000000000000000000000000000000000000..0bddb44309504048be0fa1fc12802872633bdb78 --- /dev/null +++ b/packages/core/src/plugin/provider/groq.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const GroqPlugin = define({ + id: "groq", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/groq") return + const mod = yield* Effect.promise(() => import("@ai-sdk/groq")) + evt.sdk = mod.createGroq(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ee6670ee5ddf601dbab5ae24897c4caf4143202 --- /dev/null +++ b/packages/core/src/plugin/provider/kilo.ts @@ -0,0 +1,21 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const KiloPlugin = define({ + id: "kilo", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://api.kilo.ai/api/gateway") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" + }) + } + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts new file mode 100644 index 0000000000000000000000000000000000000000..eafc5edd6c7ac0dae3bb067a56c58f220383bc7d --- /dev/null +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -0,0 +1,26 @@ +import { Effect } from "effect" +import { define } from "../internal" +import { Integration } from "../../integration" + +export const LLMGatewayPlugin = define({ + id: "llmgateway", + effect: Effect.fn(function* (ctx) { + const integrations = yield* Integration.Service + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.disabled) continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue + if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" + provider.request.headers["X-Source"] = "opencode" + }) + } + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/mistral.ts b/packages/core/src/plugin/provider/mistral.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7319756598967d3a233d5a6406c05216f6b9841 --- /dev/null +++ b/packages/core/src/plugin/provider/mistral.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const MistralPlugin = define({ + id: "mistral", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/mistral") return + const mod = yield* Effect.promise(() => import("@ai-sdk/mistral")) + evt.sdk = mod.createMistral(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts new file mode 100644 index 0000000000000000000000000000000000000000..449599727c2215ec6954f592b3507a4107ca3d00 --- /dev/null +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -0,0 +1,22 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const NvidiaPlugin = define({ + id: "nvidia", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://integrate.api.nvidia.com/v1") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" + provider.request.headers["X-BILLING-INVOKE-ORIGIN"] ??= "OpenCode" + }) + } + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/openai-compatible.ts b/packages/core/src/plugin/provider/openai-compatible.ts new file mode 100644 index 0000000000000000000000000000000000000000..d602ed0ff9571605a5fc91477868aa3b839214fa --- /dev/null +++ b/packages/core/src/plugin/provider/openai-compatible.ts @@ -0,0 +1,17 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const OpenAICompatiblePlugin = define({ + id: "openai-compatible", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.sdk) return + if (!evt.package.includes("@ai-sdk/openai-compatible")) return + if (evt.options.includeUsage !== false) evt.options.includeUsage = true + const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) + evt.sdk = mod.createOpenAICompatible(evt.options as any) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts new file mode 100644 index 0000000000000000000000000000000000000000..46553eaff42909c98f14707e24ae50e59d334097 --- /dev/null +++ b/packages/core/src/plugin/provider/openai.ts @@ -0,0 +1,292 @@ +import { createServer } from "node:http" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" +import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import { Deferred, Effect } from "effect" +import type { Scope } from "effect" +import { Credential } from "../../credential" +import { InstallationVersion } from "../../installation/version" +import { Integration } from "../../integration" +import { ModelV2 } from "../../model" +import { OauthCallbackPage } from "../../oauth/page" +import { ProviderV2 } from "../../provider" +import type { PluginInternal } from "../internal" + +const clientID = "app_EMoamEEZ73f0CkXaXp7hrann" +const issuer = "https://auth.openai.com" +const callbackPort = 1455 +const pollingSafetyMargin = 3000 +const browserMethodID = Integration.MethodID.make("chatgpt-browser") +const headlessMethodID = Integration.MethodID.make("chatgpt-headless") + +type Pkce = { + verifier: string + challenge: string +} + +type TokenResponse = { + id_token: string + access_token: string + refresh_token: string + expires_in?: number +} + +type Claims = { + chatgpt_account_id?: string + organizations?: Array<{ id: string }> + "https://api.openai.com/auth"?: { chatgpt_account_id?: string } +} + +const browser = { + integrationID: Integration.ID.make("openai"), + method: { + id: browserMethodID, + type: "oauth", + label: "ChatGPT Pro/Plus (browser)", + }, + authorize: () => + Effect.gen(function* () { + const pkce = yield* Effect.promise(generatePKCE) + const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer) + const code = yield* Deferred.make() + const redirect = `http://localhost:${callbackPort}/auth/callback` + const server = createServer((request, response) => { + const url = new URL(request.url ?? "/", `http://localhost:${callbackPort}`) + if (url.pathname !== "/auth/callback") { + response.writeHead(404).end("Not found") + return + } + const error = url.searchParams.get("error_description") ?? url.searchParams.get("error") + const value = url.searchParams.get("code") + if (error) { + Effect.runFork(Deferred.fail(code, new Error(error))) + response + .writeHead(400, { "Content-Type": "text/html" }) + .end(OauthCallbackPage.error(error, { provider: "ChatGPT" })) + return + } + if (!value || url.searchParams.get("state") !== state) { + const message = value ? "Invalid OAuth state" : "Missing authorization code" + Effect.runFork(Deferred.fail(code, new Error(message))) + response + .writeHead(400, { "Content-Type": "text/html" }) + .end(OauthCallbackPage.error(message, { provider: "ChatGPT" })) + return + } + Effect.runFork(Deferred.succeed(code, value)) + response.writeHead(200, { "Content-Type": "text/html" }).end(OauthCallbackPage.success({ provider: "ChatGPT" })) + }) + yield* Effect.callback((resume) => { + server.once("error", (error) => resume(Effect.fail(error))) + server.listen(callbackPort, "localhost", () => resume(Effect.void)) + }) + yield* Effect.addFinalizer(() => Effect.sync(() => server.close())) + return { + mode: "auto" as const, + url: authorizeURL(redirect, pkce, state), + instructions: "Complete authorization in your browser. This window will close automatically.", + callback: Deferred.await(code).pipe( + Effect.flatMap((value) => exchange(value, redirect, pkce)), + Effect.map((tokens) => credential(browserMethodID, tokens)), + ), + } + }), + refresh: (value) => refresh(browserMethodID, value), +} satisfies IntegrationOAuthMethodRegistration + +const headless = { + integrationID: Integration.ID.make("openai"), + method: { + id: headlessMethodID, + type: "oauth", + label: "ChatGPT Pro/Plus (headless)", + }, + authorize: () => + Effect.gen(function* () { + const device = yield* request<{ device_auth_id: string; user_code: string; interval: string }>( + `${issuer}/api/accounts/deviceauth/usercode`, + { + method: "POST", + headers: headers("application/json"), + body: JSON.stringify({ client_id: clientID }), + }, + ) + const interval = Math.max(Number.parseInt(device.interval) || 5, 1) * 1000 + return { + mode: "auto" as const, + url: `${issuer}/codex/device`, + instructions: `Enter code: ${device.user_code}`, + callback: Effect.gen(function* () { + while (true) { + const response = yield* Effect.tryPromise({ + try: (signal) => + fetch(`${issuer}/api/accounts/deviceauth/token`, { + method: "POST", + headers: headers("application/json"), + body: JSON.stringify({ device_auth_id: device.device_auth_id, user_code: device.user_code }), + signal, + }), + catch: (cause) => cause, + }) + if (response.ok) { + const data = (yield* Effect.promise(() => response.json())) as { + authorization_code: string + code_verifier: string + } + return credential( + headlessMethodID, + yield* exchange(data.authorization_code, `${issuer}/deviceauth/callback`, { + verifier: data.code_verifier, + challenge: "", + }), + ) + } + if (response.status !== 403 && response.status !== 404) { + return yield* Effect.fail(new Error(`Device authorization failed: ${response.status}`)) + } + yield* Effect.sleep(interval + pollingSafetyMargin) + } + }), + } + }), + refresh: (value) => refresh(headlessMethodID, value), +} satisfies IntegrationOAuthMethodRegistration + +export const OpenAIPlugin = define({ + id: "openai", + effect: Effect.fn(function* (ctx) { + yield* ctx.integration.transform((draft) => { + draft.method.update(browser) + draft.method.update(headless) + }) + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai") continue + if (!item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) continue + evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => { + // OpenAIPlugin sends OpenAI models through Responses; this alias is a + // chat-completions-only model, so hide it only from OpenAI's catalog. + model.enabled = false + }) + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/openai") return + const mod = yield* Effect.promise(() => import("@ai-sdk/openai")) + evt.sdk = mod.createOpenAI(evt.options) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.openai) return + evt.language = evt.sdk.responses(evt.model.api.id) + }), + ) + }), +} satisfies PluginInternal.Plugin) + +function headers(contentType: string) { + return { "Content-Type": contentType, "User-Agent": `opencode/${InstallationVersion}` } +} + +function exchange(code: string, redirect: string, pkce: Pkce) { + return request(`${issuer}/oauth/token`, { + method: "POST", + headers: headers("application/x-www-form-urlencoded"), + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: redirect, + client_id: clientID, + code_verifier: pkce.verifier, + }).toString(), + }) +} + +function refresh(methodID: Integration.MethodID, value: Pick) { + return request(`${issuer}/oauth/token`, { + method: "POST", + headers: headers("application/x-www-form-urlencoded"), + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: value.refresh, + client_id: clientID, + }).toString(), + }).pipe( + Effect.map((tokens) => { + const next = credential(methodID, tokens) + return Credential.OAuth.make({ ...next, metadata: next.metadata ?? value.metadata }) + }), + ) +} + +function request(url: string, init: RequestInit) { + return Effect.tryPromise({ + try: async (signal) => { + const response = await fetch(url, { ...init, signal }) + if (!response.ok) throw new Error(`Request failed: ${response.status}`) + return response.json() as Promise + }, + catch: (cause) => cause, + }) +} + +function credential(methodID: Integration.MethodID, tokens: TokenResponse) { + const accountID = extractAccountID(tokens) + return Credential.OAuth.make({ + type: "oauth", + methodID, + refresh: tokens.refresh_token, + access: tokens.access_token, + expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + metadata: accountID ? { accountID } : undefined, + }) +} + +async function generatePKCE(): Promise { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + const verifier = Array.from(crypto.getRandomValues(new Uint8Array(43)), (byte) => chars[byte % chars.length]).join("") + const challenge = base64UrlEncode(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))) + return { verifier, challenge } +} + +function base64UrlEncode(buffer: ArrayBuffer) { + return Buffer.from(buffer).toString("base64url") +} + +function authorizeURL(redirect: string, pkce: Pkce, state: string) { + return `${issuer}/oauth/authorize?${new URLSearchParams({ + response_type: "code", + client_id: clientID, + redirect_uri: redirect, + scope: "openid profile email offline_access", + code_challenge: pkce.challenge, + code_challenge_method: "S256", + id_token_add_organizations: "true", + codex_cli_simplified_flow: "true", + state, + originator: "opencode", + })}` +} + +function extractAccountID(tokens: TokenResponse) { + return claim(tokens.id_token) ?? claim(tokens.access_token) +} + +function claim(token: string) { + const part = token.split(".")[1] + if (!part) return + try { + const claims = JSON.parse(Buffer.from(part, "base64url").toString()) as Claims + return ( + claims.chatgpt_account_id ?? + claims["https://api.openai.com/auth"]?.chatgpt_account_id ?? + claims.organizations?.[0]?.id + ) + } catch { + return + } +} diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bd78cae415d213ac2b939c376fcfc246dec71e8 --- /dev/null +++ b/packages/core/src/plugin/provider/opencode.ts @@ -0,0 +1,320 @@ +import { Duration, Effect, Schema, Semaphore, Stream } from "effect" +import type { Scope } from "effect" +import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/v2/effect/integration" +import { define } from "@opencode-ai/plugin/v2/effect/plugin" +import type { CredentialValue } from "@opencode-ai/sdk/v2/types" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { EventV2 } from "../../event" +import { Credential } from "../../credential" +import { Integration } from "../../integration" +import { ModelV2 } from "../../model" +import { ProviderV2 } from "../../provider" +import { ConfigProviderV1 } from "../../v1/config/provider" +import { ConfigProviderOptionsV1 } from "../../v1/config/provider-options" +import { ConfigV1 } from "../../v1/config/config" + +const defaultServer = "https://opencode.ai/console" +const clientID = "opencode-cli" +const methodID = Integration.MethodID.make("device") +const RemoteResponse = Schema.Struct({ config: ConfigV1.Info }) +const Device = Schema.Struct({ + device_code: Schema.String, + user_code: Schema.String, + verification_uri_complete: Schema.String, + expires_in: Schema.Number, + interval: Schema.Number, +}) +const Token = Schema.Struct({ + access_token: Schema.String, + refresh_token: Schema.String, + expires_in: Schema.Number, +}) +const TokenPending = Schema.Struct({ error: Schema.String }) +const DeviceToken = Schema.Union([Token, TokenPending]) +const User = Schema.Struct({ id: Schema.String, email: Schema.String }) +const Org = Schema.Struct({ id: Schema.String, name: Schema.String }) + +function oauth(http: HttpClient.HttpClient) { + return { + integrationID: Integration.ID.make("opencode"), + method: { + id: methodID, + type: "oauth", + label: "OpenCode Console account", + }, + authorize: () => + Effect.gen(function* () { + const device = yield* post(http, `${defaultServer}/auth/device/code`, { client_id: clientID }, Device) + const verification = yield* Effect.try({ + try: () => { + const url = new URL(device.verification_uri_complete, `${defaultServer}/`) + if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("expected HTTP(S)") + return url + }, + catch: (cause) => + new Error(`Invalid device verification URL: ${cause instanceof Error ? cause.message : String(cause)}`), + }) + return { + mode: "auto" as const, + url: verification.href, + instructions: `Enter code: ${device.user_code}`, + callback: poll(http, defaultServer, device.device_code, Duration.seconds(device.interval)), + } + }), + refresh: (credential) => + Effect.gen(function* () { + const server = typeof credential.metadata?.server === "string" ? credential.metadata.server : defaultServer + const token = yield* post( + http, + `${server}/auth/device/token`, + { grant_type: "refresh_token", refresh_token: credential.refresh, client_id: clientID }, + Token, + ) + return { + ...credential, + access: token.access_token, + refresh: token.refresh_token, + expires: Date.now() + token.expires_in * 1000, + } + }), + label: (credential) => { + return typeof credential.metadata?.orgName === "string" ? credential.metadata.orgName : undefined + }, + } satisfies IntegrationOAuthMethodRegistration +} + +export const OpencodePlugin = define({ + id: "opencode", + effect: Effect.fn(function* (ctx) { + const events = yield* EventV2.Service + const http = yield* HttpClient.HttpClient + const loading = Semaphore.makeUnsafe(1) + let connected = false + let providers: typeof ConfigV1.Info.Type.provider | undefined + + const load = Effect.fn("OpencodePlugin.load")(function* () { + const connection = yield* ctx.integration.connection.active("opencode") + const credential = connection + ? yield* ctx.integration.connection.resolve(connection).pipe(Effect.catch(() => Effect.succeed(undefined))) + : undefined + connected = connection !== undefined + providers = credential + ? yield* fetchProviders(http, credential).pipe( + Effect.catch((cause) => + Effect.logWarning("failed to load OpenCode provider config", { cause }).pipe(Effect.as(undefined)), + ), + ) + : undefined + }) + + yield* ctx.integration.transform((draft) => { + draft.update("opencode", (integration) => { + integration.name = "OpenCode" + }) + draft.method.update(oauth(http)) + draft.method.update({ integrationID: "opencode", method: { type: "key", label: "API key (service account)" } }) + }) + + connected = (yield* ctx.integration.connection.active("opencode")) !== undefined + yield* ctx.catalog.transform((catalog) => { + for (const [providerID, item] of Object.entries(providers ?? {})) { + catalog.provider.update(providerID, (provider) => { + provider.integrationID = Integration.ID.make("opencode") + if (item.name !== undefined) provider.name = item.name + provider.api = item.npm + ? { type: "aisdk", package: item.npm, url: item.api } + : { type: "native", url: item.api, settings: {} } + Object.assign(provider.request.headers, item.options?.headers) + Object.assign(provider.request.body, withoutCredentials(item.options)) + }) + + for (const [modelID, config] of Object.entries(item.models ?? {})) { + catalog.model.update(providerID, modelID, (model) => { + if (config.family !== undefined) model.family = config.family + if (config.name !== undefined) model.name = config.name + if (config.id !== undefined) model.api.id = config.id + if (config.provider !== undefined) { + model.api = config.provider.npm + ? { + id: model.api.id, + type: "aisdk", + package: config.provider.npm, + url: config.provider.api, + } + : { id: model.api.id, type: "native", url: config.provider.api, settings: {} } + } + if (config.tool_call !== undefined) model.capabilities.tools = config.tool_call + if (config.modalities?.input !== undefined) model.capabilities.input = [...config.modalities.input] + if (config.modalities?.output !== undefined) model.capabilities.output = [...config.modalities.output] + const packageName = config.provider?.npm ?? item.npm + const lowerer = ConfigProviderOptionsV1.get(packageName) + Object.assign(model.request.headers, config.headers) + Object.assign(model.request.body, lowerer.request(withoutCredentials(config.options))) + if (config.variants !== undefined) { + model.variants = Object.entries(config.variants).map(([id, options]) => ({ + id: ModelV2.VariantID.make(id), + headers: { ...(options.headers ?? {}) }, + body: lowerer.request(withoutCredentials(options)), + })) + } + if (config.release_date !== undefined) { + const released = Date.parse(config.release_date) + model.time.released = Number.isFinite(released) ? released : 0 + } + if (config.cost !== undefined) { + model.cost = remoteCost(config.cost) + } + model.status = config.status ?? "active" + model.enabled = config.status !== "deprecated" + if (config.limit !== undefined) model.limit = { ...config.limit } + }) + } + } + + const item = catalog.provider.get(ProviderV2.ID.opencode) + if (!item) return + const hasKey = Boolean(process.env.OPENCODE_API_KEY || connected || item.provider.request.body.apiKey) + catalog.provider.update(item.provider.id, (provider) => { + if (!hasKey) provider.request.body.apiKey = "public" + }) + if (hasKey) return + for (const model of item.models.values()) { + if (!model.cost.some((cost) => cost.input > 0)) continue + catalog.model.update(item.provider.id, model.id, (draft) => { + draft.enabled = false + }) + } + }) + + const refresh = () => loading.withPermit(load().pipe(Effect.andThen(ctx.catalog.reload()))) + yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe( + Stream.filter((event) => event.data.integrationID === Integration.ID.make("opencode")), + Stream.runForEach(refresh), + Effect.forkScoped({ startImmediately: true }), + ) + yield* refresh().pipe(Effect.forkScoped) + }), +}) + +function fetchProviders(http: HttpClient.HttpClient, value: CredentialValue) { + const metadata = value.metadata + const server = typeof metadata?.server === "string" ? metadata.server : defaultServer + const orgID = typeof metadata?.orgID === "string" ? metadata.orgID : undefined + const token = value.type === "oauth" ? value.access : value.key + return http + .execute( + HttpClientRequest.get(`${server}/api/config`).pipe( + HttpClientRequest.acceptJson, + HttpClientRequest.bearerToken(token), + HttpClientRequest.setHeaders(orgID ? { "x-org-id": orgID } : {}), + ), + ) + .pipe( + Effect.flatMap((response) => { + if (response.status === 404) return Effect.succeed(undefined) + return HttpClientResponse.filterStatusOk(response).pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(RemoteResponse)), + Effect.map((remote) => remote.config.provider), + ) + }), + ) +} + +function withoutCredentials(body: Readonly> | undefined) { + return Object.fromEntries(Object.entries(body ?? {}).filter(([key]) => key !== "apiKey" && key !== "headers")) +} + +function remoteCost(input: NonNullable<(typeof ConfigProviderV1.Model.Type)["cost"]>) { + const base = { + input: input.input, + output: input.output, + cache: { read: input.cache_read ?? 0, write: input.cache_write ?? 0 }, + } + if (!input.context_over_200k) return [base] + return [ + base, + { + tier: { type: "context" as const, size: 200_000 }, + input: input.context_over_200k.input, + output: input.context_over_200k.output, + cache: { + read: input.context_over_200k.cache_read ?? 0, + write: input.context_over_200k.cache_write ?? 0, + }, + }, + ] +} + +function poll(http: HttpClient.HttpClient, server: string, deviceCode: string, interval: Duration.Duration) { + const loop = (wait: Duration.Duration): Effect.Effect => + Effect.gen(function* () { + yield* Effect.sleep(wait) + const result = yield* post( + http, + `${server}/auth/device/token`, + { + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + device_code: deviceCode, + client_id: clientID, + }, + DeviceToken, + false, + ) + if ("access_token" in result) return yield* credential(http, server, result) + if (result.error === "authorization_pending") return yield* loop(wait) + if (result.error === "slow_down") { + return yield* loop(Duration.sum(wait, Duration.seconds(5))) + } + return yield* Effect.fail(new Error(`Device authorization failed: ${result.error}`)) + }) + return loop(interval) +} + +function credential(http: HttpClient.HttpClient, server: string, token: typeof Token.Type) { + return Effect.gen(function* () { + const [user, orgs] = yield* Effect.all( + [ + get(http, `${server}/api/user`, token.access_token, User), + get(http, `${server}/api/orgs`, token.access_token, Schema.Array(Org)), + ], + { concurrency: 2 }, + ) + const org = orgs.toSorted((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id))[0] + return Credential.OAuth.make({ + type: "oauth" as const, + methodID, + access: token.access_token, + refresh: token.refresh_token, + expires: Date.now() + token.expires_in * 1000, + metadata: { + server, + accountID: user.id, + email: user.email, + orgID: org?.id, + orgName: org?.name, + }, + }) + }) +} + +function get(http: HttpClient.HttpClient, url: string, token: string, schema: S) { + return HttpClient.filterStatusOk(http) + .execute(HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson, HttpClientRequest.bearerToken(token))) + .pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(schema))) +} + +function post( + http: HttpClient.HttpClient, + url: string, + body: Record, + schema: S, + statusOk = true, +) { + return HttpClientRequest.post(url).pipe( + HttpClientRequest.acceptJson, + HttpClientRequest.schemaBodyJson(Schema.Record(Schema.String, Schema.String))(body), + Effect.flatMap((request) => http.execute(request)), + Effect.flatMap((response) => (statusOk ? HttpClientResponse.filterStatusOk(response) : Effect.succeed(response))), + Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)), + ) +} diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f295fb0950f1a5ddc6ed1a08af1e9d38b0dea54 --- /dev/null +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -0,0 +1,36 @@ +import { Effect } from "effect" +import { ModelV2 } from "../../model" +import { define } from "../internal" + +export const OpenRouterPlugin = define({ + id: "openrouter", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" + provider.request.headers["X-Title"] = "opencode" + }) + for (const modelID of [ModelV2.ID.make("gpt-5-chat-latest"), ModelV2.ID.make("openai/gpt-5-chat")]) { + if (!item.models.has(modelID)) continue + evt.model.update(item.provider.id, modelID, (model) => { + // These are OpenRouter-specific OpenAI chat aliases that do not work + // on the generic path. Keep custom providers with matching IDs untouched. + model.enabled = false + }) + } + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@openrouter/ai-sdk-provider") return + const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider")) + evt.sdk = mod.createOpenRouter(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/perplexity.ts b/packages/core/src/plugin/provider/perplexity.ts new file mode 100644 index 0000000000000000000000000000000000000000..44c1ef2fc0a16951c82bc75c5c907827316773d6 --- /dev/null +++ b/packages/core/src/plugin/provider/perplexity.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const PerplexityPlugin = define({ + id: "perplexity", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/perplexity") return + const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity")) + evt.sdk = mod.createPerplexity(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/sap-ai-core.ts b/packages/core/src/plugin/provider/sap-ai-core.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c668d8b4147ef913bc3f1fe61748db494ae0a24 --- /dev/null +++ b/packages/core/src/plugin/provider/sap-ai-core.ts @@ -0,0 +1,46 @@ +import { Effect } from "effect" +import { pathToFileURL } from "url" +import { define } from "../internal" +import { Npm } from "../../npm" +import { ProviderV2 } from "../../provider" + +export const SapAICorePlugin = define({ + id: "sap-ai-core", + effect: Effect.fn(function* (ctx) { + const npm = yield* Npm.Service + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return + const serviceKey = + process.env.AICORE_SERVICE_KEY ?? + (typeof evt.options.serviceKey === "string" ? evt.options.serviceKey : undefined) + if (serviceKey && !process.env.AICORE_SERVICE_KEY) process.env.AICORE_SERVICE_KEY = serviceKey + + const installedPath = evt.package.startsWith("file://") + ? evt.package + : (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint + if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) + + const mod = yield* Effect.promise(async () => { + return (await import( + installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href + )) as Record any> + }).pipe(Effect.orDie) + const match = Object.keys(mod).find((name) => name.startsWith("create")) + if (!match) throw new Error(`Package ${evt.package} has no provider factory export`) + + evt.sdk = mod[match]( + serviceKey + ? { deploymentId: process.env.AICORE_DEPLOYMENT_ID, resourceGroup: process.env.AICORE_RESOURCE_GROUP } + : {}, + ) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return + evt.language = evt.sdk(evt.model.api.id) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts new file mode 100644 index 0000000000000000000000000000000000000000..788ac63eb0370f89461a1acecfd018b72729bfa4 --- /dev/null +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -0,0 +1,89 @@ +import { Effect } from "effect" +import { define } from "../internal" +import { ProviderV2 } from "../../provider" + +type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise + +// Exported for testing: intercepts Cortex-specific request/response quirks. +export function cortexFetch(upstream: FetchLike = fetch) { + return async (url: string | URL | Request, init?: RequestInit): Promise => { + if (init?.body && typeof init.body === "string") { + try { + const body = JSON.parse(init.body) + if ("max_tokens" in body) { + body.max_completion_tokens = body.max_tokens + delete body.max_tokens + init = { ...init, body: JSON.stringify(body) } + } + } catch {} + } + + const response = await upstream(url, init) + + // Cortex returns 400 "conversation complete" as a normal stop condition + if (!response.ok && response.status === 400) { + try { + const errorData = (await response.clone().json()) as Record + if ( + String(errorData.message || errorData.error || "") + .toLowerCase() + .includes("conversation complete") + ) { + return new Response( + JSON.stringify({ choices: [{ finish_reason: "stop", message: { content: "", role: "assistant" } }] }), + { status: 200, headers: new Headers({ "content-type": "application/json" }) }, + ) + } + } catch {} + } + + // Cortex returns role:"" in streaming deltas; the AI SDK schema requires "assistant" + if (response.body && response.headers.get("content-type")?.includes("text/event-stream")) { + const reader = response.body.getReader() + const encoder = new TextEncoder() + const decoder = new TextDecoder() + const stream = new ReadableStream({ + async pull(ctrl) { + const { done, value } = await reader.read() + if (done) { + ctrl.close() + return + } + ctrl.enqueue( + encoder.encode(decoder.decode(value, { stream: true }).replace(/"role"\s*:\s*""/g, '"role":"assistant"')), + ) + }, + cancel() { + reader.cancel() + }, + }) + return new Response(stream, { headers: response.headers, status: response.status }) + } + + return response + } +} + +export const SnowflakeCortexPlugin = define({ + id: "snowflake-cortex", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return + const token = + process.env.SNOWFLAKE_CORTEX_TOKEN ?? + process.env.SNOWFLAKE_CORTEX_PAT ?? + (typeof evt.options.token === "string" ? evt.options.token : undefined) ?? + (typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined) + const upstream = typeof evt.options.fetch === "function" ? (evt.options.fetch as FetchLike) : undefined + if (evt.options.includeUsage !== false) evt.options.includeUsage = true + const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) + evt.sdk = mod.createOpenAICompatible({ + ...evt.options, + ...(token ? { apiKey: token } : {}), + fetch: cortexFetch(upstream) as typeof fetch, + } as any) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/togetherai.ts b/packages/core/src/plugin/provider/togetherai.ts new file mode 100644 index 0000000000000000000000000000000000000000..8022e0de668c9db054306ca7bf9770106ad383a1 --- /dev/null +++ b/packages/core/src/plugin/provider/togetherai.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const TogetherAIPlugin = define({ + id: "togetherai", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/togetherai") return + const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai")) + evt.sdk = mod.createTogetherAI(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/venice.ts b/packages/core/src/plugin/provider/venice.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a602ffd50bec8f3a670f997ec38510b829fe994 --- /dev/null +++ b/packages/core/src/plugin/provider/venice.ts @@ -0,0 +1,15 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const VenicePlugin = define({ + id: "venice", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "venice-ai-sdk-provider") return + const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider")) + evt.sdk = mod.createVenice(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/vercel.ts b/packages/core/src/plugin/provider/vercel.ts new file mode 100644 index 0000000000000000000000000000000000000000..00f5601430cdbdb10a2f94335d5ae1333ef6a3af --- /dev/null +++ b/packages/core/src/plugin/provider/vercel.ts @@ -0,0 +1,27 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const VercelPlugin = define({ + id: "vercel", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/vercel") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["http-referer"] = "https://opencode.ai/" + provider.request.headers["x-title"] = "opencode" + }) + } + }), + ) + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/vercel") return + const mod = yield* Effect.promise(() => import("@ai-sdk/vercel")) + evt.sdk = mod.createVercel(evt.options) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/xai.ts b/packages/core/src/plugin/provider/xai.ts new file mode 100644 index 0000000000000000000000000000000000000000..8145a3480a0a382c0c1f061238492bca09635428 --- /dev/null +++ b/packages/core/src/plugin/provider/xai.ts @@ -0,0 +1,22 @@ +import { Effect } from "effect" +import { define } from "../internal" +import { ProviderV2 } from "../../provider" + +export const XAIPlugin = define({ + id: "xai", + effect: Effect.fn(function* (ctx) { + yield* ctx.aisdk.sdk( + Effect.fn(function* (evt) { + if (evt.package !== "@ai-sdk/xai") return + const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) + evt.sdk = mod.createXai(evt.options) + }), + ) + yield* ctx.aisdk.language( + Effect.fn(function* (evt) { + if (evt.model.providerID !== ProviderV2.ID.make("xai")) return + evt.language = evt.sdk.responses(evt.model.api.id) + }), + ) + }), +}) diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts new file mode 100644 index 0000000000000000000000000000000000000000..29adebc0ee5c7f05b297269b9184d689856db429 --- /dev/null +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -0,0 +1,21 @@ +import { Effect } from "effect" +import { define } from "../internal" + +export const ZenmuxPlugin = define({ + id: "zenmux", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform( + Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + if (item.provider.api.url !== "https://zenmux.ai/api/v1") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["HTTP-Referer"] ??= "https://opencode.ai/" + provider.request.headers["X-Title"] ??= "opencode" + }) + } + }), + ) + }), +}) diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea723dd89dea01fce6ebab3d32ec0116552ae4a0 --- /dev/null +++ b/packages/core/src/plugin/skill.ts @@ -0,0 +1,31 @@ +/// + +export * as SkillPlugin from "./skill" + +import { define } from "./internal" +import { Effect } from "effect" +import { AbsolutePath } from "../schema" +import { SkillV2 } from "../skill" +import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" } + +export const CustomizeOpencodeContent = customizeOpencodeContent + +export const Plugin = define({ + id: "skill", + effect: Effect.fn(function* (ctx) { + yield* ctx.skill.transform((draft) => { + draft.source( + SkillV2.EmbeddedSource.make({ + type: "embedded", + skill: SkillV2.Info.make({ + name: "customize-opencode", + description: + "Use ONLY when the user is editing or creating opencode's own configuration: opencode.json, opencode.jsonc, files under .opencode/, or files under ~/.config/opencode/. Also use when creating or fixing opencode agents, subagents, commands, skills, plugins, MCP servers, or permission rules. Do not use for the user's own application code, or for any project that is not configuring opencode itself.", + location: AbsolutePath.make("/builtin/customize-opencode.md"), + content: CustomizeOpencodeContent, + }), + }), + ) + }) + }), +}) diff --git a/packages/core/src/plugin/skill/customize-opencode.md b/packages/core/src/plugin/skill/customize-opencode.md new file mode 100644 index 0000000000000000000000000000000000000000..c02ed72efb746d96c5e7c02994c0b3352b881e2c --- /dev/null +++ b/packages/core/src/plugin/skill/customize-opencode.md @@ -0,0 +1,453 @@ + + +# Customizing opencode + +opencode validates its own config strictly and refuses to start when a field +is wrong. The shapes below cover the common surface area, but they are a +**summary, not the source of truth**. + +## Full schema reference + +The authoritative list of every config option — with field types, enums, +defaults, and descriptions — lives in the published JSON Schema: + +**** + +If a field is not documented in this skill, or you need to confirm an exact +shape before writing config, **fetch that URL and read the schema directly** +rather than guessing. opencode hard-fails on invalid config, so the cost of a +wrong shape is a broken startup. + +Independently, every `opencode.json` should declare +`"$schema": "https://opencode.ai/config.json"` so the user's editor catches +mistakes as they type. + +## Applying changes + +Config is loaded once when opencode starts and is not hot-reloaded. After +saving changes to `opencode.json`, an agent file, a skill, a plugin, or any +other config-time file, **tell the user to quit and restart opencode** for +the changes to take effect. The running session will keep using the +already-loaded config until then. + +## Where files live + +| Scope | Path | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Project config | `./opencode.json`, `./opencode.jsonc`, or `.opencode/opencode.json` (opencode walks up from the cwd to the worktree root) | +| Global config | `~/.config/opencode/opencode.json` or `~/.config/opencode/opencode.jsonc` (NOT `~/.opencode/`) | +| Project agents | `.opencode/agent/.md` or `.opencode/agents/.md` | +| Global agents | `~/.config/opencode/agent(s)/.md` | +| Project commands | `.opencode/command/.md` or `.opencode/commands/.md` | +| Global commands | `~/.config/opencode/command(s)/.md` | +| Project skills | `.opencode/skill(s)//SKILL.md` | +| Global skills | `~/.config/opencode/skill(s)//SKILL.md` | +| External skills (auto-loaded) | `~/.claude/skills//SKILL.md`, `~/.agents/skills//SKILL.md` | + +Configs from each scope are deep-merged. Project overrides global. Unknown +top-level keys in `opencode.json` are rejected with `ConfigInvalidError`. + +## opencode.json + +Every field is optional. + +```json +{ + "$schema": "https://opencode.ai/config.json", + "username": "string", + "model": "provider/model-id", + "small_model": "provider/model-id", + "default_agent": "agent-name", + "shell": "/bin/zsh", + "logLevel": "DEBUG" | "INFO" | "WARN" | "ERROR", + "share": "manual" | "auto" | "disabled", + "autoupdate": true | false | "notify", + "snapshot": true, + "instructions": ["AGENTS.md", "docs/style.md"], + + "skills": { + "paths": [".opencode/skills", "/abs/path/to/skills"], + "urls": ["https://example.com/.well-known/skills/"] + }, + + "references": { + "docs": { + "path": "../docs", + "description": "Use for product behavior and documentation conventions" + }, + "sdk": { + "repository": "owner/sdk", + "branch": "main", + "description": "Use for SDK implementation details", + "hidden": true + } + }, + + "agent": { + "my-agent": { + "model": "anthropic/claude-sonnet-4-6", + "mode": "subagent", + "description": "...", + "permission": { "edit": "deny" } + } + }, + + "command": { + "deploy": { "description": "...", "template": "..." } + }, + + "provider": { + "anthropic": { "options": { "apiKey": "..." } } + }, + "disabled_providers": ["openai"], + "enabled_providers": ["anthropic"], + + "mcp": { + "playwright": { + "type": "local", + "command": ["npx", "-y", "@playwright/mcp"], + "enabled": true, + "environment": {} + }, + "remote-thing": { + "type": "remote", + "url": "https://...", + "headers": { "Authorization": "Bearer ..." } + } + }, + + "plugin": [ + "opencode-gemini-auth", + "opencode-foo@1.2.3", + "./local-plugin.ts", + ["opencode-bar", { "option": "value" }] + ], + + "permission": { + "edit": "deny", + "bash": { "git *": "allow", "*": "ask" } + }, + + "formatter": false, + "lsp": false, + + "experimental": { + "primary_tools": ["edit"], + "mcp_timeout": 30000 + }, + + "tool_output": { "max_lines": 200, "max_bytes": 8192 }, + + "compaction": { "auto": true, "tail_turns": 15 } +} +``` + +Shape notes worth being explicit about: + +- `model` always carries a provider prefix: `"anthropic/claude-sonnet-4-6"`. +- `skills` is an object with `paths` and/or `urls`, not an array. +- `references` is an object keyed by alias. Each value is a local path, Git repository, or string shorthand. +- `agent` is an object keyed by agent name, not an array. +- `command` is an object keyed by command name, not an array. +- `plugin` is an array of strings or `[name, options]` tuples, not an object. +- `mcp[name].command` is an array of strings, never a single string. `type` is required. +- `permission` is either a string action or an object keyed by tool name. + +## Skills + +opencode's skill loader scans for `**/SKILL.md` inside skill directories. The +file is named `SKILL.md` exactly, and lives in its own folder named after the +skill: + +``` +.opencode/skills/my-skill/SKILL.md +``` + +Frontmatter: + +```markdown +--- +name: my-skill +description: One sentence covering what this skill does AND when to trigger it. Front-load the literal keywords or filenames the user is likely to say. +--- + +# My Skill + +(skill body in markdown: instructions, examples, references) +``` + +- `name` is required, lowercase hyphen-separated, up to 64 chars, and matches the folder name. +- `description` is effectively required: skills without one are filtered out and never surfaced to the model. Cover both _what_ the skill does and _when_ to use it. Write in third person ("Use when...", not "I help with..."). Front-load concrete trigger keywords and filenames; gate with "Use ONLY when..." if the skill should stay quiet on adjacent topics. +- Optional: `license`, `compatibility`, `metadata` (string-string map). + +Register skills from non-default locations via `skills.paths` (scanned +recursively for `**/SKILL.md`) and `skills.urls` (each URL serves a list of +skills). + +## References + +References make local directories and Git repositories outside the active +project available as supporting context. Configure them under `references`, +keyed by the alias used in `@` autocomplete: + +```json +{ + "references": { + "docs": { + "path": "../product-docs", + "description": "Use for product behavior and terminology" + }, + "effect": { + "repository": "Effect-TS/effect", + "branch": "main", + "description": "Use for Effect implementation details" + } + } +} +``` + +Local `path` values may be relative to the declaring config, absolute, or use +`~/`. Git `repository` values accept Git URLs, host/path references, and GitHub +`owner/repo` shorthand; `branch` is optional. Both forms support optional +`description` and `hidden` fields. + +- Only references with a `description` are advertised to agents in system context. +- `hidden: true` removes a reference from TUI `@` autocomplete only. It remains available to agents and by direct path. +- Reference directories are automatically allowed through the external-directory boundary; normal read/edit/tool permissions still apply. +- String shorthand is supported: use `"docs": "../docs"` for local paths or `"effect": "Effect-TS/effect"` for Git repositories. + +## Agents + +Two ways to define an agent. Use the file form for anything non-trivial. + +### Inline (in `opencode.json`) + +```json +{ + "agent": { + "my-reviewer": { + "description": "Reviews PRs for style violations.", + "mode": "subagent", + "model": "anthropic/claude-sonnet-4-6", + "permission": { "edit": "deny", "bash": "ask" }, + "prompt": "You are a strict PR reviewer..." + } + } +} +``` + +### File + +``` +.opencode/agent/my-reviewer.md OR .opencode/agents/my-reviewer.md +``` + +```markdown +--- +description: Reviews PRs for style violations. +mode: subagent +model: anthropic/claude-sonnet-4-6 +permission: + edit: deny + bash: ask +--- + +You are a strict PR reviewer. Focus on... +``` + +The file body becomes the agent's `prompt`. Do not also put `prompt:` in the +frontmatter. + +`mode` is one of `"primary"`, `"subagent"`, `"all"`. + +Allowed top-level frontmatter fields: `name, model, variant, description, mode, +hidden, color, steps, options, permission, disable, temperature, top_p`. Any +unknown field is silently routed into `options`. + +To disable a built-in agent: `agent: { build: { disable: true } }`, or in a +file, `disable: true` in frontmatter. + +`default_agent` must point to a non-hidden, primary-mode agent. + +### Built-in agents + +opencode ships with `build`, `plan`, `general`, `explore`. Hidden internal agents: +`compaction`, `title`, `summary`. To override a built-in's fields, define the +same key in `agent: { : { ... } }`. + +## Commands + +opencode's command loader scans for `**/*.md` inside command directories. The +file is named after the command, and lives directly inside the `command` folder: + +``` +.opencode/command/deploy.md +``` + +Frontmatter: + +```markdown +--- +description: One sentence describing what the command does. +agent: build +model: anthropic/claude-sonnet-4-6 +--- + +(command body in markdown: the prompt opencode runs, with $ARGUMENTS for the user's input) +``` + +- `template` is the command body — everything below the frontmatter — and is required: it is the prompt opencode runs when the command is invoked. Do not also put a `template:` key in the frontmatter. +- `$ARGUMENTS` is replaced with everything the user typed after the command; `$1`, `$2`, … pull individual positional arguments. +- Optional: `description`, `agent`, `model`, `variant`, `subtask`. + +## Plugins + +`plugin:` is an array. Each entry is one of: + +```json +"plugin": [ + "opencode-gemini-auth", // npm spec, latest + "opencode-foo@1.2.3", // npm spec, pinned + "./local-plugin.ts", // file path, relative to the declaring config + "file:///abs/path/plugin.js", // file URL + ["opencode-bar", { "key": "val" }] // tuple form with options +] +``` + +Auto-discovered plugins (no config entry needed): any `*.ts` or `*.js` file in +`.opencode/plugin/` or `.opencode/plugins/`. + +A plugin module exports `default` (or any named export) of type +`Plugin = (input: PluginInput, options?) => Promise`. The export is a +function, not a plain object literal, and the function returns an object +(return `{}` if there is nothing to register). + +```ts +import type { Plugin } from "@opencode-ai/plugin" + +export default (async ({ client, project, directory, $ }) => { + return { + config: (cfg) => { + // cfg is the live merged config; mutate fields here. + }, + "tool.execute.before": async (input, output) => { + // mutate output.args before the tool runs + }, + } +}) satisfies Plugin +``` + +Hook surface (mutate `output` in place; return `void`): + +- `event(input)`: every bus event +- `config(cfg)`: once on init with the merged config +- `chat.message`, `chat.params`, `chat.headers` +- `tool.execute.before`, `tool.execute.after` +- `tool.definition` +- `command.execute.before` +- `shell.env` +- `permission.ask` +- `experimental.chat.messages.transform`, `experimental.chat.system.transform`, + `experimental.session.compacting`, `experimental.compaction.autocontinue`, + `experimental.text.complete` + +Special object-shaped (not callbacks): `tool: { my_tool: { ... } }`, +`auth: { ... }`, `provider: { ... }`. + +## MCP servers + +`mcp:` is an object keyed by server name. Each server is discriminated by +`type`: + +```json +{ + "mcp": { + "playwright": { + "type": "local", + "command": ["npx", "-y", "@playwright/mcp"], + "enabled": true, + "environment": { "BROWSER": "chromium" } + }, + "github": { + "type": "remote", + "url": "https://...", + "enabled": true, + "headers": { "Authorization": "Bearer {env:GITHUB_TOKEN}" } + }, + "old-server": { "enabled": false } + } +} +``` + +`command` is an array of strings. `environment` sets environment variables for +a local MCP server. `type` is required. Use `enabled: false` to +disable a server inherited from a parent config. String values such as header +tokens support `{env:VAR}` interpolation (and `{file:path}`); the shell-style +`${VAR}` is not substituted. + +## Permissions + +```json +"permission": { + "edit": "deny", + "bash": { "git *": "allow", "rm *": "deny", "*": "ask" }, + "external_directory": { "~/secrets/**": "deny", "*": "allow" } +} +``` + +Actions: `"allow"`, `"ask"`, `"deny"`. + +Per-tool value forms: `"allow"` shorthand (treated as `{"*": "allow"}`), or an +object `{ pattern: action }`. Within an object, **insertion order matters**. +opencode evaluates the LAST matching rule, so put broad rules first and narrow +rules last. + +`permission: "allow"` (a string at the top level) is shorthand for "allow +everything" and is rarely what the user wants. + +Known permission keys: `read, edit, glob, grep, list, bash, task, +external_directory, todowrite, question, webfetch, websearch, lsp, doom_loop, +skill`. Some of these (`todowrite, +question, webfetch, websearch, doom_loop`) only accept a flat +action, not a per-pattern object. + +`external_directory` patterns are filesystem paths (use `~/`, absolute paths, +or globs like `~/projects/**`). + +Per-agent `permission:` overrides top-level `permission:`. Plan Mode lives on +the `plan` agent's permission ruleset (`edit: deny *`). + +## Escape hatches + +When a user's config is broken and opencode won't start, these env vars help: + +- `OPENCODE_DISABLE_PROJECT_CONFIG=1`: skip the project's local `opencode.json` + and start from globals only. Run from the project directory, opencode loads, + the user edits the broken file, then they restart without the flag. +- `OPENCODE_CONFIG=/path/to/file.json`: load an additional explicit config. +- `OPENCODE_CONFIG_CONTENT='{"$schema":"https://opencode.ai/config.json"}'`: + inject inline JSON as a final local-scope merge. +- `OPENCODE_DISABLE_DEFAULT_PLUGINS=1`: skip default plugins. +- `OPENCODE_PURE=1`: skip external plugins entirely. +- `OPENCODE_DISABLE_EXTERNAL_SKILLS=1`, + `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1`: skip the external skill scans under + `~/.claude/` and `~/.agents/`. + +## When proposing edits + +- Validate against the schema before writing. If you are unsure of a field's + exact shape, or the field is not covered in this skill, fetch + `https://opencode.ai/config.json` and read the schema rather than guessing. +- Preserve `$schema` and any existing fields the user did not ask to change. +- For agent, command, skill, and plugin definitions, prefer creating new files + in the correct location over inlining everything in `opencode.json`. +- If the user's existing config is malformed, point them at the env-var escape + hatches above so they can edit from inside opencode without breaking their + session. +- After saving any config change, remind the user to quit and restart opencode + — running sessions keep using the already-loaded config. diff --git a/packages/core/src/plugin/variant.ts b/packages/core/src/plugin/variant.ts new file mode 100644 index 0000000000000000000000000000000000000000..05e1653b6e530814e1f887439bd1d957b9064f93 --- /dev/null +++ b/packages/core/src/plugin/variant.ts @@ -0,0 +1,39 @@ +export * as VariantPlugin from "./variant" + +import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" +import { Effect } from "effect" +import { define } from "./internal" + +export const Plugin = define({ + id: "variant", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform((catalog) => { + for (const record of catalog.provider.list()) { + for (const model of record.models.values()) { + catalog.model.update(model.providerID, model.id, (draft) => { + const generated = generate(draft) + if (generated.length === 0) return + + const explicit = new Map(draft.variants.map((variant) => [variant.id, variant])) + const generatedIDs = new Set(generated.map((variant) => variant.id)) + draft.variants = [ + ...generated.map((variant) => explicit.get(variant.id) ?? variant), + ...draft.variants.filter((variant) => !generatedIDs.has(variant.id)), + ] + }) + } + } + }) + }), +}) + +export function generate(model: ModelV2Info): ModelV2Info["variants"] { + if (model.api.type !== "aisdk" || model.api.package !== "@ai-sdk/openai-compatible") return [] + const ids = `${model.id} ${model.api.id}`.toLowerCase() + if (!["glm-5.2", "glm-5-2", "glm-5p2"].some((name) => ids.includes(name))) return [] + return ["high", "max"].map((id) => ({ + id, + headers: {}, + body: { reasoning_effort: id }, + })) +} diff --git a/packages/core/src/policy.ts b/packages/core/src/policy.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2adebb54e4e90706de5cfee99d286537b3865e8 --- /dev/null +++ b/packages/core/src/policy.ts @@ -0,0 +1,49 @@ +export * as Policy from "./policy" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect as EffectRuntime, Layer, Schema } from "effect" +import { Wildcard } from "./util/wildcard" +import { Location } from "./location" + +export const Effect = Schema.Literals(["allow", "deny"]).annotate({ identifier: "Policy.Effect" }) +export type Effect = typeof Effect.Type + +export class Info extends Schema.Class("Policy.Info")({ + action: Schema.String, + effect: Effect, + resource: Schema.String, +}) {} + +export interface Interface { + readonly load: (statements: Info[]) => EffectRuntime.Effect + readonly evaluate: (action: string, resource: string, fallback: Effect) => EffectRuntime.Effect + readonly hasStatements: () => boolean +} + +export class Service extends Context.Service()("@opencode/v2/Policy") {} + +const layer = Layer.effect( + Service, + EffectRuntime.gen(function* () { + let statements: Info[] = [] + yield* Location.Service + + return Service.of({ + load: EffectRuntime.fn("Policy.load")(function* (input) { + statements = input + }), + hasStatements: () => statements.length > 0, + evaluate: EffectRuntime.fn("Policy.evaluate")(function* (action, resource, fallback) { + return ( + statements.findLast( + (statement) => Wildcard.match(action, statement.action) && Wildcard.match(resource, statement.resource), + )?.effect ?? fallback + ) + }), + }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [Location.node] }) diff --git a/packages/core/src/process.ts b/packages/core/src/process.ts new file mode 100644 index 0000000000000000000000000000000000000000..16e9118d1f59dfa1480008060b8392082044061d --- /dev/null +++ b/packages/core/src/process.ts @@ -0,0 +1,261 @@ +import { Context, Duration, Effect, Fiber, Layer, Schema, Stream } from "effect" +import type { PlatformError } from "effect/PlatformError" +import { ChildProcess } from "effect/unstable/process" +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { CrossSpawnSpawner } from "./cross-spawn-spawner" +import { makeGlobalNode } from "./effect/app-node" + +export class AppProcessError extends Schema.TaggedErrorClass()("AppProcessError", { + command: Schema.String, + exitCode: Schema.optional(Schema.Number), + stderr: Schema.optional(Schema.String), + cause: Schema.optional(Schema.Defect()), +}) { + override get message() { + const detail = + this.stderr?.trim() || (this.cause instanceof Error ? this.cause.message : this.cause && String(this.cause)) + const status = this.exitCode === undefined ? "" : ` (exit ${this.exitCode})` + return `Command failed${status}: ${this.command}${detail ? `: ${detail}` : ""}` + } +} + +export interface RunOptions { + readonly combineOutput?: boolean + readonly maxOutputBytes?: number + readonly maxErrorBytes?: number + readonly signal?: AbortSignal + readonly timeout?: Duration.Input + readonly stdin?: string | Uint8Array | Stream.Stream +} + +export interface RunStreamOptions { + readonly signal?: AbortSignal + readonly includeStderr?: boolean + readonly okExitCodes?: ReadonlyArray + readonly maxErrorBytes?: number +} + +export interface RunResult { + readonly command: string + readonly exitCode: number + readonly output?: Buffer + readonly stdout: Buffer + readonly stderr: Buffer + readonly outputTruncated?: boolean + readonly stdoutTruncated: boolean + readonly stderrTruncated: boolean +} + +export type Interface = ChildProcessSpawner["Service"] & { + readonly run: (command: ChildProcess.Command, options?: RunOptions) => Effect.Effect + readonly runStream: ( + command: ChildProcess.Command, + options?: RunStreamOptions, + ) => Stream.Stream +} + +export class Service extends Context.Service()("@opencode/AppProcess") {} + +export const requireSuccess = (result: RunResult): Effect.Effect => + result.exitCode === 0 + ? Effect.succeed(result) + : Effect.fail( + new AppProcessError({ + command: result.command, + exitCode: result.exitCode, + stderr: result.stderr.toString("utf8"), + }), + ) + +export const requireExitIn = + (codes: ReadonlyArray) => + (result: RunResult): Effect.Effect => + codes.includes(result.exitCode) + ? Effect.succeed(result) + : Effect.fail( + new AppProcessError({ + command: result.command, + exitCode: result.exitCode, + stderr: result.stderr.toString("utf8"), + }), + ) + +const describeCommand = (command: ChildProcess.Command): string => { + if (command._tag === "StandardCommand") { + return command.args.length ? `${command.command} ${command.args.join(" ")}` : command.command + } + return `${describeCommand(command.left)} | ${describeCommand(command.right)}` +} + +const wrapError = (description: string, cause: unknown): AppProcessError => + cause instanceof AppProcessError ? cause : new AppProcessError({ command: description, cause }) + +export const abortError = (signal: AbortSignal): Error => { + const reason = signal.reason + if (reason instanceof Error) return reason + const err = new Error("Aborted") + err.name = "AbortError" + return err +} + +export const waitForAbort = (signal: AbortSignal) => + Effect.callback((resume) => { + if (signal.aborted) { + resume(Effect.fail(abortError(signal))) + return + } + const onabort = () => resume(Effect.fail(abortError(signal))) + signal.addEventListener("abort", onabort, { once: true }) + return Effect.sync(() => signal.removeEventListener("abort", onabort)) + }) + +const normalizeStdin = ( + input: string | Uint8Array | Stream.Stream, +): Stream.Stream => + typeof input === "string" + ? Stream.make(new TextEncoder().encode(input)) + : input instanceof Uint8Array + ? Stream.make(input) + : input + +export const collectStream = (stream: Stream.Stream, maxOutputBytes: number | undefined) => + Stream.runFold( + stream, + () => ({ chunks: [] as Uint8Array[], bytes: 0, truncated: false }), + (acc, chunk) => { + if (maxOutputBytes === undefined) { + acc.chunks.push(chunk) + acc.bytes += chunk.length + return acc + } + const remaining = maxOutputBytes - acc.bytes + if (remaining > 0) acc.chunks.push(remaining >= chunk.length ? chunk : chunk.slice(0, remaining)) + acc.bytes += chunk.length + acc.truncated = acc.truncated || acc.bytes > maxOutputBytes + return acc + }, + ).pipe(Effect.map((x) => ({ buffer: Buffer.concat(x.chunks), truncated: x.truncated }))) + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const spawner = yield* ChildProcessSpawner + + const runCommand = (command: ChildProcess.Command, options?: RunOptions) => { + const description = describeCommand(command) + const collect = Effect.scoped( + Effect.gen(function* () { + const handle = yield* spawner.spawn(command) + if (options?.combineOutput) { + const [output, exitCode] = yield* Effect.all( + [collectStream(handle.all, options.maxOutputBytes), handle.exitCode], + { concurrency: "unbounded" }, + ) + return { + command: description, + exitCode, + output: output.buffer, + stdout: Buffer.alloc(0), + stderr: Buffer.alloc(0), + outputTruncated: output.truncated, + stdoutTruncated: false, + stderrTruncated: false, + } satisfies RunResult + } + const [stdout, stderr, exitCode] = yield* Effect.all( + [ + collectStream(handle.stdout, options?.maxOutputBytes), + collectStream(handle.stderr, options?.maxErrorBytes), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ) + return { + command: description, + exitCode, + stdout: stdout.buffer, + stderr: stderr.buffer, + stdoutTruncated: stdout.truncated, + stderrTruncated: stderr.truncated, + } satisfies RunResult + }), + ) + const timed = options?.timeout + ? Effect.timeoutOrElse(collect, { + duration: options.timeout, + orElse: () => Effect.fail(new AppProcessError({ command: description, cause: new Error("Timed out") })), + }) + : collect + const aborted = options?.signal + ? timed.pipe( + Effect.raceFirst( + waitForAbort(options.signal).pipe(Effect.mapError((cause) => wrapError(description, cause))), + ), + ) + : timed + return aborted.pipe(Effect.catch((cause) => Effect.fail(wrapError(description, cause)))) + } + + const run = Effect.fn("AppProcess.run")(function* (command: ChildProcess.Command, options?: RunOptions) { + if (options?.stdin === undefined) return yield* runCommand(command, options) + if (command._tag !== "StandardCommand") { + return yield* new AppProcessError({ + command: describeCommand(command), + cause: new Error("stdin option only supports StandardCommand; received PipedCommand"), + }) + } + const next = ChildProcess.make(command.command, command.args, { + ...command.options, + stdin: normalizeStdin(options.stdin), + }) + return yield* runCommand(next, options) + }) + + const runStream = ( + command: ChildProcess.Command, + options?: RunStreamOptions, + ): Stream.Stream => { + const description = describeCommand(command) + const okExitCodes = options?.okExitCodes + const built: Stream.Stream = Stream.unwrap( + Effect.gen(function* () { + const handle = yield* spawner.spawn(command) + const stderrFiber = yield* Effect.forkScoped( + collectStream(handle.stderr, options?.maxErrorBytes).pipe(Effect.map((x) => x.buffer.toString("utf8"))), + ) + const source = options?.includeStderr === true ? handle.all : handle.stdout + const lines = source.pipe( + Stream.decodeText, + Stream.splitLines, + Stream.filter((line) => line.length > 0), + ) + const tail = Stream.unwrap( + Effect.gen(function* () { + const code = yield* handle.exitCode + if (okExitCodes && okExitCodes.length > 0 && !okExitCodes.includes(code)) { + const stderr = yield* Fiber.join(stderrFiber) + return Stream.fail(new AppProcessError({ command: description, exitCode: code, stderr })) + } + return Stream.empty + }), + ) + return Stream.concat(lines, tail) as Stream.Stream + }), + ) + const mapped = built.pipe( + Stream.catch((cause): Stream.Stream => Stream.fail(wrapError(description, cause))), + ) + if (!options?.signal) return mapped + const signal = options.signal + return mapped.pipe( + Stream.interruptWhen(waitForAbort(signal).pipe(Effect.mapError((cause) => wrapError(description, cause)))), + ) + } + + return Service.of({ ...spawner, run, runStream }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [CrossSpawnSpawner.node] }) + +export * as AppProcess from "./process" diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts new file mode 100644 index 0000000000000000000000000000000000000000..49e054d7cc9ae2c3229669e949089f3174242ecd --- /dev/null +++ b/packages/core/src/project.ts @@ -0,0 +1,136 @@ +export * as ProjectV2 from "./project" +export * as Project from "./project" + +import { Context, Effect, Layer, Schema } from "effect" +import path from "path" +import { AbsolutePath } from "./schema" +import { FSUtil } from "./fs-util" +import { Git } from "./git" +import { makeGlobalNode } from "./effect/app-node" +import { Hash } from "./util/hash" +import { ProjectDirectories } from "./project/directories" +import { ProjectSchema } from "./project/schema" + +export const ID = ProjectSchema.ID +export type ID = ProjectSchema.ID + +export const Vcs = ProjectSchema.Vcs +export type Vcs = ProjectSchema.Vcs + +export class Info extends Schema.Class("Project.Info")({ + id: ID, +}) {} + +export const DirectoriesInput = ProjectDirectories.ListInput +export type DirectoriesInput = typeof DirectoriesInput.Type + +export const Directories = ProjectDirectories.ListOutput +export type Directories = typeof Directories.Type + +export interface Resolved { + readonly previous?: ID + readonly id: ID + readonly directory: AbsolutePath + readonly vcs?: Vcs +} + +export interface Interface { + readonly directories: (input: DirectoriesInput) => Effect.Effect + readonly resolve: (input: AbsolutePath) => Effect.Effect + /** + * Temporary bridge method for writing the resolved project ID to the repo-local cache. + * + * This exists while the old opencode project service and this core project + * service work together: core resolves the ID, while the old service still owns + * database migration and persistence. The old service should call this after it + * finishes migrating from `resolve().previous` to `resolve().id`; once project + * persistence moves into core, this separate bridge method can go away. + */ + readonly commit: (input: { store: AbsolutePath; id: ID }) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ProjectV2") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const git = yield* Git.Service + const projectDirectories = yield* ProjectDirectories.Service + + const directories = Effect.fn("Project.directories")(function* (input: DirectoriesInput) { + return yield* projectDirectories.list(input.projectID) + }) + + const cached = Effect.fnUntraced(function* (dir: string) { + return yield* fs.readFileString(path.join(dir, "opencode")).pipe( + Effect.map((value) => value.trim()), + Effect.map((value) => (value ? ID.make(value) : undefined)), + Effect.catch(() => Effect.succeed(undefined)), + ) + }) + + const remote = Effect.fnUntraced(function* (repo: Git.Repository) { + const origin = yield* git.remote.get(repo) + if (!origin) return undefined + const normalized = url(origin) + if (!normalized) return undefined + return ID.make(Hash.fast(`git-remote:${normalized}`)) + }) + + function url(input: string) { + const value = input.trim() + if (!value) return undefined + + try { + const parsed = new URL(value) + if (parsed.protocol === "file:") return undefined + return parts(parsed.hostname, parsed.pathname) + } catch { + const scp = value.match(/^([^@/:]+@)?([^/:]+):(.+)$/) + if (scp) return parts(scp[2], scp[3]) + return undefined + } + } + + function parts(host: string, name: string) { + const pathname = name + .replace(/^\/+/, "") + .replace(/\.git\/?$/, "") + .replace(/\/+$/, "") + if (!host || !pathname) return undefined + return `${host.toLowerCase()}/${pathname}` + } + + const root = Effect.fnUntraced(function* (repo: Git.Repository) { + const root = (yield* git.history.rootCommits(repo))[0] + return root ? ID.make(root) : undefined + }) + + const resolve = Effect.fn("Project.resolve")(function* (input: AbsolutePath) { + const repo = yield* git.repo.discover(input) + if (!repo) return { id: ID.global, directory: AbsolutePath.make(path.parse(input).root), vcs: undefined } + + const previous = yield* cached(repo.commonDirectory) + const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo)) + return { + previous, + id: id ?? ID.global, + directory: repo.worktree, + vcs: { type: "git" as const, store: repo.commonDirectory }, + } + }) + + const commit = Effect.fn("Project.commit")(function* (input: { store: AbsolutePath; id: ID }) { + yield* fs.writeFileString(path.join(input.store, "opencode"), input.id).pipe(Effect.ignore) + }) + + return Service.of({ directories, resolve, commit }) + }), +) + +export const node = makeGlobalNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, Git.node, ProjectDirectories.node], +}) diff --git a/packages/core/src/project/copy-strategies.ts b/packages/core/src/project/copy-strategies.ts new file mode 100644 index 0000000000000000000000000000000000000000..59466b9901ae58098bf892dde8bf4bb69a970072 --- /dev/null +++ b/packages/core/src/project/copy-strategies.ts @@ -0,0 +1,35 @@ +import { Effect } from "effect" +import { AbsolutePath } from "../schema" +import { Git } from "../git" +import { DirectoryUnavailableError, StrategyID, type ListEntry, type Strategy } from "./copy" + +export function makeGitWorktreeStrategy(input: { + git: Git.Interface + canonical: (directory: AbsolutePath) => Effect.Effect +}) { + return { + id: StrategyID.make("git_worktree"), + create: Effect.fn("ProjectCopy.GitWorktree.create")(function* (options) { + const repository = yield* input.git.repo.discover(options.sourceDirectory) + if (!repository) return yield* new DirectoryUnavailableError({ directory: options.sourceDirectory }) + yield* input.git.worktree.create({ repository, directory: options.directory }) + return { directory: yield* input.canonical(options.directory) } + }), + remove: Effect.fn("ProjectCopy.GitWorktree.remove")(function* (options) { + const found = yield* input.git.repo.discover(options.directory) + if (!found) return yield* new DirectoryUnavailableError({ directory: options.directory }) + yield* input.git.worktree.remove({ repository: found, directory: options.directory, force: options.force }) + }), + list: Effect.fn("ProjectCopy.GitWorktree.list")(function* (directory) { + const found = yield* input.git.repo.discover(directory) + if (!found) return yield* new DirectoryUnavailableError({ directory }) + const entries = yield* input.git.worktree.list(found) + return yield* Effect.forEach(entries, (entry) => + input.canonical(entry.directory).pipe( + Effect.map((directory) => ({ directory, type: entry.kind === "main" ? "root" : "copy" }) as const), + Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed(undefined)), + ), + ).pipe(Effect.map((items) => items.filter((item): item is ListEntry => item !== undefined))) + }), + } satisfies Strategy +} diff --git a/packages/core/src/project/copy.ts b/packages/core/src/project/copy.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1a5cc84dbb3f104d989b37e19cdd27f1ecdb91c --- /dev/null +++ b/packages/core/src/project/copy.ts @@ -0,0 +1,292 @@ +export * as ProjectCopy from "./copy" + +import { Context, Effect, Layer, Schema } from "effect" +import path from "path" +import { AbsolutePath } from "../schema" +import { FSUtil } from "../fs-util" +import { Git } from "../git" +import { makeLocationNode } from "../effect/app-node" +import { Project } from "../project" +import { ProjectDirectories } from "./directories" +import { makeGitWorktreeStrategy } from "./copy-strategies" +import { Slug } from "../util/slug" +import { EventV2 } from "../event" +import { Database } from "../database/database" +import { Location } from "../location" +import { Event } from "@opencode-ai/schema/project-directories" +import { ProjectCopy } from "@opencode-ai/schema/project-copy" + +export const StrategyID = ProjectCopy.StrategyID +export type StrategyID = typeof StrategyID.Type + +export const CreateInput = ProjectCopy.CreateInput +export type CreateInput = typeof CreateInput.Type + +export const RemoveInput = ProjectCopy.RemoveInput +export type RemoveInput = typeof RemoveInput.Type + +export const RefreshInput = Schema.Struct({ + projectID: Project.ID, +}).annotate({ identifier: "ProjectCopy.RefreshInput" }) +export type RefreshInput = typeof RefreshInput.Type + +export const RefreshResult = Schema.Struct({ + updated: Schema.Array(AbsolutePath), + removed: Schema.Array(AbsolutePath), +}).annotate({ identifier: "ProjectCopy.RefreshResult" }) +export type RefreshResult = typeof RefreshResult.Type + +export const Copy = ProjectCopy.Copy +export type Copy = typeof Copy.Type + +export const ListEntry = Schema.Struct({ + directory: AbsolutePath, + type: Schema.Literals(["root", "copy"]), +}).annotate({ identifier: "ProjectCopy.ListEntry" }) +export type ListEntry = typeof ListEntry.Type + +export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass()( + "ProjectCopy.SourceDirectoryNotFoundError", + { directory: AbsolutePath }, +) {} + +export class DestinationExistsError extends Schema.TaggedErrorClass()( + "ProjectCopy.DestinationExistsError", + { directory: AbsolutePath }, +) {} + +export class DirectoryUnavailableError extends Schema.TaggedErrorClass()( + "ProjectCopy.DirectoryUnavailableError", + { directory: AbsolutePath }, +) {} + +export class InvalidDirectoryError extends Schema.TaggedErrorClass()( + "ProjectCopy.InvalidDirectoryError", + { directory: AbsolutePath }, +) {} + +export class StrategyUnavailableError extends Schema.TaggedErrorClass()( + "ProjectCopy.StrategyUnavailableError", + { strategy: StrategyID }, +) {} + +export class DuplicateStrategyError extends Schema.TaggedErrorClass()( + "ProjectCopy.DuplicateStrategyError", + { strategy: StrategyID }, +) {} + +export type Error = + | SourceDirectoryNotFoundError + | DestinationExistsError + | DirectoryUnavailableError + | InvalidDirectoryError + | StrategyUnavailableError + | Git.WorktreeError + +export interface Strategy { + readonly id: StrategyID + readonly create: (input: { + sourceDirectory: AbsolutePath + directory: AbsolutePath + }) => Effect.Effect + readonly remove: (input: { + directory: AbsolutePath + force: boolean + }) => Effect.Effect + readonly list: (directory: AbsolutePath) => Effect.Effect +} + +export { Event } + +export interface Interface { + readonly register: (strategy: Strategy) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly remove: (input: RemoveInput) => Effect.Effect + readonly refresh: (input: RefreshInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ProjectCopy") {} + +export const refreshAfterBoot = Effect.gen(function* () { + const location = yield* Location.Service + const copies = yield* Service + yield* Effect.gen(function* () { + yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id }) + const result = yield* copies.refresh({ projectID: location.project.id }) + yield* Effect.logInfo("project copy refresh done", { + projectID: location.project.id, + updated: result.updated, + removed: result.removed, + }) + }).pipe( + Effect.catchCause((cause) => Effect.logWarning("project copy refresh failed", { cause })), + Effect.forkScoped, + Effect.asVoid, + ) +}) + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const git = yield* Git.Service + const directories = yield* ProjectDirectories.Service + const db = (yield* Database.Service).db + const events = yield* EventV2.Service + + const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) { + if (update) yield* events.publish(Event.Updated, { projectID }) + }) + + const canonical = Effect.fnUntraced(function* (input: AbsolutePath) { + const resolved = AbsolutePath.make(yield* fs.resolve(input)) + if (!(yield* fs.isDir(resolved))) return yield* new DirectoryUnavailableError({ directory: input }) + return resolved + }) + + const registry = new Map() + + const register = Effect.fn("ProjectCopy.register")(function* (strategy: Strategy) { + if (registry.has(strategy.id)) return yield* new DuplicateStrategyError({ strategy: strategy.id }) + registry.set(strategy.id, strategy) + }) + + // Register default strategies + yield* register(makeGitWorktreeStrategy({ git, canonical })).pipe(Effect.orDie) + + const strategies = () => Array.from(registry.values()) + + const source = Effect.fnUntraced(function* (input: AbsolutePath, projectID: Project.ID) { + const sourceDirectory = yield* canonical(input) + if (!(yield* directories.contains({ projectID, directory: sourceDirectory }))) + return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory }) + return sourceDirectory + }) + + const getStrategy = Effect.fnUntraced(function* (id: StrategyID) { + const found = registry.get(id) + if (!found) return yield* new StrategyUnavailableError({ strategy: id }) + return found + }) + + const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) { + const selected = yield* getStrategy(input.strategy) + const sourceDirectory = yield* source(input.sourceDirectory, input.projectID) + yield* fs.makeDirectory(input.directory, { recursive: true }).pipe(Effect.orDie) + const name = input.name ?? Slug.create() + let suffix = 1 + let copyDirectory = AbsolutePath.make(path.join(input.directory, name)) + while (yield* fs.existsSafe(copyDirectory)) { + suffix++ + if (suffix > 10) return yield* new DestinationExistsError({ directory: copyDirectory }) + copyDirectory = AbsolutePath.make(path.join(input.directory, `${name}-${suffix}`)) + } + + const result = yield* selected.create({ + directory: copyDirectory, + sourceDirectory, + }) + yield* changed( + input.projectID, + yield* directories.create({ + projectID: input.projectID, + directory: result.directory, + strategy: input.strategy, + behavior: "replace", + }), + ) + return result + }) + + const remove = Effect.fn("ProjectCopy.remove")(function* (input: RemoveInput) { + const copyDirectory = yield* canonical(input.directory) + const stored = yield* directories.get({ projectID: input.projectID, directory: copyDirectory }) + if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: copyDirectory }) + yield* (yield* getStrategy(StrategyID.make(stored.strategy))).remove({ + directory: copyDirectory, + force: input.force, + }) + yield* changed( + input.projectID, + yield* directories.remove({ projectID: input.projectID, directory: copyDirectory }), + ) + }) + + const refresh = Effect.fn("ProjectCopy.refresh")(function* (input: RefreshInput) { + const stored = yield* directories.list(input.projectID) + const checked = yield* Effect.forEach( + stored, + (item) => fs.isDir(item.directory).pipe(Effect.map((exists) => ({ ...item, exists }))), + { concurrency: "unbounded" }, + ) + const sourceDirectories = checked + .filter((item) => item.strategy === undefined && item.exists) + .map((item) => item.directory) + const discovered = yield* Effect.forEach( + sourceDirectories, + (sourceDirectory) => + Effect.forEach(strategies(), (strategy) => + strategy.list(sourceDirectory).pipe( + Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed([])), + Effect.map((items) => + items.map((item) => ({ + directory: item.directory, + strategy: item.type === "copy" ? strategy.id : undefined, + })), + ), + ), + ), + { concurrency: "unbounded" }, + ).pipe( + Effect.map((sets) => new Map(sets.flat(2).map((item) => [item.directory, item] as const)).values().toArray()), + ) + const removed = checked.filter((item) => !item.exists).map((item) => item.directory) + const result = yield* db + .transaction((tx) => + Effect.all({ + updated: Effect.forEach(discovered, (item) => + directories.create( + { + projectID: input.projectID, + directory: item.directory, + strategy: item.strategy, + behavior: "replace", + }, + tx, + ), + ), + removed: Effect.forEach(removed, (directory) => + directories.remove({ projectID: input.projectID, directory }, tx), + ), + }), + ) + .pipe(Effect.orDie) + const changes = { + updated: discovered.filter((_, index) => result.updated[index]).map((item) => item.directory), + removed: removed.filter((_, index) => result.removed[index]), + } + yield* changed(input.projectID, changes.updated.length > 0 || changes.removed.length > 0) + return changes + }) + + return Service.of({ + register, + create, + remove, + refresh, + }) + }), +) + +export const locationLayer = layer +export const node = makeLocationNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node], +}) + +export const refreshNode = makeLocationNode({ + name: "project-copy-refresh", + layer: Layer.effectDiscard(refreshAfterBoot), + deps: [node, Location.node], +}) diff --git a/packages/core/src/project/directories.ts b/packages/core/src/project/directories.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c9ad2e51594e0ca0bed7833e4cb333feed496ae --- /dev/null +++ b/packages/core/src/project/directories.ts @@ -0,0 +1,158 @@ +export * as ProjectDirectories from "./directories" + +import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Database } from "../database/database" +import { makeGlobalNode } from "../effect/app-node" +import { AbsolutePath, optional } from "../schema" +import { ProjectSchema } from "./schema" +import { ProjectDirectoryTable } from "./sql" +import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" + +export interface Directory { + readonly directory: AbsolutePath + readonly strategy?: string +} + +export const CreateInput = Schema.Struct({ + projectID: ProjectSchema.ID, + directory: AbsolutePath, + strategy: Schema.optional(Schema.String), + behavior: Schema.Literals(["ignore", "replace"]).pipe(Schema.optional), +}) +export type CreateInput = typeof CreateInput.Type + +export const RemoveInput = Schema.Struct({ + projectID: ProjectSchema.ID, + directory: AbsolutePath, +}) +export type RemoveInput = typeof RemoveInput.Type + +type DatabaseClient = EffectDrizzleSqlite.EffectSQLiteDatabase +export type Transaction = Parameters[0]>[0] + +export const ListInput = Schema.Struct({ + projectID: ProjectSchema.ID, +}).annotate({ identifier: "Project.DirectoriesInput" }) +export type ListInput = typeof ListInput.Type + +export const ListOutput = Schema.Array( + Schema.Struct({ + directory: AbsolutePath, + strategy: optional(Schema.String), + }), +).annotate({ identifier: "Project.Directories" }) +export type ListOutput = typeof ListOutput.Type + +export interface Interface { + readonly list: (projectID: ProjectSchema.ID) => Effect.Effect> + readonly get: (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) => Effect.Effect + readonly contains: (input: { projectID: ProjectSchema.ID; directory: AbsolutePath }) => Effect.Effect + readonly create: (input: CreateInput, tx?: Transaction) => Effect.Effect + readonly remove: (input: RemoveInput, tx?: Transaction) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ProjectDirectories") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const db = (yield* Database.Service).db + + const create = Effect.fn("ProjectDirectories.create")(function* (input: CreateInput, tx?: Transaction) { + const insert = (tx ?? db) + .insert(ProjectDirectoryTable) + .values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy }) + const query = + input.behavior === "replace" + ? insert.onConflictDoUpdate({ + target: [ProjectDirectoryTable.project_id, ProjectDirectoryTable.directory], + set: { strategy: input.strategy ?? null }, + setWhere: input.strategy + ? or(isNull(ProjectDirectoryTable.strategy), ne(ProjectDirectoryTable.strategy, input.strategy)) + : isNotNull(ProjectDirectoryTable.strategy), + }) + : insert.onConflictDoNothing() + return ( + (yield* query.returning({ directory: ProjectDirectoryTable.directory }).get().pipe(Effect.orDie)) !== undefined + ) + }) + + const remove = Effect.fn("ProjectDirectories.remove")(function* (input: RemoveInput, tx?: Transaction) { + return ( + (yield* (tx ?? db) + .delete(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .returning({ directory: ProjectDirectoryTable.directory }) + .get() + .pipe(Effect.orDie)) !== undefined + ) + }) + + const list = Effect.fn("ProjectDirectories.list")(function* (projectID: ProjectSchema.ID) { + const rows = yield* db + .select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy }) + .from(ProjectDirectoryTable) + .where(eq(ProjectDirectoryTable.project_id, projectID)) + .orderBy(desc(ProjectDirectoryTable.time_created), asc(ProjectDirectoryTable.directory)) + .all() + .pipe(Effect.orDie) + return rows.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined })) + }) + + const contains = Effect.fn("ProjectDirectories.contains")(function* (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) { + return ( + (yield* db + .select({ directory: ProjectDirectoryTable.directory }) + .from(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .get() + .pipe(Effect.orDie)) !== undefined + ) + }) + + const get = Effect.fn("ProjectDirectories.get")(function* (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) { + const row = yield* db + .select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy }) + .from(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .get() + .pipe(Effect.orDie) + return row ? { directory: row.directory, strategy: row.strategy ?? undefined } : undefined + }) + + return Service.of({ + list, + get, + contains, + create, + remove, + }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] }) diff --git a/packages/core/src/project/schema.ts b/packages/core/src/project/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..eed359abad2ae1d5ff4be0219adb8629d818afb8 --- /dev/null +++ b/packages/core/src/project/schema.ts @@ -0,0 +1,16 @@ +export * as ProjectSchema from "./schema" + +import { Schema } from "effect" +import { Project } from "@opencode-ai/schema/project" +import { AbsolutePath } from "../schema" + +export const ID = Project.ID +export type ID = typeof ID.Type + +export const Vcs = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("git"), + store: AbsolutePath, + }), +]) +export type Vcs = typeof Vcs.Type diff --git a/packages/core/src/project/sql.ts b/packages/core/src/project/sql.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab05fdac4a1adb971ec18ecb53c80bd8859be188 --- /dev/null +++ b/packages/core/src/project/sql.ts @@ -0,0 +1,35 @@ +import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" +import * as DatabasePath from "../database/path" +import { Timestamps } from "../database/schema.sql" +import { ProjectSchema } from "./schema" + +export const ProjectTable = sqliteTable("project", { + id: text().$type().primaryKey(), + worktree: DatabasePath.absoluteColumn().notNull(), + vcs: text(), + name: text(), + icon_url: text(), + icon_url_override: text(), + icon_color: text(), + ...Timestamps, + time_initialized: integer(), + sandboxes: DatabasePath.absoluteArrayColumn().notNull(), + commands: text({ mode: "json" }).$type<{ start?: string }>(), +}) + +export const ProjectDirectoryTable = sqliteTable( + "project_directory", + { + project_id: text() + .$type() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + directory: DatabasePath.absoluteColumn().notNull(), + type: text().$type<"main" | "root" | "git_worktree">(), + strategy: text(), + time_created: integer() + .notNull() + .$default(() => Date.now()), + }, + (table) => [primaryKey({ columns: [table.project_id, table.directory] })], +) diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..03f7d7eef378ce8843c31635bbe95956d8f1ae5a --- /dev/null +++ b/packages/core/src/provider.ts @@ -0,0 +1,25 @@ +export * as ProviderV2 from "./provider" + +import { Types } from "effect" +import { Provider } from "@opencode-ai/schema/provider" + +export const ID = Provider.ID +export type ID = typeof ID.Type + +export const AISDK = Provider.AISDK + +export const Native = Provider.Native + +export const Api = Provider.Api +export type Api = Provider.Api +export type MutableApi = T extends Api + ? Omit, "settings"> & (undefined extends T["settings"] ? { settings?: any } : { settings: any }) + : never + +export const Request = Provider.Request +export type Request = Provider.Request + +export const Info = Provider.Info +export type Info = Provider.Info + +export type MutableInfo = Omit, "api"> & { api: MutableApi } diff --git a/packages/core/src/pty.ts b/packages/core/src/pty.ts new file mode 100644 index 0000000000000000000000000000000000000000..e5be4d4b649ec356b0d2a4f28053c823152185d3 --- /dev/null +++ b/packages/core/src/pty.ts @@ -0,0 +1,318 @@ +export * as Pty from "./pty" + +import { makeLocationNode } from "./effect/app-node" +import type { Disp, Proc } from "#pty" +import { Context, Effect, Layer, Schema, Types } from "effect" +import { Pty } from "@opencode-ai/schema/pty" +import { Config } from "./config" +import { EventV2 } from "./event" +import { Location } from "./location" +import { PtyID } from "./pty/schema" +import { Shell } from "./shell" +import { lazy } from "./util/lazy" + +const BUFFER_LIMIT = 1024 * 1024 * 2 +// Exited sessions stay observable (status, exit code, retained output) until removed explicitly. +// Cap retention so abandoned terminals do not accumulate unbounded buffers. +const EXITED_LIMIT = 25 +const pty = lazy(() => import("#pty")) + +type Subscriber = { + readonly onData: (chunk: string) => void + readonly onEnd: (event: { exitCode?: number }) => void + active: boolean + detached: boolean + pending: string[] + end?: { exitCode?: number } +} + +type Active = { + info: Info + process: Proc + buffer: string + bufferCursor: number + cursor: number + subscribers: Map + listeners: Disp[] +} + +export const Info = Pty.Info +export type Info = Types.DeepMutable + +export const CreateInput = Pty.CreateInput + +export type CreateInput = Types.DeepMutable + +export const UpdateInput = Pty.UpdateInput + +export type UpdateInput = Types.DeepMutable + +export const Event = Pty.Event + +export type AttachInput = { + // Absolute output cursor to replay from. -1 tails from the current end; omitted replays the full retained buffer. + readonly cursor?: number + // Callbacks fire synchronously from the native PTY data path; keep them non-blocking. + readonly onData: (chunk: string) => void + // Fired once when the session stops producing output: process exit (exitCode set), removal, or service teardown. + readonly onEnd: (event: { exitCode?: number }) => void +} + +export type Attachment = { + // Retained output from the requested cursor to the current end. + readonly replay: string + // Absolute output cursor after replay. + readonly cursor: number + readonly write: (data: string) => void + // Starts live delivery after the caller has applied replay and cursor metadata. + readonly activate: () => void + readonly detach: () => void +} + +export class NotFoundError extends Schema.TaggedErrorClass()("Pty.NotFoundError", { + ptyID: PtyID, +}) {} + +export class ExitedError extends Schema.TaggedErrorClass()("Pty.ExitedError", { + ptyID: PtyID, +}) {} + +export interface Interface { + readonly list: () => Effect.Effect + readonly get: (id: PtyID) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly update: (id: PtyID, input: UpdateInput) => Effect.Effect + readonly remove: (id: PtyID) => Effect.Effect + readonly write: (id: PtyID, data: string) => Effect.Effect + readonly attach: (id: PtyID, input: AttachInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Pty") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const location = yield* Location.Service + const config = yield* Config.Service + const context = yield* Effect.context() + const runFork = Effect.runForkWith(context) + const sessions = new Map() + const exitOrder: PtyID[] = [] + + function notifyEnd(session: Active, event: { exitCode?: number }) { + for (const subscriber of session.subscribers.values()) { + if (!subscriber.active) { + subscriber.end = event + continue + } + try { + subscriber.onEnd(event) + } catch {} + } + session.subscribers.clear() + } + + function teardown(session: Active) { + for (const listener of session.listeners) listener.dispose() + session.listeners.length = 0 + if (session.info.status === "running") { + try { + session.process.kill() + } catch {} + } + notifyEnd(session, {}) + } + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + for (const session of sessions.values()) teardown(session) + sessions.clear() + exitOrder.length = 0 + }), + ) + + const requireSession = Effect.fn("Pty.requireSession")(function* (id: PtyID) { + const session = sessions.get(id) + if (!session) return yield* new NotFoundError({ ptyID: id }) + return session + }) + + const removeSession = Effect.fnUntraced(function* (id: PtyID) { + const session = sessions.get(id) + if (!session) return + sessions.delete(id) + const index = exitOrder.indexOf(id) + if (index !== -1) exitOrder.splice(index, 1) + yield* Effect.logInfo("removing session", { id }) + teardown(session) + yield* events.publish(Event.Deleted, { id: session.info.id }) + }) + + const remove = Effect.fn("Pty.remove")(function* (id: PtyID) { + yield* requireSession(id) + yield* removeSession(id) + }) + + const list = Effect.fn("Pty.list")(function* () { + return Array.from(sessions.values()).map((session) => session.info) + }) + + const get = Effect.fn("Pty.get")(function* (id: PtyID) { + return (yield* requireSession(id)).info + }) + + const create = Effect.fn("Pty.create")(function* (input: CreateInput) { + const id = PtyID.ascending() + const command = input.command || Shell.preferred(Config.latest(yield* config.entries(), "shell")) + const args = Shell.login(command) ? [...(input.args ?? []), "-l"] : [...(input.args ?? [])] + const cwd = input.cwd || location.directory + const env = { + ...process.env, + ...input.env, + TERM: "xterm-256color", + OPENCODE_TERMINAL: "1", + } as Record + if (process.platform === "win32") { + env.LC_ALL = "C.UTF-8" + env.LC_CTYPE = "C.UTF-8" + env.LANG = "C.UTF-8" + } + yield* Effect.logInfo("creating session", { id, cmd: command, args, cwd }) + const { spawn } = yield* Effect.promise(() => pty()) + const proc = yield* Effect.sync(() => spawn(command, args, { name: "xterm-256color", cwd, env })) + const info: Info = { + id, + title: input.title || `Terminal ${id.slice(-4)}`, + command, + args, + cwd, + status: "running", + pid: proc.pid, + } + const session: Active = { + info, + process: proc, + buffer: "", + bufferCursor: 0, + cursor: 0, + subscribers: new Map(), + listeners: [], + } + sessions.set(id, session) + session.listeners.push( + proc.onData((chunk) => { + session.cursor += chunk.length + for (const [token, subscriber] of session.subscribers.entries()) { + if (!subscriber.active) { + subscriber.pending.push(chunk) + continue + } + try { + subscriber.onData(chunk) + } catch { + session.subscribers.delete(token) + } + } + session.buffer += chunk + if (session.buffer.length <= BUFFER_LIMIT) return + const excess = session.buffer.length - BUFFER_LIMIT + session.buffer = session.buffer.slice(excess) + session.bufferCursor += excess + }), + proc.onExit(({ exitCode }) => { + if (session.info.status === "exited") return + session.info.status = "exited" + session.info.exitCode = exitCode + notifyEnd(session, { exitCode }) + exitOrder.push(id) + runFork( + Effect.gen(function* () { + yield* Effect.logInfo("session exited", { id, exitCode }) + yield* events.publish(Event.Exited, { id, exitCode }) + while (exitOrder.length > EXITED_LIMIT) { + const oldest = exitOrder[0] + if (!oldest) break + yield* removeSession(oldest) + } + }), + ) + }), + ) + yield* events.publish(Event.Created, { info }) + return info + }) + + const update = Effect.fn("Pty.update")(function* (id: PtyID, input: UpdateInput) { + const session = yield* requireSession(id) + if (input.title) session.info.title = input.title + if (input.size && session.info.status === "running") session.process.resize(input.size.cols, input.size.rows) + yield* events.publish(Event.Updated, { info: session.info }) + return session.info + }) + + const write = Effect.fn("Pty.write")(function* (id: PtyID, data: string) { + const session = yield* requireSession(id) + if (session.info.status === "running") session.process.write(data) + }) + + const attach = Effect.fn("Pty.attach")(function* (id: PtyID, input: AttachInput) { + const session = yield* requireSession(id) + if (session.info.status !== "running") return yield* new ExitedError({ ptyID: id }) + yield* Effect.logInfo("client attached to session", { id, directory: location.directory }) + const token = {} + const subscriber: Subscriber = { + onData: input.onData, + onEnd: input.onEnd, + active: false, + detached: false, + pending: [], + } + session.subscribers.set(token, subscriber) + const start = session.bufferCursor + const end = session.cursor + const from = + input.cursor === -1 + ? end + : typeof input.cursor === "number" && Number.isSafeInteger(input.cursor) + ? Math.max(0, input.cursor) + : 0 + const replay = (() => { + if (!session.buffer || from >= end) return "" + const offset = Math.max(0, from - start) + if (offset >= session.buffer.length) return "" + return session.buffer.slice(offset) + })() + return { + replay, + cursor: end, + write: (data: string) => { + if (session.info.status === "running") session.process.write(data) + }, + activate: () => { + if (subscriber.active || subscriber.detached) return + subscriber.active = true + try { + for (const chunk of subscriber.pending) subscriber.onData(chunk) + subscriber.pending.length = 0 + if (subscriber.end) subscriber.onEnd(subscriber.end) + } catch { + session.subscribers.delete(token) + } + }, + detach: () => { + subscriber.detached = true + subscriber.pending.length = 0 + subscriber.end = undefined + session.subscribers.delete(token) + }, + } + }) + + return Service.of({ list, get, create, update, remove, write, attach }) + }), +) + +export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer)) + +export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node, Location.node, Config.node] }) diff --git a/packages/core/src/pty/protocol.ts b/packages/core/src/pty/protocol.ts new file mode 100644 index 0000000000000000000000000000000000000000..21c6a89f734641a9cea41268cd4d6bddc85b6bb5 --- /dev/null +++ b/packages/core/src/pty/protocol.ts @@ -0,0 +1,37 @@ +export * as PtyProtocol from "./protocol" + +// Wire protocol for PTY websocket transports. The PTY domain service is transport-free; server +// routes adapt Pty.attach to websockets with these helpers so every surface speaks one protocol. +// +// Outbound frames are raw UTF-8 terminal chunks. One control frame — a 0x00 byte followed by +// UTF-8 JSON — carries the absolute output cursor after replay so clients can resume later. + +const encoder = new TextEncoder() +const decoder = new TextDecoder("utf-8", { fatal: true }) + +// Replay can be megabytes; send it in bounded frames. +export const REPLAY_CHUNK = 64 * 1024 + +export function metaFrame(cursor: number) { + const bytes = encoder.encode(JSON.stringify({ cursor })) + const out = new Uint8Array(bytes.length + 1) + out[0] = 0 + out.set(bytes, 1) + return out +} + +export function chunks(data: string) { + const out: string[] = [] + for (let i = 0; i < data.length; i += REPLAY_CHUNK) out.push(data.slice(i, i + REPLAY_CHUNK)) + return out +} + +// Inbound client frames are UTF-8 text or binary; invalid UTF-8 input is dropped. +export function decodeInput(message: string | Uint8Array | ArrayBuffer) { + if (typeof message === "string") return message + try { + return decoder.decode(message instanceof ArrayBuffer ? new Uint8Array(message) : message) + } catch { + return undefined + } +} diff --git a/packages/core/src/pty/pty.bun.ts b/packages/core/src/pty/pty.bun.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f8ce8e4546b815c013cc028f5f5a91139517c03 --- /dev/null +++ b/packages/core/src/pty/pty.bun.ts @@ -0,0 +1,26 @@ +import { spawn as create } from "bun-pty" +import type { Opts, Proc } from "./pty" + +export type { Disp, Exit, Opts, Proc } from "./pty" + +export function spawn(file: string, args: string[], opts: Opts): Proc { + const pty = create(file, args, opts) + return { + pid: pty.pid, + onData(listener) { + return pty.onData(listener) + }, + onExit(listener) { + return pty.onExit(listener) + }, + write(data) { + pty.write(data) + }, + resize(cols, rows) { + pty.resize(cols, rows) + }, + kill(signal) { + pty.kill(signal) + }, + } +} diff --git a/packages/core/src/pty/pty.node.ts b/packages/core/src/pty/pty.node.ts new file mode 100644 index 0000000000000000000000000000000000000000..54691d1e84d618757feb9503b273a8a2a4836b99 --- /dev/null +++ b/packages/core/src/pty/pty.node.ts @@ -0,0 +1,29 @@ +import * as pty from "@lydell/node-pty" +import type { Opts, Proc } from "./pty" + +export type { Disp, Exit, Opts, Proc } from "./pty" + +export function spawn(file: string, args: string[], opts: Opts): Proc { + const proc = pty.spawn(file, args, { + ...opts, + ...(process.platform === "win32" ? { useConptyDll: true } : {}), + }) + return { + pid: proc.pid, + onData(listener) { + return proc.onData(listener) + }, + onExit(listener) { + return proc.onExit(listener) + }, + write(data) { + proc.write(data) + }, + resize(cols, rows) { + proc.resize(cols, rows) + }, + kill(signal) { + proc.kill(signal) + }, + } +} diff --git a/packages/core/src/pty/pty.ts b/packages/core/src/pty/pty.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbd1710e52a1f61dab4a66fcecd700cee96c0b06 --- /dev/null +++ b/packages/core/src/pty/pty.ts @@ -0,0 +1,25 @@ +export type Disp = { + dispose(): void +} + +export type Exit = { + exitCode: number + signal?: number | string +} + +export type Opts = { + name: string + cols?: number + rows?: number + cwd?: string + env?: Record +} + +export type Proc = { + pid: number + onData(listener: (data: string) => void): Disp + onExit(listener: (event: Exit) => void): Disp + write(data: string): void + resize(cols: number, rows: number): void + kill(signal?: string): void +} diff --git a/packages/core/src/pty/schema.ts b/packages/core/src/pty/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab0c40521b1fbfeaa7253eab704913b2ba264666 --- /dev/null +++ b/packages/core/src/pty/schema.ts @@ -0,0 +1 @@ +export { ID as PtyID } from "@opencode-ai/schema/pty" diff --git a/packages/core/src/pty/ticket.ts b/packages/core/src/pty/ticket.ts new file mode 100644 index 0000000000000000000000000000000000000000..07838b14157f54142a8ea5546c4601e838067388 --- /dev/null +++ b/packages/core/src/pty/ticket.ts @@ -0,0 +1,56 @@ +export * as PtyTicket from "./ticket" + +import { WorkspaceV2 } from "../workspace" +import { PtyTicket } from "@opencode-ai/schema/pty-ticket" +import { PtyID } from "./schema" +import { Cache, Context, Duration, Effect, Layer } from "effect" +import { makeGlobalNode } from "../effect/app-node" + +const DEFAULT_TTL = Duration.seconds(60) +const CAPACITY = 10_000 + +export const ConnectToken = PtyTicket.ConnectToken + +export type Scope = { + readonly ptyID: PtyID + readonly directory?: string + readonly workspaceID?: WorkspaceV2.ID +} + +export interface Interface { + issue(input: Scope): Effect.Effect + consume(input: Scope & { readonly ticket: string }): Effect.Effect +} + +export class Service extends Context.Service()("@opencode/PtyTicket") {} + +function matches(record: Scope, input: Scope) { + return ( + record.ptyID === input.ptyID && record.directory === input.directory && record.workspaceID === input.workspaceID + ) +} + +// Tickets are inserted via Cache.set and removed atomically via invalidateWhen. The lookup is +// never invoked; it dies if it ever is, which would signal a misuse of the Service interface. +const noLookup = () => Effect.die("PtyTicket cache must be used via set/invalidateWhen, never get") + +// Visible for tests so the TTL can be shortened. Production uses `layer` with the default TTL. +export const make = (ttl: Duration.Input = DEFAULT_TTL) => + Effect.gen(function* () { + const cache = yield* Cache.make({ capacity: CAPACITY, lookup: noLookup, timeToLive: ttl }) + const expiresIn = Math.max(1, Math.round(Duration.toSeconds(Duration.fromInputUnsafe(ttl)))) + return Service.of({ + issue: Effect.fn("PtyTicket.issue")(function* (input) { + const ticket = crypto.randomUUID() + yield* Cache.set(cache, ticket, input) + return { ticket, expires_in: expiresIn } + }), + consume: Effect.fn("PtyTicket.consume")(function* (input) { + return yield* Cache.invalidateWhen(cache, input.ticket, (stored) => matches(stored, input)) + }), + }) + }) + +const layer = Layer.effect(Service, make()) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] }) diff --git a/packages/core/src/public-event-manifest.ts b/packages/core/src/public-event-manifest.ts new file mode 100644 index 0000000000000000000000000000000000000000..11b84f790546718a107a0447935be5fca371b863 --- /dev/null +++ b/packages/core/src/public-event-manifest.ts @@ -0,0 +1,7 @@ +export * as PublicEventManifest from "./public-event-manifest" + +import { Event } from "@opencode-ai/schema/event" +import { EventManifest } from "@opencode-ai/schema/event-manifest" + +export const Definitions = EventManifest.ServerDefinitions +export const Latest = Event.latest(Definitions) diff --git a/packages/core/src/question.ts b/packages/core/src/question.ts new file mode 100644 index 0000000000000000000000000000000000000000..79e0ea5e0384fcb6f3f6a3959a38e6ac4010fab0 --- /dev/null +++ b/packages/core/src/question.ts @@ -0,0 +1,153 @@ +export * as QuestionV2 from "./question" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Deferred, Effect, Layer, Schema } from "effect" +import { Question } from "@opencode-ai/schema/question" +import { EventV2 } from "./event" +import { SessionSchema } from "./session/schema" + +export const ID = Question.ID +export type ID = typeof ID.Type + +export const Option = Question.Option +export type Option = typeof Option.Type + +export const Info = Question.Info +export type Info = typeof Info.Type + +export const Prompt = Question.Prompt +export type Prompt = typeof Prompt.Type + +export const Tool = Question.Tool +export type Tool = typeof Tool.Type + +export const Request = Question.Request +export type Request = typeof Request.Type + +export const Answer = Question.Answer +export type Answer = typeof Answer.Type + +export const Reply = Question.Reply +export type Reply = typeof Reply.Type + +export const Event = Question.Event + +export class RejectedError extends Schema.TaggedErrorClass()("QuestionV2.RejectedError", {}) { + override get message() { + return "The user dismissed this question" + } +} + +export class NotFoundError extends Schema.TaggedErrorClass()("QuestionV2.NotFoundError", { + requestID: ID, +}) {} + +export interface AskInput { + readonly sessionID: SessionSchema.ID + readonly questions: ReadonlyArray + readonly tool?: Tool +} + +export interface ReplyInput { + readonly requestID: ID + readonly answers: ReadonlyArray +} + +export interface Interface { + readonly ask: (input: AskInput) => Effect.Effect, RejectedError> + readonly reply: (input: ReplyInput) => Effect.Effect + readonly reject: (requestID: ID) => Effect.Effect + readonly list: () => Effect.Effect> +} + +export class Service extends Context.Service()("@opencode/v2/Question") {} + +interface Pending { + readonly request: Request + readonly deferred: Deferred.Deferred, RejectedError> +} + +/** + * Location-owned pending prompts. The Location layer map must materialize this + * layer once per embedded Location so replies cannot settle another Location's + * deferred request. + */ +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const events = yield* EventV2.Service + const pending = new Map() + + yield* Effect.addFinalizer(() => + Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), { + discard: true, + }).pipe( + Effect.ensuring( + Effect.sync(() => { + pending.clear() + }), + ), + ), + ) + + const ask = Effect.fn("QuestionV2.ask")((input: AskInput) => + Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const id = ID.ascending() + const deferred = yield* Deferred.make, RejectedError>() + const request: Request = { id, ...input } + pending.set(id, { request, deferred }) + return yield* events.publish(Event.Asked, request).pipe( + Effect.andThen(restore(Deferred.await(deferred))), + Effect.ensuring( + Effect.sync(() => { + pending.delete(id) + }), + ), + ) + }), + ), + ) + + const reply = Effect.fn("QuestionV2.reply")((input: ReplyInput) => + Effect.uninterruptible( + Effect.gen(function* () { + const existing = pending.get(input.requestID) + if (!existing) return yield* new NotFoundError({ requestID: input.requestID }) + yield* events.publish(Event.Replied, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + answers: input.answers.map((answer) => [...answer]), + }) + yield* Deferred.succeed(existing.deferred, input.answers) + pending.delete(input.requestID) + }), + ), + ) + + const reject = Effect.fn("QuestionV2.reject")((requestID: ID) => + Effect.uninterruptible( + Effect.gen(function* () { + const existing = pending.get(requestID) + if (!existing) return yield* new NotFoundError({ requestID }) + yield* events.publish(Event.Rejected, { + sessionID: existing.request.sessionID, + requestID: existing.request.id, + }) + yield* Deferred.fail(existing.deferred, new RejectedError()) + pending.delete(requestID) + }), + ), + ) + + const list = Effect.fn("QuestionV2.list")(function* () { + return Array.from(pending.values(), (item) => item.request) + }) + + return Service.of({ ask, reply, reject, list }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e1ab9d20f758ca00996c22a9c2b77eade563998 --- /dev/null +++ b/packages/core/src/reference.ts @@ -0,0 +1,125 @@ +export * as Reference from "./reference" + +import { makeLocationNode } from "./effect/app-node" +import { Context, Effect, Layer, Scope, Types } from "effect" +import { Reference } from "@opencode-ai/schema/reference" +import { Global } from "./global" +import { EventV2 } from "./event" +import { Repository } from "./repository" +import { RepositoryCache } from "./repository-cache" +import { AbsolutePath } from "./schema" +import { State } from "./state" + +export const LocalSource = Reference.LocalSource +export type LocalSource = Reference.LocalSource + +export const GitSource = Reference.GitSource +export type GitSource = Reference.GitSource + +export const Source = Reference.Source +export type Source = Reference.Source + +export const Event = Reference.Event + +export const Info = Reference.Info +export type Info = Reference.Info + +type Data = { + sources: Map> +} + +type Draft = { + add(name: string, source: Source): void + remove(name: string): void + list(): readonly [string, Source][] +} + +export interface Interface extends State.Transformable { + readonly list: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Reference") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const global = yield* Global.Service + const events = yield* EventV2.Service + const cache = yield* RepositoryCache.Service + const scope = yield* Scope.Scope + const materialized = new Map() + const state = State.create({ + initial: () => ({ sources: new Map() }), + draft: (draft) => ({ + add: (name, source) => draft.sources.set(name, source as Types.DeepMutable), + remove: (name) => draft.sources.delete(name), + list: () => Array.from(draft.sources.entries()) as [string, Source][], + }), + finalize: (draft) => + Effect.gen(function* () { + materialized.clear() + for (const [name, source] of draft.list()) { + if (source.type === "local") { + materialized.set( + name, + new Info({ + name, + path: source.path, + ...(source.description === undefined ? {} : { description: source.description }), + ...(source.hidden === undefined ? {} : { hidden: source.hidden }), + source, + }), + ) + continue + } + const repository = Repository.parse(source.repository) + if (!repository || !Repository.isRemote(repository)) continue + if (source.branch) { + try { + Repository.validateBranch(source.branch) + } catch { + continue + } + } + materialized.set( + name, + new Info({ + name, + path: AbsolutePath.make(Repository.cachePath(global.repos, repository, source.branch)), + ...(source.description === undefined ? {} : { description: source.description }), + ...(source.hidden === undefined ? {} : { hidden: source.hidden }), + source, + }), + ) + yield* cache.ensure({ reference: repository, branch: source.branch, refresh: true }).pipe( + Effect.catchCause((cause) => + Effect.logWarning("failed to materialize reference", { + name, + repository: source.repository, + cause, + }), + ), + Effect.forkIn(scope), + ) + } + yield* events.publish(Event.Updated, {}) + }), + }) + + return Service.of({ + transform: state.transform, + reload: state.reload, + list: Effect.fn("Reference.list")(function* () { + return Array.from(materialized.values()) + }), + }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ + service: Service, + layer, + deps: [Global.node, EventV2.node, RepositoryCache.node], +}) diff --git a/packages/core/src/reference/guidance.ts b/packages/core/src/reference/guidance.ts new file mode 100644 index 0000000000000000000000000000000000000000..25566e2f2f6e978d030413ac3abc2aa6aefa1d05 --- /dev/null +++ b/packages/core/src/reference/guidance.ts @@ -0,0 +1,69 @@ +export * as ReferenceGuidance from "./guidance" + +import { makeLocationNode } from "../effect/app-node" +import { Context, Effect, Layer, Schema } from "effect" +import { Reference } from "../reference" +import { SystemContext } from "../system-context/index" + +const Summary = Schema.Struct({ + name: Schema.String, + path: Schema.String, + description: Schema.String.pipe(Schema.optional), +}) + +const render = (references: ReadonlyArray) => + [ + "Project references provide additional directories that can be accessed when relevant.", + "", + ...references.flatMap((reference) => [ + " ", + ` ${reference.name}`, + ` ${reference.path}`, + ...(reference.description === undefined ? [] : [` ${reference.description}`]), + " ", + ]), + "", + ].join("\n") + +export interface Interface { + readonly load: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ReferenceGuidance") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const references = yield* Reference.Service + + return Service.of({ + load: Effect.fn("ReferenceGuidance.load")(function* () { + const available = (yield* references.list()) + .filter((reference) => reference.description !== undefined) + .map((reference) => ({ + name: reference.name, + path: reference.path, + description: reference.description, + })) + .toSorted((a, b) => a.name.localeCompare(b.name)) + if (available.length === 0) return SystemContext.empty + return SystemContext.make({ + key: SystemContext.Key.make("core/reference-guidance"), + codec: Schema.toCodecJson(Schema.Array(Summary)), + load: Effect.succeed(available), + baseline: render, + update: (_previous, current) => + [ + "The available project references have changed. This list supersedes the previous reference list.", + render(current), + ].join("\n"), + removed: () => "Project reference guidance is no longer available. Do not use previously listed references.", + }) + }), + }) + }), +) + +export const locationLayer = layer + +export const node = makeLocationNode({ service: Service, layer, deps: [Reference.node] }) diff --git a/packages/core/src/repository-cache.ts b/packages/core/src/repository-cache.ts new file mode 100644 index 0000000000000000000000000000000000000000..cab2f631ffb84c7df07dfe95dd6a25d8396b1d6d --- /dev/null +++ b/packages/core/src/repository-cache.ts @@ -0,0 +1,259 @@ +/** + * Local tracking checkouts for remote Git references, one per remote and + * branch. Each checkout permanently tracks a single ref: the requested branch + * when the cache key has one, otherwise the remote default branch. Content + * follows "newest wins": refresh fetches and hard-resets, so readers may + * observe the checkout move underneath them. + */ +import path from "path" +import { Context, Effect, Layer, Schema } from "effect" +import { FSUtil } from "./fs-util" +import { Git } from "./git" +import { Global } from "./global" +import { Repository } from "./repository" +import { AbsolutePath } from "./schema" +import { makeGlobalNode } from "./effect/app-node" +import { EffectFlock } from "./util/effect-flock" + +export type Result = { + readonly repository: string + readonly host: string + readonly remote: string + readonly localPath: string + readonly status: "cached" | "cloned" | "refreshed" + readonly head?: string + readonly branch?: string +} + +export type EnsureInput = { + readonly reference: Repository.RemoteReference + readonly refresh?: boolean + readonly branch?: string +} + +export class InvalidRepositoryError extends Schema.TaggedErrorClass()( + "RepositoryCacheInvalidRepositoryError", + { + repository: Schema.String, + message: Schema.String, + }, +) {} + +export class InvalidBranchError extends Schema.TaggedErrorClass()( + "RepositoryCacheInvalidBranchError", + { + branch: Schema.String, + message: Schema.String, + }, +) {} + +export class CloneFailedError extends Schema.TaggedErrorClass()("RepositoryCacheCloneFailedError", { + repository: Schema.String, + message: Schema.String, +}) {} + +export class FetchFailedError extends Schema.TaggedErrorClass()("RepositoryCacheFetchFailedError", { + repository: Schema.String, + message: Schema.String, +}) {} + +export class CheckoutFailedError extends Schema.TaggedErrorClass()( + "RepositoryCacheCheckoutFailedError", + { + repository: Schema.String, + branch: Schema.String, + message: Schema.String, + }, +) {} + +export class ResetFailedError extends Schema.TaggedErrorClass()("RepositoryCacheResetFailedError", { + repository: Schema.String, + message: Schema.String, +}) {} + +export class LockFailedError extends Schema.TaggedErrorClass()("RepositoryCacheLockFailedError", { + localPath: Schema.String, + message: Schema.String, +}) {} + +export class CacheOperationError extends Schema.TaggedErrorClass()( + "RepositoryCacheOperationError", + { + operation: Schema.String, + path: Schema.String, + message: Schema.String, + }, +) {} + +export type Error = + | InvalidRepositoryError + | InvalidBranchError + | CloneFailedError + | FetchFailedError + | CheckoutFailedError + | ResetFailedError + | LockFailedError + | CacheOperationError + +export interface Interface { + readonly ensure: (input: EnsureInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/RepositoryCache") {} + +export function isError(error: unknown): error is Error { + return ( + error instanceof InvalidRepositoryError || + error instanceof InvalidBranchError || + error instanceof CloneFailedError || + error instanceof FetchFailedError || + error instanceof CheckoutFailedError || + error instanceof ResetFailedError || + error instanceof LockFailedError || + error instanceof CacheOperationError + ) +} + +export const parseRemote = Effect.fn("RepositoryCache.parseRemote")(function* (repository: string) { + return yield* Effect.try({ + try: () => Repository.parseRemote(repository), + catch: (error) => new InvalidRepositoryError({ repository, message: errorMessage(error) }), + }) +}) + +export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(function* (branch: string) { + return yield* Effect.try({ + try: () => Repository.validateBranch(branch), + catch: (error) => new InvalidBranchError({ branch, message: errorMessage(error) }), + }) +}) + +const layer: Layer.Layer = + Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const git = yield* Git.Service + const flock = yield* EffectFlock.Service + const global = yield* Global.Service + + return Service.of({ + ensure: Effect.fn("RepositoryCache.ensure")(function* (input) { + if (input.branch) yield* validateBranch(input.branch) + + const repository = input.reference.label + const localPath = Repository.cachePath(global.repos, input.reference, input.branch) + const cloneTarget = Repository.parse(input.reference.remote) ?? input.reference + + return yield* flock + .withLock( + Effect.gen(function* () { + yield* cacheOperation(fs.ensureDir(path.dirname(localPath)), "ensure cache directory", localPath) + + const existing = yield* git.repo.discover(AbsolutePath.make(localPath)) + const origin = existing ? yield* git.remote.get(existing) : undefined + const originReference = origin ? Repository.parse(origin) : undefined + // Discovery walks upward, so an enclosing repository with a + // matching origin could masquerade as the cache entry; reuse + // requires the checkout to live exactly at the cache path. + const worktree = existing ? yield* fs.resolve(localPath) : undefined + const reuse = Boolean( + existing && + existing.worktree === worktree && + originReference && + Repository.same(originReference, cloneTarget), + ) + if (!reuse && (yield* fs.existsSafe(localPath))) { + yield* cacheOperation(fs.remove(localPath, { recursive: true }), "remove stale cache", localPath) + } + + const status = !reuse + ? ("cloned" as const) + : input.refresh + ? ("refreshed" as const) + : ("cached" as const) + + if (status === "cloned") { + yield* git.repo + .clone({ + remote: input.reference.remote, + directory: AbsolutePath.make(localPath), + branch: input.branch, + }) + .pipe(Effect.mapError((error) => new CloneFailedError({ repository, message: error.message }))) + } + + if (status === "refreshed") { + if (!existing) + return yield* new FetchFailedError({ repository, message: "Repository is unavailable" }) + yield* git.sync + .fetchRemotes(existing) + .pipe(Effect.mapError((error) => new FetchFailedError({ repository, message: error.message }))) + + if (input.branch) { + yield* git.sync + .fetchBranch(existing, { branch: input.branch }) + .pipe(Effect.mapError((error) => new FetchFailedError({ repository, message: error.message }))) + } + + // Checking out the tracked ref before resetting keeps the + // checkout self-healing even if it was left on another + // branch. + const branch = input.branch ?? (yield* git.history.defaultRemoteBranch(existing)) + if (branch) { + yield* git.sync + .checkoutRemoteBranch(existing, { branch }) + .pipe( + Effect.mapError( + (error) => new CheckoutFailedError({ repository, branch, message: error.message }), + ), + ) + } + + const target = branch ?? (yield* git.history.branch(existing)) + yield* git.sync + .resetHard(existing, target ? `origin/${target}` : "HEAD") + .pipe(Effect.mapError((error) => new ResetFailedError({ repository, message: error.message }))) + } + + const checkout = yield* git.repo.discover(AbsolutePath.make(localPath)) + + return { + repository, + host: input.reference.host, + remote: input.reference.remote, + localPath, + status, + head: checkout ? yield* git.history.head(checkout) : undefined, + branch: checkout ? yield* git.history.branch(checkout) : undefined, + } satisfies Result + }), + `repository-cache:${localPath}`, + ) + .pipe( + Effect.mapError((error) => + isError(error) ? error : new LockFailedError({ localPath, message: errorMessage(error) }), + ), + ) + }), + }) + }), + ) + +export const node = makeGlobalNode({ + service: Service, + layer, + deps: [EffectFlock.node, FSUtil.node, Git.node, Global.node], +}) + +function errorMessage(error: unknown) { + return error instanceof globalThis.Error ? error.message : String(error) +} + +function cacheOperation(effect: Effect.Effect, operation: string, target: string) { + return effect.pipe( + Effect.mapError((error) => new CacheOperationError({ operation, path: target, message: errorMessage(error) })), + ) +} + +export * as RepositoryCache from "./repository-cache" diff --git a/packages/core/src/repository.ts b/packages/core/src/repository.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ee5be600e3a4284092cb525dfa401a31dbb2075 --- /dev/null +++ b/packages/core/src/repository.ts @@ -0,0 +1,214 @@ +import path from "path" +import { fileURLToPath } from "url" +import { Schema } from "effect" + +type BaseReference = { + readonly host: string + readonly path: string + readonly segments: string[] + readonly owner?: string + readonly repo: string + readonly remote: string + readonly label: string +} + +export type RemoteReference = BaseReference & { + readonly protocol?: string +} + +export type FileReference = BaseReference & { + readonly host: "file" + readonly protocol: "file:" +} + +export type Reference = RemoteReference | FileReference + +export class InvalidReferenceError extends Schema.TaggedErrorClass()( + "RepositoryInvalidReferenceError", + { + repository: Schema.String, + message: Schema.String, + }, +) {} + +export class UnsupportedLocalRepositoryError extends Schema.TaggedErrorClass()( + "RepositoryUnsupportedLocalRepositoryError", + { + repository: Schema.String, + message: Schema.String, + }, +) {} + +export class InvalidBranchError extends Schema.TaggedErrorClass()("RepositoryInvalidBranchError", { + branch: Schema.String, + message: Schema.String, +}) {} + +export type Error = InvalidReferenceError | UnsupportedLocalRepositoryError | InvalidBranchError + +export function isError(error: unknown): error is Error { + return ( + error instanceof InvalidReferenceError || + error instanceof UnsupportedLocalRepositoryError || + error instanceof InvalidBranchError + ) +} + +export function parse(input: string): Reference | undefined { + const cleaned = normalizeInput(input) + if (!cleaned) return + + const githubPrefixed = cleaned.match(/^github:([^/\s]+)\/([^/\s]+)$/) + if (githubPrefixed) return buildRemote({ host: "github.com", segments: [githubPrefixed[1], githubPrefixed[2]] }) + + if (!cleaned.includes("://")) { + const scp = cleaned.match(/^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/) + if (scp) return buildRemote({ host: scp[1], segments: parts(scp[2]), remote: cleaned }) + + const direct = parts(cleaned) + if (direct.length >= 2 && hostLike(direct[0])) return buildRemote({ host: direct[0], segments: direct.slice(1) }) + if (direct.length === 2) return buildRemote({ host: "github.com", segments: direct }) + } + + try { + const url = new URL(cleaned) + if (url.protocol === "file:") return buildFile({ url, remote: cleaned }) + const segments = parts(url.pathname) + return buildRemote({ + host: url.host, + segments, + remote: url.host === "github.com" ? githubRemote(segments.join("/")) : cleaned, + protocol: url.protocol, + }) + } catch { + return + } +} + +export function parseRemote(input: string): RemoteReference { + const reference = parse(input) + if (!reference) { + throw new InvalidReferenceError({ + repository: input, + message: "Repository must be a git URL, host/path reference, or GitHub owner/repo shorthand", + }) + } + if (!isRemote(reference)) { + throw new UnsupportedLocalRepositoryError({ + repository: input, + message: "Local file repositories are not supported", + }) + } + return reference +} + +export function validateBranch(branch: string): void { + if (/^[A-Za-z0-9/_.-]+$/.test(branch) && !branch.startsWith("-") && !branch.includes("..")) return + throw new InvalidBranchError({ + branch, + message: "Branch must contain only alphanumeric characters, /, _, ., and -, and cannot start with - or contain ..", + }) +} + +export function isFile(reference: Reference): reference is FileReference { + return reference.protocol === "file:" +} + +export function isRemote(reference: Reference): reference is RemoteReference { + return !isFile(reference) +} + +/** + * Checkouts are keyed by remote and branch: a branch-specific reference gets + * its own directory so branchless refreshes can never move it. The branch is + * percent-encoded because valid branch names may contain `/`. + */ +export function cachePath(root: string, reference: Reference, branch?: string): string { + const base = path.join(root, ...reference.host.split(":"), ...reference.segments) + return branch ? `${base}@${encodeURIComponent(branch)}` : base +} + +export function cacheIdentity(reference: Reference): string { + return `${reference.host}/${reference.path}` +} + +export function same(left: Reference, right: Reference): boolean { + return cacheIdentity(left) === cacheIdentity(right) +} + +function normalizeInput(input: string) { + return input + .trim() + .replace(/^git\+/, "") + .replace(/#.*$/, "") + .replace(/\/+$/, "") +} + +function trimGitSuffix(input: string) { + return input.replace(/\.git$/, "") +} + +function parts(input: string) { + return input + .split("/") + .map((item) => trimGitSuffix(item.trim())) + .filter(Boolean) +} + +function safeHost(input: string) { + return Boolean(input) && !input.startsWith("-") && !/[\s/\\]/.test(input) +} + +function safeSegment(input: string) { + return input !== "." && input !== ".." && !input.includes(":") && !/[\s/\\]/.test(input) +} + +function hostLike(input: string) { + return input.includes(".") || input.includes(":") || input === "localhost" +} + +function withSlash(input: string) { + return input.endsWith("/") ? input : `${input}/` +} + +function githubRemote(pathname: string) { + const base = process.env.OPENCODE_REPO_CLONE_GITHUB_BASE_URL + if (!base) return `https://github.com/${pathname}.git` + return new URL(`${pathname}.git`, withSlash(base)).href +} + +function buildRemote(input: { host: string; segments: string[]; remote?: string; protocol?: string }) { + const segments = input.segments.map(trimGitSuffix).filter(Boolean) + if (!safeHost(input.host) || !segments.length || segments.some((segment) => !safeSegment(segment))) return + const repositoryPath = segments.join("/") + const host = input.host.toLowerCase() + return { + host, + path: repositoryPath, + segments, + owner: segments.length === 2 ? segments[0] : undefined, + repo: segments[segments.length - 1], + remote: + input.remote ?? (host === "github.com" ? githubRemote(repositoryPath) : `https://${host}/${repositoryPath}.git`), + label: host === "github.com" && segments.length === 2 ? repositoryPath : `${host}/${repositoryPath}`, + protocol: input.protocol, + } satisfies RemoteReference +} + +function buildFile(input: { url: URL; remote: string }) { + const filePath = path.normalize(fileURLToPath(input.url)) + const segments = filePath.split(/[\\/]+/).filter(Boolean) + if (!segments.length) return + return { + host: "file", + path: filePath, + segments: segments.map((segment) => segment.replace(/:$/, "")), + owner: undefined, + repo: trimGitSuffix(segments[segments.length - 1]), + remote: input.remote, + label: filePath, + protocol: "file:", + } satisfies FileReference +} + +export * as Repository from "./repository" diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e32ddb6038af2ee58ff19ced7f9a2217a9ffdd5 --- /dev/null +++ b/packages/core/src/ripgrep.ts @@ -0,0 +1,284 @@ +export * as Ripgrep from "./ripgrep" + +import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect" +import { ChildProcess } from "effect/unstable/process" +import { Entry, Match } from "@opencode-ai/schema/filesystem" +import { makeGlobalNode } from "./effect/app-node" +import { AppProcess, collectStream, waitForAbort } from "./process" +import { NonNegativeInt, PositiveInt, RelativePath } from "./schema" +import { RipgrepBinary } from "./ripgrep/binary" + +/** + * Small core-owned ripgrep execution adapter. It deliberately exposes raw + * process-oriented rows, not model text or permission behavior. Search maps + * these rows into filesystem results; leaf tools own + * presentation and permission prompts. + */ + +const ERROR_BYTES = 8 * 1024 +const MAX_RECORD_BYTES = 64 * 1024 +const MAX_SUBMATCHES = 100 + +const RawMatch = Schema.Struct({ + type: Schema.Literal("match"), + data: Schema.Struct({ + path: Schema.Struct({ text: Schema.String }), + lines: Schema.Struct({ text: Schema.String }), + line_number: PositiveInt, + absolute_offset: NonNegativeInt, + submatches: Schema.Array( + Schema.Struct({ + match: Schema.Struct({ text: Schema.String }), + start: NonNegativeInt, + end: NonNegativeInt, + }), + ), + }), +}) + +type RawMatchData = (typeof RawMatch.Type)["data"] + +export class Error extends Schema.TaggedErrorClass()("Ripgrep.Error", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()), +}) {} + +export class InvalidPatternError extends Schema.TaggedErrorClass()("Ripgrep.InvalidPatternError", { + pattern: Schema.String, + message: Schema.String, +}) {} + +export interface FindInput { + readonly cwd: string + readonly pattern: string + readonly limit: number + readonly hidden?: boolean + readonly follow?: boolean + readonly signal?: AbortSignal + readonly onEntry?: (entry: Entry) => Effect.Effect +} + +export interface GlobInput { + readonly cwd: string + readonly pattern: string + readonly limit: number + readonly hidden?: boolean + readonly follow?: boolean + readonly signal?: AbortSignal +} + +export interface GrepInput { + readonly cwd: string + readonly pattern: string + readonly file?: string + readonly include?: string + readonly limit: number + readonly signal?: AbortSignal +} + +export interface Interface { + readonly find: (input: FindInput) => Effect.Effect + readonly glob: (input: GlobInput) => Effect.Effect + readonly grep: (input: GrepInput) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/Ripgrep") {} + +const failure = (message: string, cause?: unknown) => new Error({ message, cause }) + +const isInvalidPattern = (stderr: string) => + stderr.includes("regex parse error") || stderr.includes("error parsing regex") + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const process = yield* AppProcess.Service + const binary = yield* RipgrepBinary.Service + + const run = (input: { + readonly cwd: string + readonly args: string[] + readonly limit: number + readonly signal?: AbortSignal + readonly parse: (line: string) => Effect.Effect + readonly pattern?: string + readonly onItem?: (item: A) => Effect.Effect + }) => { + const program = Effect.scoped( + Effect.gen(function* () { + const handle = yield* process.spawn( + ChildProcess.make(yield* binary.filepath, input.args, { cwd: input.cwd, extendEnv: true, stdin: "ignore" }), + ) + const stderrFiber = yield* collectStream(handle.stderr, ERROR_BYTES).pipe( + Effect.map((output) => output.buffer.toString("utf8")), + Effect.forkScoped, + ) + let observed = 0 + const rows = yield* Stream.decodeText(handle.stdout).pipe( + Stream.splitLines, + Stream.filter((line) => line.length > 0), + Stream.mapEffect(input.parse), + Stream.filter((row): row is A => row !== undefined), + Stream.tap((row) => { + if (!input.onItem || observed++ >= input.limit) return Effect.void + return input.onItem(row) + }), + Stream.take(input.limit + 1), + Stream.runCollect, + Effect.map((chunk) => [...chunk]), + ) + const truncated = rows.length > input.limit + if (truncated) return { items: rows.slice(0, input.limit), truncated, partial: false } + + const code = yield* handle.exitCode + const stderr = yield* Fiber.join(stderrFiber) + if (input.pattern && code === 2 && isInvalidPattern(stderr)) { + return yield* new InvalidPatternError({ pattern: input.pattern, message: stderr.trim() }) + } + if (code !== 0 && code !== 1 && code !== 2) { + return yield* failure(stderr.trim() || `ripgrep failed with code ${code}`) + } + return { items: code === 1 ? [] : rows, truncated: false, partial: code === 2 } + }), + ) + const abortable = input.signal ? program.pipe(Effect.raceFirst(waitForAbort(input.signal))) : program + return abortable.pipe( + Effect.mapError((cause) => + cause instanceof Error || cause instanceof InvalidPatternError + ? cause + : failure("ripgrep execution failed", cause), + ), + ) + } + + return Service.of({ + glob: (input) => + run({ + cwd: input.cwd, + limit: input.limit, + signal: input.signal, + args: [ + "--no-config", + "--files", + ...(input.hidden ? ["--hidden"] : []), + ...(input.follow ? ["--follow"] : []), + `--glob=${input.pattern}`, + "--glob=!**/.git/**", + ".", + ], + parse: (line) => + Effect.succeed( + line + .replace(/^(?:\.[\\/])+/u, "") + .replace(/^[\\/]+/u, "") + .replaceAll("\\", "/"), + ), + }).pipe( + Effect.map((result) => + result.items.map((relative) => + Entry.make({ + path: RelativePath.make(relative), + type: "file", + }), + ), + ), + Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))), + ), + find: (input) => + run({ + cwd: input.cwd, + limit: input.limit, + signal: input.signal, + args: [ + "--no-config", + "--files", + ...(input.hidden ? ["--hidden"] : []), + ...(input.follow ? ["--follow"] : []), + ...(input.pattern === "*" ? [] : [`--glob=${input.pattern}`]), + "--glob=!**/.git/**", + ".", + ], + parse: (line) => { + const relative = line + .replace(/^(?:\.[\\/])+/u, "") + .replace(/^[\\/]+/u, "") + .replaceAll("\\", "/") + return Effect.succeed( + Entry.make({ + path: RelativePath.make(relative), + type: "file", + }), + ) + }, + onItem: input.onEntry, + }).pipe( + Effect.map((result) => result.items), + Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))), + ), + grep: (input) => + run({ + ...input, + args: [ + "--no-config", + "--json", + "--hidden", + "--no-messages", + ...(input.include ? [`--glob=${input.include}`] : []), + "--glob=!**/.git/**", + "--", + input.pattern, + input.file ?? ".", + ], + parse: (line) => + (Buffer.byteLength(line, "utf8") > MAX_RECORD_BYTES + ? Effect.fail(failure(`Ripgrep JSON record exceeded ${MAX_RECORD_BYTES} bytes`)) + : Effect.try({ + try: () => JSON.parse(line) as unknown, + catch: (cause) => failure("Invalid ripgrep JSON output", cause), + }) + ).pipe( + Effect.flatMap((json) => { + if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match") + return Effect.succeed(undefined) + return Schema.decodeUnknownEffect(RawMatch)(json).pipe( + Effect.map((match) => ({ + ...match.data, + path: { text: match.data.path.text.replace(/^\.[\\/]/, "") }, + submatches: match.data.submatches.slice(0, MAX_SUBMATCHES), + })), + Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)), + ) + }), + ), + }).pipe( + Effect.map((result) => + result.items.map((match) => { + const relative = match.path.text + .replace(/^(?:\.[\\/])+/u, "") + .replace(/^[\\/]+/u, "") + .replaceAll("\\", "/") + return Match.make({ + entry: Entry.make({ + path: RelativePath.make(relative), + type: "file", + }), + line: match.line_number, + offset: match.absolute_offset, + text: + match.lines.text.length > 2_000 + ? match.lines.text.slice(0, 2_000).replace(/[\uD800-\uDBFF]$/, "") + "..." + : match.lines.text, + submatches: match.submatches.map((submatch) => ({ + text: submatch.match.text, + start: submatch.start, + end: submatch.end, + })), + }) + }), + ), + ), + }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer: layer, deps: [RipgrepBinary.node, AppProcess.node] }) diff --git a/packages/core/src/ripgrep/binary.ts b/packages/core/src/ripgrep/binary.ts new file mode 100644 index 0000000000000000000000000000000000000000..762a8e613ef0ebb668c3a245b0ca4060c946a89c --- /dev/null +++ b/packages/core/src/ripgrep/binary.ts @@ -0,0 +1,132 @@ +import path from "path" +import { Context, Effect, Layer, Stream } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" +import { ChildProcess } from "effect/unstable/process" +import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { CrossSpawnSpawner } from "../cross-spawn-spawner" +import { makeGlobalNode } from "../effect/app-node" +import { httpClient } from "../effect/app-node-platform" +import { FSUtil } from "../fs-util" +import { Global } from "../global" +import { which } from "../util/which" + +export namespace RipgrepBinary { + const VERSION = "15.1.0" + const PLATFORM = { + "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" }, + "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" }, + "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" }, + "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" }, + "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" }, + "ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" }, + "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }, + } as const + + interface Interface { + readonly filepath: Effect.Effect + } + + export class Service extends Context.Service()("@opencode/RipgrepBinary") {} + + const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FSUtil.Service + const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient) + const spawner = yield* ChildProcessSpawner + + const run = Effect.fnUntraced(function* (command: string, args: string[]) { + const handle = yield* spawner.spawn(ChildProcess.make(command, args, { extendEnv: true, stdin: "ignore" })) + const [stdout, stderr, code] = yield* Effect.all( + [ + Stream.mkString(Stream.decodeText(handle.stdout)), + Stream.mkString(Stream.decodeText(handle.stderr)), + handle.exitCode, + ], + { concurrency: "unbounded" }, + ) + return { stdout, stderr, code } + }, Effect.scoped) + + const extract = Effect.fnUntraced(function* ( + archive: string, + config: (typeof PLATFORM)[keyof typeof PLATFORM], + target: string, + ) { + const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" }) + + if (config.extension === "zip") { + const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe" + const result = yield* run(shell, [ + "-NoProfile", + "-NonInteractive", + "-Command", + `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -LiteralPath '${archive.replaceAll("'", "''")}' -DestinationPath '${dir.replaceAll("'", "''")}' -Force`, + ]) + if (result.code !== 0) + throw new Error( + result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`, + ) + } + + if (config.extension === "tar.gz") { + const result = yield* run("tar", ["-xzf", archive, "-C", dir]) + if (result.code !== 0) + throw new Error( + result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`, + ) + } + + const extracted = path.join( + dir, + `ripgrep-${VERSION}-${config.platform}`, + process.platform === "win32" ? "rg.exe" : "rg", + ) + if (!(yield* fs.isFile(extracted))) throw new Error(`ripgrep archive did not contain executable: ${extracted}`) + + yield* fs.copyFile(extracted, target) + if (process.platform !== "win32") yield* fs.chmod(target, 0o755) + }, Effect.scoped) + + return Service.of({ + filepath: yield* Effect.cached( + Effect.gen(function* () { + const system = yield* Effect.sync(() => which(process.platform === "win32" ? "rg.exe" : "rg")) + if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system + + const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`) + if (yield* fs.isFile(target).pipe(Effect.orDie)) return target + + const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM + const config = PLATFORM[platformKey] + if (!config) throw new Error(`unsupported platform for ripgrep: ${platformKey}`) + + const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}` + const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}` + const archive = path.join(Global.Path.bin, filename) + + yield* Effect.logInfo("downloading ripgrep", { url }) + yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie) + const bytes = yield* HttpClientRequest.get(url).pipe( + http.execute, + Effect.flatMap((response) => response.arrayBuffer), + Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))), + ) + if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`) + + yield* fs.writeWithDirs(archive, new Uint8Array(bytes)) + yield* extract(archive, config, target) + yield* fs.remove(archive, { force: true }).pipe(Effect.ignore) + return target + }), + ), + }) + }), + ) + + export const node = makeGlobalNode({ + service: Service, + layer: layer, + deps: [FSUtil.node, httpClient, CrossSpawnSpawner.node], + }) +} diff --git a/packages/core/src/schema.ts b/packages/core/src/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c25bee19b1deae56434adc919ebd5fface6a8d3 --- /dev/null +++ b/packages/core/src/schema.ts @@ -0,0 +1,79 @@ +import { Schema } from "effect" +import { + AbsolutePath, + DateTimeUtcFromMillis, + NonNegativeInt, + optional, + PositiveInt, + RelativePath, + statics, +} from "@opencode-ai/schema/schema" + +export { AbsolutePath, DateTimeUtcFromMillis, NonNegativeInt, optional, PositiveInt, RelativePath, statics } + +/** + * Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable` + * until `effect:core/x228my` ("Types.DeepMutable widens unknown to `{}`") lands. + * + * The upstream version falls through `unknown` into `{ -readonly [K in keyof T]: ... }` + * where `keyof unknown = never`, so `unknown` collapses to `{}`. This local + * version gates the object branch on `extends object` (which `unknown` does + * not) so `unknown` passes through untouched. + * + * Primitive bailout matches upstream — without it, branded strings like + * `string & Brand<"SessionID">` fall into the object branch and get their + * prototype methods walked. + * + * Tuple branch preserves readonly tuples (e.g. `ConfigPlugin.Spec`'s + * `readonly [string, Options]`); the general array branch would otherwise + * widen them to unbounded arrays. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export type DeepMutable = T extends string | number | boolean | bigint | symbol | Function + ? T + : T extends readonly [unknown, ...unknown[]] + ? { -readonly [K in keyof T]: DeepMutable } + : T extends readonly (infer U)[] + ? DeepMutable[] + : T extends object + ? { -readonly [K in keyof T]: DeepMutable } + : T + +/** + * Nominal wrapper for scalar types. The class itself is a valid schema — + * pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc. + * + * Overrides `~type.make` on the derived `Schema.Opaque` so `Schema.Schema.Type` + * of a field using this newtype resolves to `Self` rather than the underlying + * branded phantom. Without that override, passing a class instance to code + * typed against `Schema.Schema.Type` would require a cast even + * though the values are structurally equivalent at runtime. + * + * @example + * class QuestionID extends Newtype()("QuestionID", Schema.String) { + * static make(id: string): QuestionID { + * return this.make(id) + * } + * } + * + * Schema.decodeEffect(QuestionID)(input) + */ +export function Newtype() { + return (tag: Tag, schema: S) => { + abstract class Base { + declare readonly _newtype: Tag + + static make(value: Schema.Schema.Type): Self { + return value as unknown as Self + } + } + + Object.setPrototypeOf(Base, schema) + + return Base as unknown as (abstract new (_: never) => { readonly _newtype: Tag }) & { + readonly make: (value: Schema.Schema.Type) => Self + } & Omit, "make" | "~type.make"> & { + readonly "~type.make": Self + } + } +} diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts new file mode 100644 index 0000000000000000000000000000000000000000..2dabfb2d6fba2eeff6306abcae0f5fb8c99b6f13 --- /dev/null +++ b/packages/core/src/session.ts @@ -0,0 +1,486 @@ +export * as SessionV2 from "./session" +export * from "./session/schema" + +import { DateTime, Effect, Layer, Schema, Context, Stream } from "effect" +import { ListAnchor } from "@opencode-ai/schema/session" +import { and, asc, desc, eq, gt, like, lt, or, type SQL } from "drizzle-orm" +import { ProjectV2 } from "./project" +import { WorkspaceV2 } from "./workspace" +import { ModelV2 } from "./model" +import { Location } from "./location" +import { SessionMessage } from "./session/message" +import { Prompt } from "./session/prompt" +import { PromptInput } from "@opencode-ai/schema/prompt-input" +import { EventV2 } from "./event" +import { Database } from "./database/database" +import { SessionProjector } from "./session/projector" +import { SessionMessageTable, SessionTable } from "./session/sql" +import { SessionSchema } from "./session/schema" +import { AbsolutePath, PositiveInt, RelativePath } from "./schema" +import { AgentV2 } from "./agent" +import { SessionV1 } from "./v1/session" +import { InstallationVersion } from "./installation/version" +import { Slug } from "./util/slug" +import { ProjectTable } from "./project/sql" +import path from "path" +import { fromRow } from "./session/info" +import { SessionRunner } from "./session/runner/index" +import { SessionStore } from "./session/store" +import { SessionExecution } from "./session/execution" +import { makeGlobalNode } from "./effect/app-node" +import { LocationServiceMap } from "./location-service-map" +import { MessageDecodeError } from "./session/error" +import { SessionEvent } from "./session/event" +import { SessionInput } from "./session/input" +import { Snapshot } from "./snapshot" +import { SessionRevert } from "./session/revert" +import { Revert } from "@opencode-ai/schema/revert" +import { FSUtil } from "./fs-util" +import { SessionDurable } from "@opencode-ai/schema/durable-event-manifest" + +export const RevertState = Revert.State +export type RevertState = Revert.State + +// get project -> project.locations +// +// get all sessions +// + +// - by project +// - by subpath +// - by workspace (home is special) + +export { ListAnchor } + +const ListInputBase = { + workspaceID: WorkspaceV2.ID.pipe(Schema.optional), + search: Schema.String.pipe(Schema.optional), + limit: PositiveInt.pipe(Schema.optional), + order: Schema.Literals(["asc", "desc"]).pipe(Schema.optional), + anchor: ListAnchor.pipe(Schema.optional), +} + +const ListDirectoryInput = Schema.Struct({ + ...ListInputBase, + directory: AbsolutePath, +}) + +const ListProjectInput = Schema.Struct({ + ...ListInputBase, + project: ProjectV2.ID, + subpath: RelativePath.pipe(Schema.optional), +}) + +const ListAllInput = Schema.Struct(ListInputBase) + +export const ListInput = Schema.Union([ListDirectoryInput, ListProjectInput, ListAllInput]) +export type ListInput = typeof ListInput.Type + +type CreateInput = { + id?: SessionSchema.ID + agent?: AgentV2.ID + model?: ModelV2.Ref + location: Location.Ref +} + +type CompactInput = { + sessionID: SessionSchema.ID + prompt?: Prompt +} + +export class NotFoundError extends Schema.TaggedErrorClass()("Session.NotFoundError", { + sessionID: SessionSchema.ID, +}) {} + +export class OperationUnavailableError extends Schema.TaggedErrorClass()( + "Session.OperationUnavailableError", + { + operation: Schema.Literals(["move", "shell", "skill", "switchAgent", "compact", "wait"]), + }, +) {} + +export { ContextSnapshotDecodeError, MessageDecodeError } from "./session/error" + +export class PromptConflictError extends Schema.TaggedErrorClass()("Session.PromptConflictError", { + sessionID: SessionSchema.ID, + messageID: SessionMessage.ID, +}) {} +export const MessageNotFoundError = SessionRevert.MessageNotFoundError +export type MessageNotFoundError = SessionRevert.MessageNotFoundError + +export type Error = NotFoundError | MessageDecodeError | OperationUnavailableError | PromptConflictError + +export interface Interface { + readonly list: (input?: ListInput) => Effect.Effect + readonly create: (input: CreateInput) => Effect.Effect + readonly get: (sessionID: SessionSchema.ID) => Effect.Effect + readonly messages: (input: { + sessionID: SessionSchema.ID + limit?: number + order?: "asc" | "desc" + cursor?: { + id: SessionMessage.ID + direction: "previous" | "next" + } + }) => Effect.Effect + readonly message: (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + }) => Effect.Effect + readonly context: ( + sessionID: SessionSchema.ID, + ) => Effect.Effect + readonly events: (input: { + sessionID: SessionSchema.ID + after?: number + }) => Stream.Stream + readonly history: (input: { + sessionID: SessionSchema.ID + after?: number + limit: number + }) => Effect.Effect<{ events: ReadonlyArray; hasMore: boolean }, NotFoundError> + readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect + readonly switchModel: (input: { + sessionID: SessionSchema.ID + model: ModelV2.Ref + }) => Effect.Effect + readonly prompt: (input: { + id?: SessionMessage.ID + sessionID: SessionSchema.ID + prompt: PromptInput.Prompt + delivery?: SessionInput.Delivery + resume?: boolean + }) => Effect.Effect + readonly shell: (input: { + id?: EventV2.ID + sessionID: SessionSchema.ID + command: string + resume?: boolean + }) => Effect.Effect + readonly skill: (input: { + id?: EventV2.ID + sessionID: SessionSchema.ID + skill: string + resume?: boolean + }) => Effect.Effect + readonly compact: (input: CompactInput) => Effect.Effect + readonly wait: (id: SessionSchema.ID) => Effect.Effect + readonly active: Effect.Effect> + readonly resume: (sessionID: SessionSchema.ID) => Effect.Effect + readonly interrupt: (sessionID: SessionSchema.ID) => Effect.Effect + readonly revert: { + readonly stage: (input: { + sessionID: SessionSchema.ID + messageID: SessionMessage.ID + files?: boolean + }) => Effect.Effect + readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect + readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/v2/Session") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const database = yield* Database.Service + const db = database.db + const events = yield* EventV2.Service + const projects = yield* ProjectV2.Service + const execution = yield* SessionExecution.Service + const store = yield* SessionStore.Service + const locations = yield* LocationServiceMap.Service + const decodeMessage = Schema.decodeUnknownEffect(SessionMessage.Message) + const isDurableSessionEvent = Schema.is(SessionEvent.Durable) + const decode = (row: typeof SessionMessageTable.$inferSelect) => + decodeMessage({ ...row.data, id: row.id, type: row.type }).pipe( + Effect.mapError( + () => + new MessageDecodeError({ + sessionID: SessionSchema.ID.make(row.session_id), + messageID: SessionMessage.ID.make(row.id), + }), + ), + ) + + const result = Service.of({ + create: Effect.fn("V2Session.create")(function* (input) { + const sessionID = input.id ?? SessionSchema.ID.create() + const recorded = yield* store.get(sessionID) + if (recorded) return recorded + const project = yield* projects.resolve(input.location.directory) + yield* db + .insert(ProjectTable) + .values({ id: project.id, worktree: project.directory, vcs: project.vcs?.type, sandboxes: [] }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie) + const now = Date.now() + const info = SessionV1.SessionInfo.make({ + id: sessionID, + slug: Slug.create(), + version: InstallationVersion, + projectID: project.id, + directory: input.location.directory, + path: path.relative(project.directory, input.location.directory).replaceAll("\\", "/"), + workspaceID: input.location.workspaceID ? WorkspaceV2.ID.make(input.location.workspaceID) : undefined, + title: `New session - ${new Date(now).toISOString()}`, + agent: input.agent, + model: input.model + ? { + id: ModelV2.ID.make(input.model.id), + providerID: input.model.providerID, + variant: input.model.variant, + } + : undefined, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: now, updated: now }, + }) + const projected = yield* events + .publish(SessionV1.Event.Created, { sessionID, info }, { location: input.location }) + .pipe( + Effect.as({ type: "created" } as const), + Effect.catchDefect((defect) => { + if (!(defect instanceof SessionProjector.SessionAlreadyProjected)) { + return Effect.die(defect) + } + // Concurrent creation lost the projection race. The existing Session identity wins. + return store + .get(sessionID) + .pipe( + Effect.flatMap((session) => + session ? Effect.succeed({ type: "existing", session } as const) : Effect.die(defect), + ), + ) + }), + ) + if (projected.type === "existing") return projected.session + // TODO: Restore recorded sessions onto replacement synchronized workspaces in a future API slice. + return yield* result.get(sessionID).pipe(Effect.orDie) + }), + get: Effect.fn("V2Session.get")(function* (sessionID) { + const session = yield* store.get(sessionID) + if (!session) return yield* new NotFoundError({ sessionID }) + return session + }), + list: Effect.fn("V2Session.list")(function* (input = {}) { + const direction = input.anchor?.direction ?? "next" + const requestedOrder = input.order ?? "desc" + const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder + const sortColumn = SessionTable.time_created + const conditions: SQL[] = [] + if ("directory" in input) conditions.push(eq(SessionTable.directory, input.directory)) + if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID)) + if ("project" in input) conditions.push(eq(SessionTable.project_id, input.project)) + if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`)) + if (input.anchor) { + conditions.push( + order === "asc" + ? or( + gt(sortColumn, input.anchor.time), + and(eq(sortColumn, input.anchor.time), gt(SessionTable.id, input.anchor.id)), + )! + : or( + lt(sortColumn, input.anchor.time), + and(eq(sortColumn, input.anchor.time), lt(SessionTable.id, input.anchor.id)), + )!, + ) + } + const query = db + .select() + .from(SessionTable) + .where(conditions.length > 0 ? and(...conditions) : undefined) + .orderBy( + order === "asc" ? asc(sortColumn) : desc(sortColumn), + order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id), + ) + const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( + Effect.orDie, + ) + return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row)) + }), + messages: Effect.fn("V2Session.messages")(function* (input) { + yield* result.get(input.sessionID) + const direction = input.cursor?.direction ?? "next" + const requestedOrder = input.order ?? "desc" + const order = direction === "previous" ? (requestedOrder === "asc" ? "desc" : "asc") : requestedOrder + const anchor = input.cursor + ? yield* db + .select({ seq: SessionMessageTable.seq }) + .from(SessionMessageTable) + .where( + and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)), + ) + .get() + .pipe(Effect.orDie) + : undefined + if (input.cursor && !anchor) return [] + const boundary = anchor + ? order === "asc" + ? gt(SessionMessageTable.seq, anchor.seq) + : lt(SessionMessageTable.seq, anchor.seq) + : undefined + const where = boundary + ? and(eq(SessionMessageTable.session_id, input.sessionID), boundary) + : eq(SessionMessageTable.session_id, input.sessionID) + const query = db + .select() + .from(SessionMessageTable) + .where(where) + .orderBy(order === "asc" ? asc(SessionMessageTable.seq) : desc(SessionMessageTable.seq)) + const rows = yield* (input.limit === undefined ? query.all() : query.limit(input.limit).all()).pipe( + Effect.orDie, + ) + return yield* Effect.forEach(direction === "previous" ? rows.toReversed() : rows, decode) + }), + message: Effect.fn("V2Session.message")(function* (input) { + const stored = yield* store.message(input.messageID) + return stored?.sessionID === input.sessionID ? stored.message : undefined + }), + context: Effect.fn("V2Session.context")(function* (sessionID) { + yield* result.get(sessionID) + return yield* store.context(sessionID) + }), + events: (input) => + Stream.unwrap( + result + .get(input.sessionID) + .pipe(Effect.as(events.durable({ aggregateID: input.sessionID, after: input.after }))), + ).pipe(Stream.filter((event): event is SessionEvent.DurableEvent => isDurableSessionEvent(event))), + history: Effect.fn("V2Session.history")(function* (input) { + yield* result.get(input.sessionID) + return yield* EventV2.readAggregate(db, { + ...input, + aggregateID: input.sessionID, + manifest: SessionDurable, + }) + }), + prompt: Effect.fn("V2Session.prompt")((input) => + Effect.uninterruptible( + Effect.gen(function* () { + yield* result.get(input.sessionID) + const prompt = resolvePrompt(input.prompt) + const messageID = input.id ?? SessionMessage.ID.create() + const delivery = input.delivery ?? "steer" + const expected = { sessionID: input.sessionID, messageID, prompt, delivery } + const admitted = yield* SessionInput.admit(db, events, { + id: messageID, + sessionID: input.sessionID, + prompt, + delivery, + }).pipe( + Effect.catchDefect((defect) => + defect instanceof SessionInput.LifecycleConflict + ? new PromptConflictError({ sessionID: input.sessionID, messageID }) + : Effect.die(defect), + ), + ) + if (!SessionInput.equivalent(admitted, expected)) + return yield* new PromptConflictError({ sessionID: input.sessionID, messageID }) + if (input.resume !== false) yield* execution.wake(admitted.sessionID) + return admitted + }), + ), + ), + shell: Effect.fn("V2Session.shell")(function* () { + return yield* new OperationUnavailableError({ operation: "shell" }) + }), + skill: Effect.fn("V2Session.skill")(function* () { + return yield* new OperationUnavailableError({ operation: "skill" }) + }), + switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) { + yield* result.get(input.sessionID) + yield* events.publish(SessionEvent.AgentSwitched, { + sessionID: input.sessionID, + messageID: SessionMessage.ID.create(), + timestamp: yield* DateTime.now, + agent: input.agent, + }) + }), + switchModel: Effect.fn("V2Session.switchModel")(function* (input) { + const session = yield* result.get(input.sessionID) + if ( + session.model?.providerID === input.model.providerID && + session.model.id === input.model.id && + (session.model.variant ?? "default") === (input.model.variant ?? "default") + ) + return + yield* events.publish(SessionEvent.ModelSwitched, { + sessionID: input.sessionID, + messageID: SessionMessage.ID.create(), + timestamp: yield* DateTime.now, + model: input.model, + }) + }), + compact: Effect.fn("V2Session.compact")(function* (input) { + yield* result.get(input.sessionID) + return yield* new OperationUnavailableError({ operation: "compact" }) + }), + wait: Effect.fn("V2Session.wait")(function* (sessionID) { + yield* result.get(sessionID) + return yield* new OperationUnavailableError({ operation: "wait" }) + }), + active: execution.active, + resume: Effect.fn("V2Session.resume")(function* (sessionID) { + yield* result.get(sessionID) + yield* execution.resume(sessionID) + }), + interrupt: Effect.fn("V2Session.interrupt")((sessionID) => + Effect.uninterruptible(execution.interrupt(sessionID)), + ), + revert: { + stage: Effect.fn("V2Session.revert.stage")(function* (input) { + const session = yield* result.get(input.sessionID) + return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe( + Effect.provideService(Database.Service, database), + Effect.provideService(EventV2.Service, events), + Effect.provide(locations.get(session.location)), + ) + }), + clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) { + const session = yield* result.get(sessionID) + yield* SessionRevert.clear(session).pipe( + Effect.provideService(EventV2.Service, events), + Effect.provide(locations.get(session.location)), + ) + }), + commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) { + const session = yield* result.get(sessionID) + yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events)) + }), + }, + }) + + return result + }), +) + +const resolvePrompt = (input: PromptInput.Prompt) => + Prompt.make({ + text: input.text, + agents: input.agents, + files: input.files?.map((file) => { + const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1] + const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri) + return { + ...file, + mime: dataMime ?? (target.endsWith("/") ? "application/x-directory" : FSUtil.mimeType(target)), + } + }), + }) + +export const node = makeGlobalNode({ + service: Service, + layer: layer.pipe(Layer.orDie), + deps: [ + Database.node, + EventV2.node, + ProjectV2.node, + SessionExecution.node, + SessionStore.node, + LocationServiceMap.node, + SessionProjector.node, + ], +}) diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts new file mode 100644 index 0000000000000000000000000000000000000000..f714633295a5879f2a99cb7934fa9b3c66cee3ab --- /dev/null +++ b/packages/core/src/session/compaction.ts @@ -0,0 +1,248 @@ +export * as SessionCompaction from "./compaction" + +import { LLM, LLMError, LLMEvent, Message, type LLMRequest, type Model } from "@opencode-ai/llm" +import { DateTime, Effect, Stream } from "effect" +import type { Config } from "../config" +import type { EventV2 } from "../event" +import { SessionEvent } from "./event" +import { SessionMessage } from "./message" +import { SessionSchema } from "./schema" +import { Token } from "../util/token" + +const DEFAULT_BUFFER = 20_000 +const DEFAULT_KEEP_TOKENS = 8_000 +const TOOL_OUTPUT_MAX_CHARS = 2_000 +const SUMMARY_OUTPUT_TOKENS = 4_096 +const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside