Compare commits

...
Author SHA1 Message Date
rekram1-node 62f73d1eb2 fix(core): allow plan writes from home location 2026-08-31 13:02:47 +00:00
opencode-agent[bot]andvimtor 33536da231 fix(core): commit undo before compaction (#46383)
Co-authored-by: vimtor <36263538+vimtor@users.noreply.github.com>
2026-08-31 17:03:51 +05:30
Shoubhit Dash e56ceed32b Revert "fix(tui): surface subagent permissions and questions" (#46376) 2026-08-31 15:41:46 +05:30
Shoubhit Dash b2c7246134 fix(core): refresh git references on daily activity (#45575) 2026-08-31 15:36:37 +05:30
Shoubhit Dash 9c39e75ce2 fix(tui): surface subagent permissions and questions (#44976) 2026-08-31 14:38:05 +05:30
opencode-agent[bot]andBrendonovich 0dad76e618 fix(ui): prevent menu items from shrinking (#46353)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-31 16:00:55 +08:00
Luke Parker 90fb6562ce fix(shell): bound post-exit pipe draining on all platforms (#46085) 2026-08-31 16:53:35 +10:00
opencode-agent[bot]andBrendonovich 174d263890 fix(app): save session titles on blur and add tab context menu (#46113)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-31 13:07:32 +08:00
opencode-agent[bot]andBrendonovich 5ec29e7a87 refactor(desktop): use password-only server authentication (#45958)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
2026-08-31 13:06:35 +08:00
Luke Parker 3c6b85acf7 fix(app): reveal pasted composer content with custom scrollbar (#46339) 2026-08-31 14:38:28 +10:00
Luke Parker 50e77f66fd fix(app): keep composer select all scoped to the editor (#46338) 2026-08-31 14:38:00 +10:00
Aiden Cline d484f070d1 fix(core): recover reads with non-breaking spaces (#45807) 2026-08-30 23:13:26 -05:00
Luke Parker 8890294bf0 fix(desktop): preserve Windows editing shortcuts (#46336) 2026-08-31 04:07:42 +00:00
Luke Parker a1925de0c1 fix(core): flush trailing stream chunks while providers pause (#46326) 2026-08-31 14:03:17 +10:00
Aiden Cline 24e826d06b fix(ai): validate Bedrock media data (#46333) 2026-08-30 22:58:58 -05:00
133 changed files with 1642 additions and 652 deletions
@@ -1,4 +1,4 @@
import { Effect, Schema } from "effect"
import { Effect, Encoding, Schema } from "effect"
import type { MediaPart } from "../../schema/index.js"
import { ProviderShared } from "../shared.js"
@@ -57,6 +57,16 @@ const documentBlock = (name: string, format: DocumentFormat, bytes: string): Doc
},
})
const mediaBase64 = Effect.fn("BedrockMedia.mediaBase64")(function* (part: MediaPart) {
const media = ProviderShared.normalizeMedia(part)
const bytes = yield* Effect.fromResult(Encoding.decodeBase64(media.base64)).pipe(
Effect.mapError((cause) =>
ProviderShared.invalidRequest("Bedrock Converse media data must be valid base64", cause),
),
)
return Encoding.encodeBase64(bytes)
})
// Route by MIME. Known image/document formats lower into a typed block; anything
// else fails with a clear error instead of silently degrading to a malformed
// document block. Image MIME types not in `IMAGE_FORMATS` (e.g. `image/svg+xml`)
@@ -66,8 +76,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
const mime = part.mediaType.toLowerCase()
const imageFormat = IMAGE_FORMATS[mime as keyof typeof IMAGE_FORMATS]
if (imageFormat) {
const media = ProviderShared.normalizeMedia(part)
return { image: { format: imageFormat, source: { bytes: media.base64 } } } satisfies ImageBlock
return { image: { format: imageFormat, source: { bytes: yield* mediaBase64(part) } } } satisfies ImageBlock
}
if (mime.startsWith("image/"))
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support image media type ${part.mediaType}`)
@@ -75,8 +84,7 @@ export const lower = Effect.fn("BedrockMedia.lower")(function* (part: MediaPart)
if (documentFormat) {
if (!part.filename)
return yield* ProviderShared.invalidRequest("Bedrock Converse document media requires a filename")
const media = ProviderShared.normalizeMedia(part)
return documentBlock(part.filename, documentFormat, media.base64)
return documentBlock(part.filename, documentFormat, yield* mediaBase64(part))
}
return yield* ProviderShared.invalidRequest(`Bedrock Converse does not support media type ${part.mediaType}`)
})
@@ -1429,6 +1429,20 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("rejects image media that is not valid base64", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({
model,
messages: [Message.user({ type: "media", mediaType: "image/png", data: "https://example.test/image.png" })],
}),
).pipe(Effect.flip)
expect(error).toMatchObject({ reason: { _tag: "InvalidRequest" } })
expect(error.message).toContain("Bedrock Converse media data must be valid base64")
}),
)
it.effect("lowers document media into Bedrock document blocks with format and name", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -1552,6 +1566,37 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("rejects remote media URLs in tool results", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([ToolCallPart.make({ id: "call_1", name: "read", input: {} })]),
Message.tool({
id: "call_1",
name: "read",
result: {
type: "content",
value: [
{
type: "file",
uri: "https://example.test/report.pdf",
mime: "application/pdf",
name: "report.pdf",
},
],
},
}),
],
}),
).pipe(Effect.flip)
expect(error).toMatchObject({ reason: { _tag: "InvalidRequest" } })
expect(error.message).toContain("Bedrock Converse media data must be valid base64")
}),
)
it.effect("rejects unsupported image media types", () =>
Effect.gen(function* () {
const error = yield* compileRequest(
@@ -1,5 +1,34 @@
import { expect, story } from "../../storybook/playwright/story"
for (const draft of ["empty-draft", "multiline-draft", "mixed-attachments"]) {
story(`select all stays inside the composer with ${draft}`, async ({ mount, page }) => {
const component = await mount(`opencode-composer-flow--${draft}`)
const input = component.getByRole("textbox", { name: "Prompt", exact: true })
const text = await input.textContent()
for (let count = 0; count < 2; count++) {
await input.press("ControlOrMeta+a")
expect(
await input.evaluate((editor) => {
const selection = window.getSelection()
return {
text: selection?.toString(),
inside: editor.contains(selection?.anchorNode ?? null) && editor.contains(selection?.focusNode ?? null),
}
}),
).toEqual({ text, inside: true })
}
await page.keyboard.type("Replacement draft")
await expect(input).toHaveText("Replacement draft")
await expect(component.getByRole("status")).toHaveText("Ready")
if (draft === "mixed-attachments") {
await expect(component.getByAltText("layout.png")).toBeVisible()
await expect(component.getByText("Keep the normal flow flat", { exact: true })).toBeVisible()
}
})
}
story("renders a draft once and supports editing, caret restoration, and failure recovery", async ({ mount, page }) => {
await page.addInitScript(() => {
const replace = Element.prototype.replaceChildren
@@ -1,4 +1,4 @@
import { expect, test } from "@playwright/test"
import { expect, test, type Locator } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"
@@ -65,11 +65,64 @@ for (const lines of [6000, 25000]) {
await expect.poll(async () => (await input.innerText()) === text).toBe(true)
expect(await events.evaluate((events) => events.count)).toBe(1)
await expect(input).toBeFocused()
await expectCaretVisible(input)
const scroll = page.locator('[data-component="composer-scroll"]')
await expect(scroll.locator(".scroll-view__viewport")).toHaveCSS("scrollbar-width", "none")
await expect(scroll.locator(".scroll-view__thumb")).toBeVisible()
await page.keyboard.type("!")
await expect.poll(async () => (await input.innerText()) === text + "!").toBe(true)
await expectCaretVisible(input)
const thumb = await scroll.locator(".scroll-view__thumb").boundingBox()
const bounds = await scroll.boundingBox()
if (!thumb || !bounds) throw new Error("Missing composer scrollbar bounds")
await page.mouse.move(thumb.x + thumb.width / 2, thumb.y + thumb.height / 2)
await page.mouse.down()
await page.mouse.move(thumb.x + thumb.width / 2, bounds.y + 8 + thumb.height / 2)
await page.mouse.up()
await expect(scroll.locator(".scroll-view__viewport")).toHaveJSProperty("scrollTop", 0)
await expect(input).toBeFocused()
await page.keyboard.press("ControlOrMeta+Home")
await page.keyboard.press("ControlOrMeta+End")
await expectCaretVisible(input)
})
}
async function expectCaretVisible(input: Locator) {
await expect
.poll(() =>
input.evaluate((element) => {
const selection = window.getSelection()
if (!selection?.isCollapsed || !selection.rangeCount || !element.contains(selection.anchorNode)) return false
const caret = selection.getRangeAt(0).getBoundingClientRect()
const viewport = (element.closest("[data-scrollable]") ?? element).getBoundingClientRect()
return caret.height > 0 && caret.top >= viewport.top - 1 && caret.bottom <= viewport.bottom + 1
}),
)
.toBe(true)
}
for (const width of [390, 1280]) {
for (const direction of ["ltr", "rtl"]) {
test(`reveals a multiline paste in the middle at ${width}px in ${direction}`, async ({ page }) => {
await page.setViewportSize({ width, height: 800 })
await page.evaluate((direction) => (document.documentElement.dir = direction), direction)
const input = page.getByRole("textbox", { name: "Prompt", exact: true })
const suffix = "\nExisting trailing content".repeat(100)
await input.fill("Before " + suffix)
await input.press("ControlOrMeta+Home")
await input.press("ArrowRight")
const text = "Pasted line /tmp/example.ts 123 \u0645\u0631\u062d\u0628\u0627\n".repeat(100) + "End of paste"
await page.evaluate((text) => navigator.clipboard.writeText(text), text)
await page.keyboard.press("ControlOrMeta+V")
await expect.poll(() => input.innerText()).toBe("B" + text + "efore " + suffix)
await expectCaretVisible(input)
await page.keyboard.type("!")
await expect.poll(() => input.innerText()).toBe("B" + text + "!efore " + suffix)
await expectCaretVisible(input)
})
}
}
for (const text of [
"single line <b> &amp;",
"first\nsecond",
@@ -32,12 +32,13 @@ test("server dialog keeps focus above fullscreen settings", async ({ page }) =>
const editor = page.getByRole("dialog", { name: "Add server" })
await expect(editor.getByPlaceholder("http://localhost:4096")).toBeFocused()
const username = editor.getByPlaceholder("username")
await expect(editor.getByPlaceholder("username")).toHaveCount(0)
const name = editor.getByPlaceholder("Localhost", { exact: true })
const password = editor.getByPlaceholder("password")
await username.click()
await expect(username).toBeFocused()
await username.fill("kit")
await expect(username).toHaveValue("kit")
await name.click()
await expect(name).toBeFocused()
await name.fill("Remote")
await expect(name).toHaveValue("Remote")
await page.keyboard.press("Tab")
await expect(password).toBeFocused()
await password.fill("secret")
@@ -126,6 +126,10 @@ for (const create of [false, true]) {
json: [
{ directory: canonical, strategy: null },
{ directory: "/projects/existing-worktree", strategy: "git" },
...Array.from({ length: 20 }, (_, index) => ({
directory: `/projects/worktree-${index}`,
strategy: "git",
})),
],
headers,
})
@@ -161,7 +165,9 @@ for (const create of [false, true]) {
await expect(page.getByText("Recover my worktree", { exact: true })).toBeVisible()
await expect(page.getByText("Session location unavailable", { exact: true })).toBeVisible()
listing.resolve()
await expect(page.getByRole("menuitem", { name: "existing-worktree", exact: true })).toBeVisible()
const existing = page.getByRole("menuitem", { name: "existing-worktree", exact: true })
await expect(existing).toBeVisible()
await expect(existing).toHaveCSS("height", "28px")
await page.keyboard.press("Escape")
await expect(page.getByRole("menu")).toHaveCount(0)
listing = Promise.withResolvers<void>()
@@ -0,0 +1,130 @@
import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
test.beforeEach(async ({ page }) => {
const sessions = fixture.sessions.map((session) => ({ ...session }))
await mockOpenCodeServer(page, {
sessions,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
})
await page.route("**/api/session/*/rename", async (route) => {
if (route.request().method() !== "POST") return route.fallback()
const id = new URL(route.request().url()).pathname.split("/").at(-2)
const session = sessions.find((item) => item.id === id)
const payload: unknown = route.request().postDataJSON()
if (
!session ||
!payload ||
typeof payload !== "object" ||
!("title" in payload) ||
typeof payload.title !== "string"
)
throw new Error("Invalid rename request")
session.title = payload.title
await route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
})
await page.goto("/")
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.targetTitle }).click()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
for (const commit of ["Enter", "blur", "click outside"]) {
test(`saves the session heading on ${commit}`, async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await expect(input).toBeFocused()
await input.fill("Renamed session")
if (commit === "Enter") await input.press("Enter")
if (commit === "blur") await input.press("Tab")
if (commit === "click outside") await page.locator('[data-component="composer-editor"]').click()
await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible()
await expect(page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed session" })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible()
})
}
test("cancels the session heading with Escape", async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill("Discard this title")
await input.press("Escape")
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
test("keeps the draft when saving the session heading fails", async ({ page }) => {
await page.route("**/api/session/*/rename", (route) =>
route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } }),
)
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill("Retry this title")
await input.press("Tab")
await expect(page.getByText("Request failed", { exact: true })).toBeVisible()
await expect(input).toBeEnabled()
await expect(input).toHaveValue("Retry this title")
await expect(
page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle }),
).toBeVisible()
})
test("does not save an empty session heading", async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill(" ")
await input.press("Tab")
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})
test("renames and closes the session tab from its context menu", async ({ page }) => {
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeHidden()
await expect(tab).toBeFocused()
await tab.press("Shift+F10")
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(input).toBeFocused()
await input.fill("Renamed from tab")
await input.press("Enter")
await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible()
const renamed = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed from tab" })
await renamed.click({ button: "right" })
await page.getByRole("menuitem", { name: "Close tab", exact: true }).click()
await expect(renamed).toBeHidden()
await page.getByRole("button", { name: "Home", exact: true }).click()
await expect(
page.locator('[data-component="home-session-row"]').filter({ hasText: "Renamed from tab" }),
).toBeVisible()
})
test("renames an inactive tab without switching sessions", async ({ page }) => {
await page.getByRole("button", { name: "Home", exact: true }).click()
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.sourceTitle }).click()
await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible()
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(input).toBeFocused()
await input.fill("Inactive tab renamed")
await input.press("Tab")
await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible()
await expect(page).toHaveURL(new RegExp(`/session/${fixture.sourceID}$`))
await page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Inactive tab renamed" }).click()
await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible()
})
@@ -141,7 +141,7 @@ test("shows the not found fallback when the viewed session is deleted", async ({
})
await expect(page.getByText("This session cannot be found")).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab" })).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab", exact: true })).toBeVisible()
await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0)
})
+26 -4
View File
@@ -9,6 +9,7 @@ import { Button } from "@opencode-ai/ui/button"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
import { AttachmentCard } from "@opencode-ai/session-ui/attachment-card"
import { CommentCard } from "@opencode-ai/session-ui/comment-card"
import { typeLabel } from "@opencode-ai/session-ui/message-file"
@@ -53,6 +54,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
const state = props.controller.state
const view = props.controller.view
let editor: HTMLDivElement | undefined
let viewport: HTMLDivElement | undefined
let localInput = false
const updateCursor = () => {
if (!editor || !window.getSelection()?.isCollapsed) return
@@ -145,7 +147,14 @@ export function ComposerEditor(props: ComposerEditorProps) {
/>
</Show>
<div class="relative min-h-[60px]">
<ScrollView
data-component="composer-scroll"
class="min-h-[60px] max-h-[180px]"
viewportRef={(element) => {
viewport = element
element.tabIndex = -1
}}
>
<div
ref={(element) => {
editor = element
@@ -162,7 +171,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
spellcheck={state.mode === "normal"}
// @ts-expect-error
autocomplete="off"
class="relative z-10 block min-h-[60px] max-h-[180px] w-full overflow-y-auto whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
class="relative z-10 block min-h-[60px] w-full whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
classList={{ "font-mono!": state.mode === "shell", "opacity-50": props.disabled }}
style={{
"unicode-bidi": state.mode === "normal" ? "plaintext" : undefined,
@@ -190,7 +199,20 @@ export function ComposerEditor(props: ComposerEditorProps) {
}}
onKeyUp={updateCursor}
onPointerUp={updateCursor}
onPaste={props.controller.onPaste}
onPaste={(event) => {
props.controller.onPaste(event)
// Programmatic multiline insertion does not reliably reveal the caret.
requestAnimationFrame(() => {
const selection = window.getSelection()
if (!editor || !viewport || !selection?.isCollapsed || !selection.rangeCount) return
if (!editor.contains(selection.anchorNode)) return
const caret = selection.getRangeAt(0).getBoundingClientRect()
if (!caret.height) return
const bounds = viewport.getBoundingClientRect()
if (caret.bottom > bounds.bottom - 8) viewport.scrollTop += caret.bottom - bounds.bottom + 8
if (caret.top < bounds.top + 8) viewport.scrollTop += caret.top - bounds.top - 8
})
}}
onFocus={() => props.controller.dispatch({ type: "focus.editor" })}
/>
<Show when={!props.controller.value()}>
@@ -206,7 +228,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
: i18n.t("ui.promptInput.placeholder.normal", { slash: "/", at: "@" }))}
</div>
</Show>
</div>
</ScrollView>
<div class="flex h-11 items-center px-2">
<div
-3
View File
@@ -414,8 +414,6 @@ export const dict = {
"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": "አገልጋይ አርትዕ",
@@ -498,7 +496,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ዴስክቶፕ {{version}}",
"wsl.onboarding.versionMismatch": "የተጫነው ሥሪት ከዴስክቶፕ መተግበሪያ ሥሪት ጋር አይዛመድም።",
"wsl.onboarding.adding": "በማከል ላይ...",
"server.row.noUsername": "የተጠቃሚ ስም የለም",
"dialog.project.edit.title": "ፕሮጀክት አርትዕ",
"dialog.project.edit.name": "ስም",
"dialog.project.edit.icon": "አዶ",
-3
View File
@@ -420,7 +420,6 @@ export const dict = {
"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": "الخادم الافتراضي",
@@ -1024,9 +1023,7 @@ export const dict = {
"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...",
-3
View File
@@ -421,8 +421,6 @@ export const dict = {
"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",
@@ -509,7 +507,6 @@ export const dict = {
"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...",
"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",
-3
View File
@@ -421,8 +421,6 @@ export const dict = {
"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": "Редактиране на сървъра",
@@ -508,7 +506,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "работен плот {{version}}",
"wsl.onboarding.versionMismatch": "Инсталираната версия не съответства на версията на настолното приложение.",
"wsl.onboarding.adding": "Добавяне...",
"server.row.noUsername": "няма потребителско име",
"dialog.project.edit.title": "Редактиране на проекта",
"dialog.project.edit.name": "Име",
"dialog.project.edit.icon": "Икона",
-3
View File
@@ -418,8 +418,6 @@ export const dict: Record<string, string> = {
"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": "সার্ভার সম্পাদনা করুন",
@@ -505,7 +503,6 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ডেস্কটপ {{version}}",
"wsl.onboarding.versionMismatch": "ইনস্টল করা সংস্করণ ডেস্কটপ অ্যাপ সংস্করণের সাথে মেলে না।",
"wsl.onboarding.adding": "যোগ করা হচ্ছে...",
"server.row.noUsername": "ব্যবহারকারীর নাম নেই",
"dialog.project.edit.title": "প্রকল্প সম্পাদনা করুন",
"dialog.project.edit.name": "নাম",
"dialog.project.edit.icon": "আইকন",
-3
View File
@@ -422,7 +422,6 @@ export const dict = {
"dialog.server.add.button": "Adicionar servidor",
"dialog.server.add.name": "Nome do servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nome de usuário (opcional)",
"dialog.server.add.password": "Senha (opcional)",
"dialog.server.edit.title": "Editar servidor",
"dialog.server.default.title": "Servidor padrão",
@@ -1035,9 +1034,7 @@ export const dict = {
"app.server.unreachable": "Não foi possível conectar a {{server}}",
"app.server.retrying": "Tentando novamente automaticamente...",
"app.server.otherServers": "Outros servidores",
"dialog.server.add.usernamePlaceholder": "nome de usuário",
"dialog.server.add.passwordPlaceholder": "senha",
"server.row.noUsername": "sem nome de usuário",
"session.review.noVcs.createGit.title": "Criar um repositório Git",
"session.review.noVcs.createGit.description": "Rastreie, revise e desfaça alterações neste projeto",
"session.review.noVcs.createGit.actionLoading": "Criando repositório Git...",
-3
View File
@@ -449,7 +449,6 @@ export const dict = {
"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",
@@ -1113,9 +1112,7 @@ export const dict = {
"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...",
-3
View File
@@ -420,8 +420,6 @@ export const dict = {
"dialog.server.add.button": "Afegeix servidor",
"dialog.server.add.name": "Nom del servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nom d'usuari (opcional)",
"dialog.server.add.usernamePlaceholder": "nom d'usuari",
"dialog.server.add.password": "Contrasenya (opcional)",
"dialog.server.add.passwordPlaceholder": "contrasenya",
"dialog.server.edit.title": "Edita el servidor",
@@ -507,7 +505,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "escriptori {{version}}",
"wsl.onboarding.versionMismatch": "La versió instal·lada no coincideix amb la versió de l'aplicació d'escriptori.",
"wsl.onboarding.adding": "S'està afegint...",
"server.row.noUsername": "sense nom d'usuari",
"dialog.project.edit.title": "Edita el projecte",
"dialog.project.edit.name": "Nom",
"dialog.project.edit.icon": "Icona",
-3
View File
@@ -418,8 +418,6 @@ export const dict = {
"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",
@@ -505,7 +503,6 @@ export const dict = {
"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í...",
"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",
-3
View File
@@ -346,7 +346,6 @@ export const dict = {
"dialog.server.add.button": "Tilføj server",
"dialog.server.add.name": "Servernavn (valgfrit)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Brugernavn (valgfrit)",
"dialog.server.add.password": "Adgangskode (valgfrit)",
"dialog.server.edit.title": "Rediger server",
"dialog.server.default.title": "Standardserver",
@@ -1003,9 +1002,7 @@ export const dict = {
"app.server.unreachable": "Kunne ikke nå {{server}}",
"app.server.retrying": "Prøver igen automatisk...",
"app.server.otherServers": "Andre servere",
"dialog.server.add.usernamePlaceholder": "brugernavn",
"dialog.server.add.passwordPlaceholder": "adgangskode",
"server.row.noUsername": "intet brugernavn",
"session.review.noVcs.createGit.title": "Opret et Git-repository",
"session.review.noVcs.createGit.description": "Spor, gennemgå og fortryd ændringer i dette projekt",
"session.review.noVcs.createGit.actionLoading": "Opretter Git-repository...",
-3
View File
@@ -327,7 +327,6 @@ export const dict = {
"dialog.server.add.button": "Server hinzufügen",
"dialog.server.add.name": "Servername (optional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Benutzername (optional)",
"dialog.server.add.password": "Passwort (optional)",
"dialog.server.edit.title": "Server bearbeiten",
"dialog.server.default.title": "Standardserver",
@@ -945,9 +944,7 @@ export const dict = {
"app.server.unreachable": "Konnte {{server}} nicht erreichen",
"app.server.retrying": "Verbindung wird automatisch erneut hergestellt…",
"app.server.otherServers": "Andere Server",
"dialog.server.add.usernamePlaceholder": "Benutzername",
"dialog.server.add.passwordPlaceholder": "Passwort",
"server.row.noUsername": "Kein Benutzername",
"session.review.noVcs.createGit.title": "Git-Repository erstellen",
"session.review.noVcs.createGit.description":
"Änderungen in diesem Projekt verfolgen, überprüfen und rückgängig machen",
-3
View File
@@ -424,8 +424,6 @@ export const dict = {
"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": "އެޑިޓް ސަރވަރ",
@@ -511,7 +509,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ޑެސްކްޓޮޕް {{version}} އެވެ",
"wsl.onboarding.versionMismatch": "އިންސްޓޯލް ކޮށްފައިވާ ވަރޝަން ޑެސްކްޓޮޕް އެޕް ވަރޝަން އާއި އެއްގޮތެއް ނުވެއެވެ.",
"wsl.onboarding.adding": "އިތުރުކުރަނީ...",
"server.row.noUsername": "ޔޫޒަރނޭމް އެއް ނެތެވެ",
"dialog.project.edit.title": "އެޑިޓް ޕްރޮޖެކްޓް",
"dialog.project.edit.name": "ނަން",
"dialog.project.edit.icon": "އައިކޮން",
-3
View File
@@ -423,8 +423,6 @@ export const dict: Record<string, string> = {
"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": "སར་བར་ཞུན་དག་འབད།",
@@ -510,7 +508,6 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ཌེཀསི་ཊོཔ་ {{version}}",
"wsl.onboarding.versionMismatch": "གཞི་བཙུགས་འབད་ཡོད་པའི་ཐོན་རིམ་འདི་ ཌེཀསི་ཊོཔ་ཨེཔ་ཐོན་རིམ་དང་མཐུན་སྒྲིག་མི་འབད།",
"wsl.onboarding.adding": "ཁ་སྐོང་བརྐྱབ་དོ།",
"server.row.noUsername": "སྤྱོད་མིང་མེད།",
"dialog.project.edit.title": "ཞུན་དག་ལས་གཞི།",
"dialog.project.edit.name": "མིང",
"dialog.project.edit.icon": "ངོས་དཔར།",
-3
View File
@@ -420,8 +420,6 @@ export const dict = {
"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": "Επεξεργασία διακομιστή",
@@ -507,7 +505,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "επιτραπέζιος υπολογιστής {{version}}",
"wsl.onboarding.versionMismatch": "Η εγκατεστημένη έκδοση δεν ταιριάζει με την έκδοση της εφαρμογής για υπολογιστές.",
"wsl.onboarding.adding": "Προσθήκη...",
"server.row.noUsername": "χωρίς όνομα χρήστη",
"dialog.project.edit.title": "Επεξεργασία έργου",
"dialog.project.edit.name": "Όνομα",
"dialog.project.edit.icon": "Εικονίδιο",
+1 -4
View File
@@ -363,9 +363,7 @@ export const dict = {
"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.password": "Password",
"dialog.server.add.passwordPlaceholder": "password",
"dialog.server.edit.title": "Edit server",
"dialog.server.default.title": "Default server",
@@ -453,7 +451,6 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Installed version does not match the desktop app version.",
"wsl.onboarding.adding": "Adding...",
"server.row.noUsername": "no username",
"server.row.incompatible":
"This server is running OpenCode {{version}}, which isn't compatible with this app. Upgrade it to OpenCode V2 to continue.",
-3
View File
@@ -450,7 +450,6 @@ export const dict = {
"dialog.server.add.button": "Añadir servidor",
"dialog.server.add.name": "Nombre del servidor (opcional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Nombre de usuario (opcional)",
"dialog.server.add.password": "Contraseña (opcional)",
"dialog.server.edit.title": "Editar servidor",
"dialog.server.default.title": "Servidor predeterminado",
@@ -1118,9 +1117,7 @@ export const dict = {
"app.server.unreachable": "No se pudo conectar con {{server}}",
"app.server.retrying": "Reintentando automáticamente...",
"app.server.otherServers": "Otros servidores",
"dialog.server.add.usernamePlaceholder": "usuario",
"dialog.server.add.passwordPlaceholder": "contraseña",
"server.row.noUsername": "sin usuario",
"session.review.noVcs.createGit.title": "Crear repositorio Git",
"session.review.noVcs.createGit.description": "Rastrea, revisa y deshaz cambios en este proyecto",
"session.review.noVcs.createGit.actionLoading": "Creando repositorio Git...",
-3
View File
@@ -417,8 +417,6 @@ export const dict = {
"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",
@@ -503,7 +501,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "töölaud {{version}}",
"wsl.onboarding.versionMismatch": "Installitud versioon ei ühti töölauarakenduse versiooniga.",
"wsl.onboarding.adding": "Lisamine...",
"server.row.noUsername": "kasutajanime pole",
"dialog.project.edit.title": "Redigeeri projekti",
"dialog.project.edit.name": "Nimi",
"dialog.project.edit.icon": "Ikoon",
-3
View File
@@ -418,8 +418,6 @@ export const dict = {
"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": "ویرایش سرور",
@@ -505,7 +503,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "دسکتاپ {{version}}",
"wsl.onboarding.versionMismatch": "نسخه نصب شده با نسخه برنامه دسکتاپ مطابقت ندارد.",
"wsl.onboarding.adding": "در حال افزودن...",
"server.row.noUsername": "بدون نام کاربری",
"dialog.project.edit.title": "ویرایش پروژه",
"dialog.project.edit.name": "نام",
"dialog.project.edit.icon": "نماد",
-3
View File
@@ -325,8 +325,6 @@ export const dict = {
"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",
@@ -412,7 +410,6 @@ export const dict = {
"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",
-3
View File
@@ -417,8 +417,6 @@ export const dict = {
"dialog.server.add.button": "Legg ambætara til",
"dialog.server.add.name": "Ambætaranavn (valfrítt)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Brúkaranavn (valfrítt)",
"dialog.server.add.usernamePlaceholder": "brúkaranavn",
"dialog.server.add.password": "Loyniorð (valfrítt)",
"dialog.server.add.passwordPlaceholder": "loyniorð",
"dialog.server.edit.title": "Rætta ambætara",
@@ -504,7 +502,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "skriviborð {{version}}",
"wsl.onboarding.versionMismatch": "Innsetta útgávan passar ikki til skriviborðsappiútgávuna.",
"wsl.onboarding.adding": "Leggi til...",
"server.row.noUsername": "einki brúkaranavn",
"dialog.project.edit.title": "Rætta verkætlan",
"dialog.project.edit.name": "Navn",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -426,7 +426,6 @@ export const dict = {
"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",
@@ -1044,9 +1043,7 @@ export const dict = {
"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...",
-3
View File
@@ -416,8 +416,6 @@ export const dict = {
"dialog.server.add.button": "הוסף שרת",
"dialog.server.add.name": "שם שרת (אופציונלי)",
"dialog.server.add.namePlaceholder": "מארח מקומי",
"dialog.server.add.username": "שם משתמש (אופציונלי)",
"dialog.server.add.usernamePlaceholder": "שם משתמש",
"dialog.server.add.password": "סיסמה (אופציונלי)",
"dialog.server.add.passwordPlaceholder": "סיסמה",
"dialog.server.edit.title": "ערוך שרת",
@@ -501,7 +499,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "גרסת שולחן העבודה: {{version}}",
"wsl.onboarding.versionMismatch": "הגרסה המותקנת אינה תואמת את גרסת האפליקציה לשולחן העבודה.",
"wsl.onboarding.adding": "מוסיף...",
"server.row.noUsername": "ללא שם משתמש",
"dialog.project.edit.title": "ערוך פרויקט",
"dialog.project.edit.name": "שם",
"dialog.project.edit.icon": "סמל",
-3
View File
@@ -425,8 +425,6 @@ export const dict = {
"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": "सर्वर संपादित करें",
@@ -512,7 +510,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "डेस्कटॉप {{version}}",
"wsl.onboarding.versionMismatch": "इंस्टॉल किया गया संस्करण डेस्कटॉप ऐप संस्करण से मेल नहीं खाता।",
"wsl.onboarding.adding": "जोड़ा जा रहा है...",
"server.row.noUsername": "कोई उपयोगकर्ता नाम नहीं",
"dialog.project.edit.title": "प्रोजेक्ट संपादित करें",
"dialog.project.edit.name": "नाम",
"dialog.project.edit.icon": "आइकन",
-3
View File
@@ -422,8 +422,6 @@ export const dict = {
"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",
@@ -509,7 +507,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "radna površina {{version}}",
"wsl.onboarding.versionMismatch": "Instalirana verzija ne odgovara verziji aplikacije za stolna računala.",
"wsl.onboarding.adding": "Dodavanje...",
"server.row.noUsername": "nema korisničkog imena",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.name": "Ime",
"dialog.project.edit.icon": "Ikona",
-3
View File
@@ -422,8 +422,6 @@ export const dict = {
"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",
@@ -509,7 +507,6 @@ export const dict = {
"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...",
"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",
-3
View File
@@ -420,8 +420,6 @@ export const dict = {
"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": "Խմբագրել սերվերը",
@@ -507,7 +505,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "աշխատասեղանի {{version}}",
"wsl.onboarding.versionMismatch": "Տեղադրված տարբերակը չի համապատասխանում աշխատասեղանի հավելվածի տարբերակին։",
"wsl.onboarding.adding": "Ավելացվում է...",
"server.row.noUsername": "առանց օգտվողի անուն",
"dialog.project.edit.title": "Խմբագրել նախագիծը",
"dialog.project.edit.name": "Անուն",
"dialog.project.edit.icon": "Պատկերակ",
-3
View File
@@ -453,8 +453,6 @@ export const dict = {
"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",
@@ -542,7 +540,6 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Versi terinstal tidak cocok dengan versi aplikasi desktop.",
"wsl.onboarding.adding": "Menambahkan...",
"server.row.noUsername": "tanpa nama pengguna",
"dialog.project.edit.title": "Sunting proyek",
"dialog.project.edit.name": "Nama",
-3
View File
@@ -422,8 +422,6 @@ export const dict = {
"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",
@@ -509,7 +507,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "skrifborð {{version}}",
"wsl.onboarding.versionMismatch": "Uppsett útgáfa passar ekki við útgáfu skrifborðsforritsins.",
"wsl.onboarding.adding": "Bætir við...",
"server.row.noUsername": "ekkert notendanafn",
"dialog.project.edit.title": "Breyta verkefni",
"dialog.project.edit.name": "Nafn",
"dialog.project.edit.icon": "Táknmynd",
-3
View File
@@ -327,8 +327,6 @@ export const dict = {
"dialog.server.add.button": "Aggiungi server",
"dialog.server.add.name": "Nome del server (facoltativo)",
"dialog.server.add.namePlaceholder": "Host locale",
"dialog.server.add.username": "Nome utente (facoltativo)",
"dialog.server.add.usernamePlaceholder": "nomeutente",
"dialog.server.add.password": "Password (facoltativa)",
"dialog.server.add.passwordPlaceholder": "password",
"dialog.server.edit.title": "Modifica server",
@@ -414,7 +412,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "La versione installata non corrisponde alla versione dell'app desktop.",
"wsl.onboarding.adding": "Aggiunta...",
"server.row.noUsername": "nessun nome utente",
"dialog.project.edit.title": "Modifica progetto",
"dialog.project.edit.name": "Nome",
"dialog.project.edit.icon": "Icona",
-3
View File
@@ -419,7 +419,6 @@ export const dict = {
"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": "デフォルトサーバー",
@@ -1019,9 +1018,7 @@ export const dict = {
"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 リポジトリを作成中...",
-3
View File
@@ -418,8 +418,6 @@ export const dict = {
"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": "სერვერის რედაქტირება",
@@ -505,7 +503,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "დესკტოპის {{version}}",
"wsl.onboarding.versionMismatch": "დაინსტალირებული ვერსია არ ემთხვევა დესკტოპის აპის ვერსიას.",
"wsl.onboarding.adding": "დამატება...",
"server.row.noUsername": "მომხმარებლის სახელის გარეშე",
"dialog.project.edit.title": "პროექტის რედაქტირება",
"dialog.project.edit.name": "სახელი",
"dialog.project.edit.icon": "ხატულა",
-3
View File
@@ -417,8 +417,6 @@ export const dict = {
"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": "កែសម្រួលម៉ាស៊ីនមេ",
@@ -504,7 +502,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ផ្ទៃតុ {{version}}",
"wsl.onboarding.versionMismatch": "កំណែដែលបានដំឡើងមិនត្រូវគ្នានឹងកំណែកម្មវិធីកុំព្យូទ័រលើតុទេ។",
"wsl.onboarding.adding": "កំពុងបន្ថែម...",
"server.row.noUsername": "គ្មានឈ្មោះអ្នកប្រើប្រាស់",
"dialog.project.edit.title": "កែសម្រួលគម្រោង",
"dialog.project.edit.name": "ឈ្មោះ",
"dialog.project.edit.icon": "រូបតំណាង",
-3
View File
@@ -308,7 +308,6 @@ export const dict = {
"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": "기본 서버",
@@ -768,9 +767,7 @@ export const dict = {
"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 저장소 생성 중...",
-3
View File
@@ -417,8 +417,6 @@ export const dict = {
"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": "ແກ້ໄຂເຊີບເວີ",
@@ -503,7 +501,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ເດັສທັອບ {{version}}",
"wsl.onboarding.versionMismatch": "ເວີຊັນທີ່ຕິດຕັ້ງບໍ່ກົງກັບເວີຊັນຂອງແອັບ desktop.",
"wsl.onboarding.adding": "ກຳລັງເພີ່ມ...",
"server.row.noUsername": "ບໍ່ມີຊື່ຜູ້ໃຊ້",
"dialog.project.edit.title": "ແກ້ໄຂໂຄງການ",
"dialog.project.edit.name": "ຊື່",
"dialog.project.edit.icon": "ໄອຄອນ",
-3
View File
@@ -423,8 +423,6 @@ export const dict = {
"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į",
@@ -510,7 +508,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "darbalaukis {{version}}",
"wsl.onboarding.versionMismatch": "Įdiegta versija neatitinka darbalaukio programos versijos.",
"wsl.onboarding.adding": "Pridedama...",
"server.row.noUsername": "jokio vartotojo vardo",
"dialog.project.edit.title": "Redaguoti projektą",
"dialog.project.edit.name": "Vardas",
"dialog.project.edit.icon": "Piktograma",
-3
View File
@@ -418,8 +418,6 @@ export const dict = {
"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",
@@ -505,7 +503,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "darbvirsmas {{version}}",
"wsl.onboarding.versionMismatch": "Instalētā versija neatbilst darbvirsmas lietotnes versijai.",
"wsl.onboarding.adding": "Pievieno...",
"server.row.noUsername": "nav lietotājvārda",
"dialog.project.edit.title": "Rediģēt projektu",
"dialog.project.edit.name": "Nosaukums",
"dialog.project.edit.icon": "Ikona",
-3
View File
@@ -419,8 +419,6 @@ export const dict = {
"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": "Уреди сервер",
@@ -506,7 +504,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "работна површина {{version}}",
"wsl.onboarding.versionMismatch": "Инсталираната верзија не се совпаѓа со верзијата на апликацијата за десктоп.",
"wsl.onboarding.adding": "Се додава...",
"server.row.noUsername": "нема корисничко име",
"dialog.project.edit.title": "Уреди проект",
"dialog.project.edit.name": "Име",
"dialog.project.edit.icon": "Икона",
-3
View File
@@ -421,8 +421,6 @@ export const dict = {
"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": "Сервер засах",
@@ -508,7 +506,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ширээний компьютер {{version}}",
"wsl.onboarding.versionMismatch": "Суулгасан хувилбар нь ширээний програмын хувилбартай таарахгүй байна.",
"wsl.onboarding.adding": "Нэмэж байна...",
"server.row.noUsername": "хэрэглэгчийн нэр байхгүй",
"dialog.project.edit.title": "Төслийг засварлах",
"dialog.project.edit.name": "Нэр",
"dialog.project.edit.icon": "Дүрс",
-3
View File
@@ -418,8 +418,6 @@ export const dict = {
"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",
@@ -505,7 +503,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versi yang dipasang tidak sepadan dengan versi aplikasi desktop.",
"wsl.onboarding.adding": "Sedang menambah...",
"server.row.noUsername": "tiada nama pengguna",
"dialog.project.edit.title": "Edit projek",
"dialog.project.edit.name": "Nama",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -422,8 +422,6 @@ export const dict = {
"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": "ဆာဗာကို တည်းဖြတ်ပါ။",
@@ -509,7 +507,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ဒက်စ်တော့ {{version}}",
"wsl.onboarding.versionMismatch": "ထည့်သွင်းထားသောဗားရှင်းသည် ဒက်စ်တော့အက်ပ်ဗားရှင်းနှင့် မကိုက်ညီပါ။",
"wsl.onboarding.adding": "ထည့်နေသည်...",
"server.row.noUsername": "အသုံးပြုသူအမည်မရှိပါ။",
"dialog.project.edit.title": "ပရောဂျက်ကို တည်းဖြတ်ပါ။",
"dialog.project.edit.name": "အမည်",
"dialog.project.edit.icon": "သင်္ကေတ",
-3
View File
@@ -419,8 +419,6 @@ export const dict: Record<string, string> = {
"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": "सर्भर सम्पादन गर्नुहोस्",
@@ -506,7 +504,6 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "डेस्कटप {{version}}",
"wsl.onboarding.versionMismatch": "स्थापना गरिएको संस्करण डेस्कटप एप संस्करणसँग मेल खाँदैन।",
"wsl.onboarding.adding": "थप्दै...",
"server.row.noUsername": "प्रयोगकर्ता नाम छैन",
"dialog.project.edit.title": "परियोजना सम्पादन गर्नुहोस्",
"dialog.project.edit.name": "नाम",
"dialog.project.edit.icon": "आइकन",
-3
View File
@@ -419,8 +419,6 @@ export const dict = {
"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",
@@ -506,7 +504,6 @@ export const dict = {
"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...",
"server.row.noUsername": "geen gebruikersnaam",
"dialog.project.edit.title": "Project bewerken",
"dialog.project.edit.name": "Naam",
"dialog.project.edit.icon": "Pictogram",
-3
View File
@@ -439,7 +439,6 @@ export const dict = {
"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",
@@ -963,9 +962,7 @@ export const dict = {
"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...",
-3
View File
@@ -425,8 +425,6 @@ export const dict = {
"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": "سرور وچ ترمیم کرو",
@@ -512,7 +510,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ڈیسک ٹاپ {{version}}",
"wsl.onboarding.versionMismatch": "انسٹال کیتا گیا ورژن ڈیسک ٹاپ ایپ ورژن نال میل نئیں کھاندا۔",
"wsl.onboarding.adding": "شامل کر رہیا واں...",
"server.row.noUsername": "کوئی ورتن آلا ناں نئیں",
"dialog.project.edit.title": "پروجیکٹ وچ ترمیم کرو",
"dialog.project.edit.name": "ناں",
"dialog.project.edit.icon": "آئکن",
-3
View File
@@ -422,7 +422,6 @@ export const dict = {
"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",
@@ -1036,9 +1035,7 @@ export const dict = {
"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...",
-3
View File
@@ -417,8 +417,6 @@ export const dict = {
"dialog.server.add.button": "Adaugă server",
"dialog.server.add.name": "Nume server (opțional)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Utilizator (opțional)",
"dialog.server.add.usernamePlaceholder": "utilizator",
"dialog.server.add.password": "Parolă (opțional)",
"dialog.server.add.passwordPlaceholder": "parolă",
"dialog.server.edit.title": "Editează server",
@@ -504,7 +502,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versiunea instalată nu corespunde cu versiunea aplicației desktop.",
"wsl.onboarding.adding": "Se adaugă...",
"server.row.noUsername": "fără nume de utilizator",
"dialog.project.edit.title": "Editează proiectul",
"dialog.project.edit.name": "Nume",
"dialog.project.edit.icon": "Pictogramă",
-3
View File
@@ -447,7 +447,6 @@ export const dict = {
"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": "Сервер по умолчанию",
@@ -1113,9 +1112,7 @@ export const dict = {
"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...",
-3
View File
@@ -417,8 +417,6 @@ export const dict: Record<string, string> = {
"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": "සේවාදායකය සංස්කරණය කරන්න",
@@ -504,7 +502,6 @@ export const dict: Record<string, string> = {
"wsl.onboarding.desktopVersion": "ඩෙස්ක්ටොප් {{version}}",
"wsl.onboarding.versionMismatch": "ස්ථාපිත අනුවාදය ඩෙස්ක්ටොප් යෙදුම් අනුවාදයට නොගැලපේ.",
"wsl.onboarding.adding": "එකතු කරමින්...",
"server.row.noUsername": "පරිශීලක නාමයක් නැත",
"dialog.project.edit.title": "ව්‍යාපෘතිය සංස්කරණය කරන්න",
"dialog.project.edit.name": "නම",
"dialog.project.edit.icon": "නිරූපකය",
-3
View File
@@ -417,8 +417,6 @@ export const dict = {
"dialog.server.add.button": "Pridať server",
"dialog.server.add.name": "Názov servera (voliteľné)",
"dialog.server.add.namePlaceholder": "Localhost",
"dialog.server.add.username": "Používateľské meno (voliteľné)",
"dialog.server.add.usernamePlaceholder": "používateľ",
"dialog.server.add.password": "Heslo (voliteľné)",
"dialog.server.add.passwordPlaceholder": "heslo",
"dialog.server.edit.title": "Upraviť server",
@@ -504,7 +502,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Nainštalovaná verzia sa nezhoduje s verziou desktopovej aplikácie.",
"wsl.onboarding.adding": "Pridáva sa...",
"server.row.noUsername": "bez používateľského mena",
"dialog.project.edit.title": "Upraviť projekt",
"dialog.project.edit.name": "Názov",
"dialog.project.edit.icon": "Ikona",
-3
View File
@@ -417,8 +417,6 @@ export const dict = {
"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",
@@ -504,7 +502,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "namizje {{version}}",
"wsl.onboarding.versionMismatch": "Nameščena različica se ne ujema z različico namizne aplikacije.",
"wsl.onboarding.adding": "Dodajanje ...",
"server.row.noUsername": "brez uporabniškega imena",
"dialog.project.edit.title": "Uredi projekt",
"dialog.project.edit.name": "Ime",
"dialog.project.edit.icon": "Ikona",
-3
View File
@@ -419,8 +419,6 @@ export const dict = {
"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",
@@ -505,7 +503,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Versioni i instaluar nuk përputhet me versionin e aplikacionit desktop.",
"wsl.onboarding.adding": "Po shtohet...",
"server.row.noUsername": "asnjë emër përdoruesi",
"dialog.project.edit.title": "Redakto projektin",
"dialog.project.edit.name": "Emri",
"dialog.project.edit.icon": "Ikona",
-3
View File
@@ -418,8 +418,6 @@ export const dict = {
"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": "Уреди сервер",
@@ -505,7 +503,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "десктоп {{version}}",
"wsl.onboarding.versionMismatch": "Инсталирана верзија не одговара верзији апликације за рачунар.",
"wsl.onboarding.adding": "Додавање...",
"server.row.noUsername": "нема корисничког имена",
"dialog.project.edit.title": "Уреди пројекат",
"dialog.project.edit.name": "Назив",
"dialog.project.edit.icon": "Икона",
-3
View File
@@ -419,8 +419,6 @@ export const dict = {
"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",
@@ -506,7 +504,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "desktop {{version}}",
"wsl.onboarding.versionMismatch": "Den installerade versionen matchar inte versionen av skrivbordsappen.",
"wsl.onboarding.adding": "Lägger till...",
"server.row.noUsername": "inget användarnamn",
"dialog.project.edit.title": "Redigera projekt",
"dialog.project.edit.name": "Namn",
"dialog.project.edit.icon": "Ikon",
-3
View File
@@ -420,8 +420,6 @@ export const dict = {
"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": "Серверро таҳрир кунед",
@@ -507,7 +505,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "мизи корӣ {{version}}",
"wsl.onboarding.versionMismatch": "Версияи насбшуда ба версияи барномаи мизи корӣ мувофиқат намекунад.",
"wsl.onboarding.adding": "Илова кардан...",
"server.row.noUsername": "номи корбар нест",
"dialog.project.edit.title": "Таҳрири лоиҳа",
"dialog.project.edit.name": "Ном",
"dialog.project.edit.icon": "Нишона",
-3
View File
@@ -446,7 +446,6 @@ export const dict = {
"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": "เซิร์ฟเวอร์เริ่มต้น",
@@ -1097,9 +1096,7 @@ export const dict = {
"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 รีโพซิทอรี...",
-3
View File
@@ -418,8 +418,6 @@ export const dict = {
"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äň",
@@ -504,7 +502,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "iş stoly {{version}}",
"wsl.onboarding.versionMismatch": "Gurlan wersiýa iş stoly programma wersiýasyna gabat gelenok.",
"wsl.onboarding.adding": "Goşmak ...",
"server.row.noUsername": "ulanyjy ady ýok",
"dialog.project.edit.title": "Taslamany redaktirläň",
"dialog.project.edit.name": "Ady",
"dialog.project.edit.icon": "Nyşan",
-3
View File
@@ -453,7 +453,6 @@ export const dict = {
"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",
@@ -1120,9 +1119,7 @@ export const dict = {
"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...",
-3
View File
@@ -454,8 +454,6 @@ export const dict = {
"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": "Редагувати сервер",
@@ -543,7 +541,6 @@ export const dict = {
"wsl.onboarding.versionMismatch": "Встановлена версія не відповідає версії десктопного застосунку.",
"wsl.onboarding.adding": "Додавання...",
"server.row.noUsername": "без імені користувача",
"dialog.project.edit.title": "Редагувати проєкт",
"dialog.project.edit.name": "Назва",
-3
View File
@@ -427,8 +427,6 @@ export const dict = {
"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": "سرور میں ترمیم کریں۔",
@@ -514,7 +512,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ڈیسک ٹاپ {{version}}",
"wsl.onboarding.versionMismatch": "انسٹال کردہ ورژن ڈیسک ٹاپ ایپ ورژن سے مماثل نہیں ہے۔",
"wsl.onboarding.adding": "شامل کیا جا رہا ہے...",
"server.row.noUsername": "کوئی صارف نام نہیں",
"dialog.project.edit.title": "پروجیکٹ میں ترمیم کریں۔",
"dialog.project.edit.name": "نام",
"dialog.project.edit.icon": "آئیکن",
-3
View File
@@ -420,8 +420,6 @@ export const dict = {
"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",
@@ -507,7 +505,6 @@ export const dict = {
"wsl.onboarding.desktopVersion": "ish stoli {{version}}",
"wsl.onboarding.versionMismatch": "Oʻrnatilgan versiya ish stoli ilovasi versiyasiga mos kelmaydi.",
"wsl.onboarding.adding": "Qo'shilmoqda...",
"server.row.noUsername": "foydalanuvchi nomi yo'q",
"dialog.project.edit.title": "Loyihani tahrirlash",
"dialog.project.edit.name": "Ism",
"dialog.project.edit.icon": "Belgi",
-3
View File
@@ -425,8 +425,6 @@ export const dict = {
"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ủ",
@@ -511,7 +509,6 @@ export const dict = {
"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...",
"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",
-3
View File
@@ -466,7 +466,6 @@ export const dict = {
"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": "默认服务器",
@@ -1089,9 +1088,7 @@ export const dict = {
"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 仓库...",
-3
View File
@@ -446,7 +446,6 @@ export const dict = {
"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": "預設伺服器",
@@ -1085,9 +1084,7 @@ export const dict = {
"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 儲存庫...",
+11 -5
View File
@@ -2,12 +2,13 @@ import { describe, expect, test } from "bun:test"
import { authFromToken, authTokenFromCredentials } from "./api"
describe("authFromToken", () => {
test("decodes basic auth credentials from auth_token", () => {
expect(authFromToken(btoa("kit:secret"))).toEqual({ username: "kit", password: "secret" })
test("extracts only the password from auth_token", () => {
expect(authFromToken(btoa("opencode:secret"))).toEqual({ password: "secret" })
})
test("defaults blank username to opencode", () => {
expect(authFromToken(btoa(":secret"))).toEqual({ username: "opencode", password: "secret" })
test("ignores legacy usernames and preserves colons in passwords", () => {
expect(authFromToken(btoa("legacy:secret:with:colons"))).toEqual({ password: "secret:with:colons" })
expect(authFromToken(btoa(":secret"))).toEqual({ password: "secret" })
})
test("ignores malformed tokens", () => {
@@ -17,7 +18,12 @@ describe("authFromToken", () => {
})
describe("authTokenFromCredentials", () => {
test("encodes credentials with the default username", () => {
test("encodes credentials with the fixed username", () => {
expect(authTokenFromCredentials({ password: "secret" })).toBe(btoa("opencode:secret"))
})
test("ignores usernames in legacy saved credentials", () => {
const credentials = { username: "legacy", password: "secret" }
expect(authTokenFromCredentials(credentials)).toBe(btoa("opencode:secret"))
})
})
+2 -4
View File
@@ -2,8 +2,8 @@ import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import type { ServerConnection } from "@/runtime/server/registry"
import { decode64 } from "@/runtime/persistence/base64"
export function authTokenFromCredentials(input: { username?: string; password: string }) {
return btoa(`${input.username ?? "opencode"}:${input.password}`)
export function authTokenFromCredentials(input: { password: string }) {
return btoa(`opencode:${input.password}`)
}
export function authFromToken(token: string | null) {
@@ -12,7 +12,6 @@ export function authFromToken(token: string | null) {
const separator = decoded.indexOf(":")
if (separator === -1) return
return {
username: decoded.slice(0, separator) || "opencode",
password: decoded.slice(separator + 1),
}
}
@@ -27,7 +26,6 @@ export function createApiForServer(input: {
headers: input.server.password
? {
Authorization: `Basic ${authTokenFromCredentials({
username: input.server.username,
password: input.server.password,
})}`,
}
@@ -93,7 +93,7 @@ test("rotates HTTP and PTY clients together", async () => {
return Response.json({ healthy: true, version: "2.0.0-test", pid: 1 })
}) as typeof globalThis.fetch
const transport = createServerTransport({
http: { url: "http://127.0.0.1:4100", username: "opencode", password: "first" },
http: { url: "http://127.0.0.1:4100", password: "first" },
fetch,
})
const initialPty = transport.pty
@@ -101,7 +101,6 @@ test("rotates HTTP and PTY clients together", async () => {
await transport.api.health.get()
const replacement = transport.update({
url: "http://127.0.0.1:4200",
username: "opencode",
password: "second",
})
await transport.api.health.get()
+19 -4
View File
@@ -13,6 +13,18 @@ function abortFromInput(input: RequestInfo | URL, init?: RequestInit) {
}
describe("checkServerHealth", () => {
test.each([undefined, "secret"])("authenticates using only the password (%s)", async (password) => {
const headers: Array<string | null> = []
const fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
headers.push(new Headers(init?.headers).get("authorization"))
return Response.json({ healthy: true, version: "2.0.0" })
}) as typeof globalThis.fetch
const legacy = { ...server, username: "legacy", password }
expect(await checkServerHealth(legacy, fetch)).toEqual({ healthy: true, version: "2.0.0" })
expect(headers).toEqual([password ? `Basic ${btoa(`opencode:${password}`)}` : null])
})
test("returns healthy response with version", async () => {
let request: URL | undefined
const fetch = (async (input: RequestInfo | URL) => {
@@ -34,10 +46,13 @@ describe("checkServerHealth", () => {
const fetch = (async (input: RequestInfo | URL) => {
const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input)
requests.push(url.pathname)
return new Response(JSON.stringify(url.pathname === "/global/health" ? { version: "1.18.15" } : { healthy: true }), {
status: 200,
headers: { "content-type": "application/json" },
})
return new Response(
JSON.stringify(url.pathname === "/global/health" ? { version: "1.18.15" } : { healthy: true }),
{
status: 200,
headers: { "content-type": "application/json" },
},
)
}) as unknown as typeof globalThis.fetch
const result = await checkServerHealth(server, fetch)
+2 -2
View File
@@ -24,7 +24,7 @@ const healthCache = new Map<
>()
function cacheKey(server: ServerConnection.HttpBase) {
return `${server.url}\n${server.username ?? ""}\n${server.password ?? ""}`
return `${server.url}\n${server.password ?? ""}`
}
function timeoutSignal(timeoutMs: number) {
@@ -80,7 +80,7 @@ export async function checkServerHealth(
const retryDelayMs = opts?.retryDelayMs ?? defaultRetryDelayMs
const headers = server.password
? {
Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`,
Authorization: `Basic ${authTokenFromCredentials({ password: server.password })}`,
}
: undefined
const next = (count: number, error: unknown) => {
@@ -7,7 +7,7 @@ function setup(
) {
const requests: Array<{ url: URL; init?: RequestInit }> = []
const api = createApiForServer({
server: { url: "https://server.example:4096", username: "image-user", password: "secret" },
server: { url: "https://server.example:4096", password: "secret" },
fetch: (async (input: string | URL | Request, init?: RequestInit) => {
requests.push({ url: new URL(input instanceof Request ? input.url : input), init })
return respond(init)
@@ -38,7 +38,7 @@ describe("readLocalImage", () => {
expect(requests[0].url.pathname).toBe(`/api/fs/read/${encoded}`)
expect([...requests[0].url.searchParams]).toEqual([["location[directory]", directory]])
expect(requests[0].init?.method).toBe("GET")
expect(new Headers(requests[0].init?.headers).get("authorization")).toBe(`Basic ${btoa("image-user:secret")}`)
expect(new Headers(requests[0].init?.headers).get("authorization")).toBe(`Basic ${btoa("opencode:secret")}`)
expect(requests[0].init?.signal).toBe(signal)
})
@@ -1,8 +1,46 @@
import { describe, expect, test } from "bun:test"
import { createRoot, createSignal } from "solid-js"
import { migrateCanonicalLocalServerState, resolveServerList, ServerConnection } from "./registry"
import {
migrateCanonicalLocalServerState,
migrateServerAuthState,
resolveServerList,
ServerConnection,
} from "./registry"
import { ServerScope } from "@/runtime/server/scope"
describe("migrateServerAuthState", () => {
test("removes legacy usernames without changing passwords or other saved state", () => {
const state = {
list: [
"http://localhost:4096",
{ url: "https://flat.example", username: "legacy", password: "first" },
{
type: "http",
displayName: "Remote",
http: { url: "https://nested.example", username: "legacy", password: "second" },
},
],
projects: { local: [{ worktree: "/project", expanded: true }] },
}
expect(migrateServerAuthState(state)).toEqual({
...state,
list: [
"http://localhost:4096",
{ url: "https://flat.example", password: "first" },
{ type: "http", displayName: "Remote", http: { url: "https://nested.example", password: "second" } },
],
})
expect(state.list[1]).toHaveProperty("username", "legacy")
expect(migrateServerAuthState(migrateServerAuthState(state))).toEqual(migrateServerAuthState(state))
})
test("preserves absent or malformed lists", () => {
expect(migrateServerAuthState(undefined)).toBeUndefined()
expect(migrateServerAuthState({ projects: {} })).toEqual({ projects: {} })
expect(migrateServerAuthState({ list: [null, 1, {}] })).toEqual({ list: [null, 1, {}] })
})
})
describe("resolveServerList", () => {
test("lets startup auth_token credentials override a persisted same-url server", () => {
const list = resolveServerList({
@@ -13,7 +51,6 @@ describe("resolveServerList", () => {
authToken: true,
http: {
url: "https://server.example.test",
username: "opencode",
password: "secret",
},
},
@@ -24,7 +61,6 @@ describe("resolveServerList", () => {
expect(list[0]?.type).toBe("http")
expect(list[0]?.http).toEqual({
url: "https://server.example.test",
username: "opencode",
password: "secret",
})
expect(list[0]?.type === "http" ? list[0].authToken : false).toBe(true)
@@ -36,7 +72,6 @@ describe("resolveServerList", () => {
stored: [
{
url: "https://server.example.test",
username: "opencode",
password: "saved",
},
],
@@ -47,7 +82,6 @@ describe("resolveServerList", () => {
expect(list[0]?.type).toBe("http")
expect(list[0]?.http).toEqual({
url: "https://server.example.test",
username: "opencode",
password: "saved",
})
expect(list[0]?.type === "http" ? list[0].authToken : true).toBeUndefined()
+16 -2
View File
@@ -42,6 +42,21 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
export function migrateServerAuthState(value: unknown) {
if (!isRecord(value) || !Array.isArray(value.list)) return value
return {
...value,
list: value.list.map((server) => {
if (!isRecord(server)) return server
const http = isRecord(server.http) ? server.http : server
if (!("username" in http)) return server
const next = { ...http }
delete next.username
return http === server ? next : { ...server, http: next }
}),
}
}
export function migrateCanonicalLocalServerState(value: unknown, canonicalLocalServer?: ServerConnection.Key) {
if (!canonicalLocalServer || canonicalLocalServer === "local") return value
if (!isRecord(value)) return value
@@ -194,7 +209,6 @@ export namespace ServerConnection {
export type HttpBase = {
url: string
username?: string
password?: string
}
@@ -266,7 +280,7 @@ export const { use: useServers, provider: ServersProvider } = createSimpleContex
...Persist.global("server"),
sync: true,
previousKey: "server.v3",
migrate: (value) => migrateCanonicalLocalServerState(value, props.canonicalLocalServer),
migrate: (value) => migrateCanonicalLocalServerState(migrateServerAuthState(value), props.canonicalLocalServer),
},
createStore({
list: [] as StoredServer[],
+14 -37
View File
@@ -18,8 +18,6 @@ import { useTabs } from "@/shell/tabs/tabs"
import { useCheckServerHealth } from "@/runtime/server/health"
import "@/settings/settings.css"
const DEFAULT_USERNAME = "opencode"
type FormMode = "list" | "add" | "edit"
export const DialogServer: Component<{
@@ -103,33 +101,18 @@ export const DialogServer: Component<{
onKeyDown={keyDown}
/>
</div>
<div class="grid w-full min-w-0 grid-cols-2 gap-4">
<div class="flex min-w-0 flex-col gap-2">
<label class="settings-server-dialog-label">{language.t("dialog.server.add.username")}</label>
<TextInput
type="text"
appearance="large"
class="!w-full self-stretch"
value={form.state.username()}
placeholder={language.t("dialog.server.add.usernamePlaceholder")}
disabled={form.state.busy()}
onInput={(event) => form.change.username(event.currentTarget.value)}
onKeyDown={keyDown}
/>
</div>
<div class="flex min-w-0 flex-col gap-2">
<label class="settings-server-dialog-label">{language.t("dialog.server.add.password")}</label>
<TextInput
type="password"
appearance="large"
class="!w-full self-stretch"
value={form.state.password()}
placeholder={language.t("dialog.server.add.passwordPlaceholder")}
disabled={form.state.busy()}
onInput={(event) => form.change.password(event.currentTarget.value)}
onKeyDown={keyDown}
/>
</div>
<div class="flex w-full min-w-0 flex-col gap-2">
<label class="settings-server-dialog-label">{language.t("dialog.server.add.password")}</label>
<TextInput
type="password"
appearance="large"
class="!w-full self-stretch"
value={form.state.password()}
placeholder={language.t("dialog.server.add.passwordPlaceholder")}
disabled={form.state.busy()}
onInput={(event) => form.change.password(event.currentTarget.value)}
onKeyDown={keyDown}
/>
</div>
</div>
</DialogBody>
@@ -155,7 +138,7 @@ function createFormController(options: { onSelect?: () => void } = {}) {
const [store, setStore] = createStore({
mode: "list" as FormMode,
originalUrl: undefined as string | undefined,
values: { url: "", name: "", username: DEFAULT_USERNAME, password: "" },
values: { url: "", name: "", password: "" },
error: "",
status: undefined as boolean | undefined,
})
@@ -167,7 +150,7 @@ function createFormController(options: { onSelect?: () => void } = {}) {
setStore({
mode: "list",
originalUrl: undefined,
values: { url: "", name: "", username: DEFAULT_USERNAME, password: "" },
values: { url: "", name: "", password: "" },
error: "",
status: undefined,
})
@@ -197,13 +180,11 @@ function createFormController(options: { onSelect?: () => void } = {}) {
const original = store.mode === "edit" ? editing() : undefined
if (store.mode === "edit" && !original) return
const name = store.values.name.trim() || undefined
const username = store.values.username || undefined
const password = store.values.password || undefined
if (
original?.type === "http" &&
normalized === original.http.url &&
name === original.displayName &&
username === original.http.username &&
password === original.http.password
) {
reset()
@@ -215,7 +196,6 @@ function createFormController(options: { onSelect?: () => void } = {}) {
displayName: name,
http: {
url: normalized,
username: store.mode === "add" && !password ? undefined : username,
password,
},
}
@@ -256,7 +236,6 @@ function createFormController(options: { onSelect?: () => void } = {}) {
values: {
url: connection.http.url,
name: connection.displayName ?? "",
username: connection.http.username ?? "",
password: connection.http.password ?? "",
},
error: "",
@@ -283,7 +262,6 @@ function createFormController(options: { onSelect?: () => void } = {}) {
busy: () => request.isPending,
value: () => store.values.url,
name: () => store.values.name,
username: () => store.values.username,
password: () => store.values.password,
error: () => store.error,
status: () => store.status,
@@ -291,7 +269,6 @@ function createFormController(options: { onSelect?: () => void } = {}) {
change: {
value: (value: string) => change("url", value),
name: (value: string) => change("name", value),
username: (value: string) => change("username", value),
password: (value: string) => change("password", value),
},
start: { add: startAdd, edit: startEdit },
@@ -10,9 +10,19 @@ function deferred<T>() {
return { promise, resolve }
}
const values = (url: string): ServerFormValues => ({ url, name: "", username: "opencode", password: "" })
const values = (url: string): ServerFormValues => ({ url, name: "", password: "" })
describe("createServerHealthPreview", () => {
test.each(["", "secret"])("previews a server with only its password (%s)", async (password) => {
const requests: ServerConnection.HttpBase[] = []
const preview = createServerHealthPreview(async (http) => {
requests.push(http)
return { healthy: true }
})
await preview.preview({ ...values("server.example.com"), password }, () => {})
expect(requests).toEqual([{ url: "http://server.example.com", ...(password ? { password } : {}) }])
})
test("ignores an older response that resolves after the latest response", async () => {
const first = deferred<{ healthy: boolean }>()
const second = deferred<{ healthy: boolean }>()
@@ -4,7 +4,6 @@ import type { ServerHealth } from "@/runtime/server/health"
export type ServerFormValues = {
url: string
name: string
username: string
password: string
}
@@ -28,7 +27,6 @@ export function createServerHealthPreview(
return
const http: ServerConnection.HttpBase = { url: normalized }
if (values.username) http.username = values.username
if (values.password) http.password = values.password
const result = await check(http)
if (current !== generation) return
+2 -18
View File
@@ -11,7 +11,6 @@ import {
type ParentProps,
Show,
} from "solid-js"
import { useLanguage } from "@/runtime/i18n/language"
import { type ServerConnection, serverName } from "@/runtime/server/registry"
import type { ServerHealth } from "@/runtime/server/health"
@@ -27,7 +26,6 @@ interface ServerRowProps extends ParentProps {
}
export function ServerRow(props: ServerRowProps) {
const language = useLanguage()
const [truncated, setTruncated] = createSignal(false)
let nameRef: HTMLSpanElement | undefined
let versionRef: HTMLSpanElement | undefined
@@ -94,22 +92,8 @@ export function ServerRow(props: ServerRowProps) {
{(badge) => badge()}
</Show>
</div>
<Show when={props.showCredentials && props.conn.type === "http" && props.conn}>
{(conn) => (
<div class="flex flex-row gap-3">
<span>
<Show
when={conn().http.username}
fallback={<span class="text-text-weaker">{language.t("server.row.noUsername")}</span>}
>
<span class="text-text-weak">{conn().http.username}</span>
</Show>
</span>
<Show when={conn().http.password}>
<span class="text-text-weak"></span>
</Show>
</div>
)}
<Show when={props.showCredentials && props.conn.type === "http" && props.conn.http.password}>
<span class="text-text-weak"></span>
</Show>
</div>
{props.children}
+1 -3
View File
@@ -30,9 +30,7 @@ function readyState(input: Partial<WslServersState> = {}): WslServersState {
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: "ready", url: "http://127.0.0.1:4096", password: null })).toBe(false)
expect(wslRuntimeRetryable({ kind: "failed", message: "boom" })).toBe(true)
expect(wslRuntimeRetryable({ kind: "stopped" })).toBe(true)
})
+1 -1
View File
@@ -39,7 +39,7 @@ export type WslServerConfig = {
export type WslServerRuntime =
| { kind: "starting" }
| { kind: "ready"; url: string; username: string | null; password: string | null }
| { kind: "ready"; url: string; password: string | null }
| { kind: "failed"; message: string }
| { kind: "stopped" }
@@ -485,6 +485,7 @@ function MessageTimelineView(
}
const saveTitleEditor = async () => {
if (!title.editing || props.pending.rename()) return
if (await props.action.rename(title.draft)) setTitle("editing", false)
}
@@ -634,6 +635,7 @@ function MessageTimelineView(
onInput={(event) => setTitle("draft", event.currentTarget.value)}
onKeyDown={(event) => {
event.stopPropagation()
if (event.isComposing || event.keyCode === 229) return
if (event.key === "Enter") {
event.preventDefault()
void saveTitleEditor()
@@ -644,7 +646,7 @@ function MessageTimelineView(
closeTitleEditor()
}
}}
onBlur={closeTitleEditor}
onBlur={() => void saveTitleEditor()}
/>
</Show>
</Show>
+3 -10
View File
@@ -112,16 +112,9 @@ export const SettingsServers: Component = () => {
<ServerHealthIndicator health={health()} />
<div class="settings-servers-copy">
<span class="settings-servers-name">{serverName(item)}</span>
<span class="settings-servers-meta">
<Show when={health()?.version}>v{health()?.version}</Show>
<Show when={health()?.version && item.type === "http"}> </Show>
<Show
when={item.type === "http" && item.http.username}
fallback={<Show when={item.type === "http"}>{language.t("server.row.noUsername")}</Show>}
>
{item.http.username}
</Show>
</span>
<Show when={health()?.version}>
<span class="settings-servers-meta">v{health()?.version}</span>
</Show>
</div>
</div>
<div class="settings-servers-actions">
+52 -21
View File
@@ -1,9 +1,11 @@
import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { createMutation } from "@tanstack/solid-query"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon } from "@opencode-ai/ui/icon"
import { Menu } from "@opencode-ai/ui/menu"
import { useGlobal, useServerCtx } from "@/runtime/server/runtime"
import { useLanguage } from "@/runtime/i18n/language"
import { ServerConnection, serverName, useServers } from "@/runtime/server/registry"
@@ -34,6 +36,8 @@ export function TabNavItem(props: {
hidden?: boolean
orientation?: "horizontal" | "vertical"
}) {
const language = useLanguage()
const [menu, setMenu] = createStore({ open: false, rename: false })
const [editing, setEditing] = createSignal(false)
const [titleOverflowing, setTitleOverflowing] = createSignal(false)
let tabRoot!: HTMLDivElement
@@ -77,7 +81,7 @@ export function TabNavItem(props: {
})
const [popoverOpen, setPopoverOpen] = createSignal(false)
const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session
const previewBlocked = () => !!props.dragging || editing() || menu.open || !!props.pressed || !props.session
const measureTitleOverflow = () => {
if (!titleEl || editing()) {
@@ -141,9 +145,9 @@ export function TabNavItem(props: {
titleEl.textContent = value
})
const openRename = (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
const openRename = (event?: MouseEvent) => {
event?.preventDefault()
event?.stopPropagation()
if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
const session = props.session
if (!session) return
@@ -174,7 +178,7 @@ export function TabNavItem(props: {
onCleanup(cleanup)
})
const tab = (
const tab = () => (
<div
ref={(el) => {
tabRoot = el
@@ -200,7 +204,11 @@ export function TabNavItem(props: {
closeTab(event)
}}
>
<a
<Menu.Context.Trigger
as="a"
disabled={editing() || props.dragging}
aria-haspopup="menu"
aria-expanded={menu.open}
data-slot="tab-link"
data-titlebar-tab-link
href={props.href}
@@ -288,7 +296,7 @@ export function TabNavItem(props: {
</span>
)}
</Show>
</a>
</Menu.Context.Trigger>
<div data-slot="tab-close">
<IconButton
@@ -301,27 +309,50 @@ export function TabNavItem(props: {
}}
onClick={closeTab}
icon={<Icon name="xmark-small" />}
aria-label={language.t("common.closeTab")}
/>
</div>
</div>
)
return (
<TabPreviewPopover
trigger={tab}
orientation={props.orientation}
open={popoverOpen() && !previewBlocked()}
onOpenChange={(value) => {
if (value && previewBlocked()) return
setPopoverOpen(value)
<Menu.Context
onOpenChange={(open) => {
setMenu("open", open)
if (open) setPopoverOpen(false)
}}
data={{
projectName: projectName(),
title: props.session?.title,
path: previewPath(),
serverName: serverLabel(),
}}
/>
>
<TabPreviewPopover
trigger={tab()}
orientation={props.orientation}
open={popoverOpen() && !previewBlocked()}
onOpenChange={(value) => {
if (value && previewBlocked()) return
setPopoverOpen(value)
}}
data={{
projectName: projectName(),
title: props.session?.title,
path: previewPath(),
serverName: serverLabel(),
}}
/>
<Menu.Context.Portal>
<Menu.Context.Content
onCloseAutoFocus={(event) => {
if (!menu.rename) return
event.preventDefault()
setMenu("rename", false)
openRename()
}}
>
<Menu.Item disabled={!props.session || rename.isPending} onSelect={() => setMenu("rename", true)}>
{language.t("common.rename")}
</Menu.Item>
<Menu.Item onSelect={props.onClose}>{language.t("common.closeTab")}</Menu.Item>
</Menu.Context.Content>
</Menu.Context.Portal>
</Menu.Context>
)
}
+1 -1
View File
@@ -298,7 +298,7 @@ export function Titlebar(props: {
id: "home.toggle",
title: language.t("home.title"),
category: language.t("command.category.view"),
keybind: "mod+b",
keybind: windows() ? "alt+home" : "mod+b",
hidden: true,
onSelect: toggleHome,
},
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test"
import { DESKTOP_MENU } from "@/shell/commands/desktop-menu"
import { windowsMenuAccelerator } from "./windows-menu"
describe("Windows app menu", () => {
@@ -8,7 +9,24 @@ describe("Windows app menu", () => {
)
})
test("leaves select all to the focused browser editor", () => {
expect(windowsMenuAccelerator(new KeyboardEvent("keydown", { key: "a", ctrlKey: true }))).toBeUndefined()
expect(windowsMenuAccelerator(new KeyboardEvent("keydown", { key: "A", ctrlKey: true }))).toBeUndefined()
})
test("ignores the accelerator without its modifiers", () => {
expect(windowsMenuAccelerator(new KeyboardEvent("keydown", { key: "N" }))).toBeUndefined()
})
test.each(["v", "c", "x", "a", "z", "y"])("leaves Ctrl+%s to the focused editor", (key) => {
expect(windowsMenuAccelerator(new KeyboardEvent("keydown", { key, ctrlKey: true }))).toBeUndefined()
})
test("preserves the paste menu action and shortcut label", () => {
expect(
DESKTOP_MENU.flatMap((menu) => menu.items ?? []).find(
(entry) => entry.type === "item" && entry.action === "edit.paste",
),
).toMatchObject({ action: "edit.paste", accelerator: { windows: "Ctrl+V" } })
})
})
@@ -16,6 +16,8 @@ import { useLanguage } from "@/runtime/i18n/language"
const accelerators = DESKTOP_MENU.flatMap((menu) => menu.items ?? []).flatMap((entry) => {
if (entry.type === "separator" || !entry.action || !entry.accelerator?.windows) return []
// Let the focused editor handle editing shortcuts without restoring stale menu focus.
if (entry.action.startsWith("edit.")) return []
return [{ action: entry.action, keybind: parseKeybind(entry.accelerator.windows) }]
})
+14 -6
View File
@@ -76,6 +76,17 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Lo
const slash = (value: string) => value.replaceAll("\\", "/")
export const permissionTarget = (location: Location.Info, absolute: string) => {
const worktree = path.resolve(location.project.directory)
const internal =
FSUtil.contains(location.directory, absolute) ||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
return {
internal,
resource: slash(internal ? path.relative(location.directory, absolute) || "." : absolute),
}
}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
@@ -85,14 +96,11 @@ const layer = Layer.effect(
const resolve = Effect.fnUntraced(function* (input: ResolveInput) {
const absolute = resolvePath(location.directory, input.path)
const worktree = path.resolve(location.project.directory)
const internal =
FSUtil.contains(location.directory, absolute) ||
(worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute))
if (internal) {
const target = permissionTarget(location, absolute)
if (target.internal) {
return {
absolute,
resource: slash(path.relative(location.directory, absolute) || "."),
resource: target.resource,
} satisfies Target
}
const type =
+6 -7
View File
@@ -53,13 +53,12 @@ export const make = Effect.fnUntraced(function* (options: Options) {
let closing: Promise<void> | undefined
let trailingBytes = 0
const stop = (handle: ChildProcessHandle) =>
Effect.gen(function* () {
const exit = yield* Effect.timeoutOption(handle.exitCode, CLOSE_GRACE)
if (exit._tag === "Some") return
const terminated = yield* Effect.timeoutOption(handle.kill({ killSignal: "SIGTERM" }), FORCE_KILL_AFTER)
if (terminated._tag === "None") yield* handle.kill({ killSignal: "SIGKILL" })
}).pipe(Effect.ignore)
const stop = Effect.fnUntraced(function* (handle: ChildProcessHandle) {
// Exit completion can precede descendant cleanup after the capture deadline.
yield* Effect.timeoutOption(handle.exitCode, CLOSE_GRACE).pipe(Effect.ignore)
const terminated = yield* Effect.timeoutOption(handle.kill({ killSignal: "SIGTERM" }), FORCE_KILL_AFTER)
if (terminated._tag === "None") yield* handle.kill({ killSignal: "SIGKILL" })
}, Effect.ignore())
const close = () =>
(closing ??= Effect.runPromise(
+3 -1
View File
@@ -7,6 +7,7 @@ import type { SessionEvent } from "@opencode-ai/schema/session-event"
import { Global } from "@opencode-ai/util/global"
import { Effect, Stream } from "effect"
import path from "path"
import { LocationMutation } from "../location-mutation.js"
import { Permission } from "../permission.js"
const plan = Agent.ID.make("plan")
@@ -29,6 +30,7 @@ export const Plugin = define({
effect: Effect.fn(function* (ctx) {
const global = yield* Global.Service
const directory = path.join(global.home, ".opencode", "plan")
const resource = LocationMutation.permissionTarget(ctx.location, directory).resource
const enterReminder = enter(directory)
yield* ctx.agent.transform((draft) => {
draft.update(plan, (item) => {
@@ -37,7 +39,7 @@ export const Plugin = define({
item.mode = "primary"
item.permissions.push({ action: "question", resource: "*", effect: "allow" })
item.permissions.push({ action: "edit", resource: "*", effect: "deny" })
item.permissions.push({ action: "edit", resource: path.join(directory, "*"), effect: "allow" })
item.permissions.push({ action: "edit", resource: path.join(resource, "*"), effect: "allow" })
item.permissions.push({ action: "external_directory", resource: path.join(directory, "*"), effect: "allow" })
})
})
+31 -24
View File
@@ -36,6 +36,8 @@ type Draft = {
export interface Interface extends State.Transformable<Draft> {
readonly list: () => Effect.Effect<Info[]>
/** Schedules daily refresh checks in the Location scope without waiting for Git. */
readonly refresh: () => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Reference") {}
@@ -48,6 +50,25 @@ const layer = Layer.effect(
const cache = yield* RepositoryCache.Service
const scope = yield* Scope.Scope
const materialized = new Map<string, Info>()
const refresh = Effect.fn("Reference.refresh")(function* () {
yield* Effect.forEach(
Array.from(materialized.values()),
(reference) =>
Effect.gen(function* () {
if (reference.source.type !== "git") return
yield* cache.ensure({
reference: Repository.parseRemote(reference.source.repository),
branch: reference.source.branch,
refresh: "daily",
})
}).pipe(
Effect.catchCause((cause) =>
Effect.logWarning("failed to materialize reference", { name: reference.name, cause }),
),
),
{ concurrency: 4, discard: true },
).pipe(Effect.forkIn(scope), Effect.asVoid)
})
const state = State.create<Data, Draft>({
name: "reference",
initial: () => ({ sources: new Map() }),
@@ -60,17 +81,14 @@ const layer = Layer.effect(
Effect.gen(function* () {
materialized.clear()
for (const [name, source] of draft.list()) {
const info = {
name,
source,
...(source.description === undefined ? {} : { description: source.description }),
...(source.hidden === undefined ? {} : { hidden: source.hidden }),
}
if (source.type === "local") {
materialized.set(
name,
Info.make({
name,
path: source.path,
...(source.description === undefined ? {} : { description: source.description }),
...(source.hidden === undefined ? {} : { hidden: source.hidden }),
source,
}),
)
materialized.set(name, Info.make({ ...info, path: source.path }))
continue
}
const repository = Repository.parse(source.repository)
@@ -85,24 +103,12 @@ const layer = Layer.effect(
materialized.set(
name,
Info.make({
name,
...info,
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* refresh()
yield* bus.publish(Reference.Event.Updated, {})
}),
})
@@ -110,6 +116,7 @@ const layer = Layer.effect(
return Service.of({
transform: state.transform,
reload: state.reload,
refresh,
list: Effect.fn("Reference.list")(function* () {
return Array.from(materialized.values())
}),
+80 -64
View File
@@ -6,7 +6,7 @@
* observe the checkout move underneath them.
*/
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { Clock, Context, Duration, Effect, Layer, Option, Schema } from "effect"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "./git.js"
import { Global } from "@opencode-ai/util/global"
@@ -14,6 +14,13 @@ import { Repository } from "./repository.js"
import { AbsolutePath } from "./schema.js"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { EffectFlock } from "@opencode-ai/util/effect-flock"
import { KV } from "./kv.js"
const Refresh = Schema.Struct({
attemptedAt: Schema.Number,
refreshedAt: Schema.optionalKey(Schema.Number),
})
const refreshInterval = Duration.toMillis(Duration.days(1))
export type Result = {
readonly repository: string
@@ -27,7 +34,8 @@ export type Result = {
export type EnsureInput = {
readonly reference: Repository.RemoteReference
readonly refresh?: boolean
/** `daily` throttles existing checkouts; `true` forces a refresh. */
readonly refresh?: boolean | "daily"
readonly branch?: string
}
@@ -105,50 +113,55 @@ export const validateBranch = Effect.fn("RepositoryCache.validateBranch")(functi
})
})
const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service | EffectFlock.Service | Global.Service> =
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
const 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
const kv = yield* KV.Service
return Service.of({
ensure: Effect.fn("RepositoryCache.ensure")(function* (input) {
if (input.branch) yield* validateBranch(input.branch)
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
const repository = input.reference.label
const localPath = Repository.cachePath(global.repos, input.reference, input.branch)
const key = `repository-cache:${localPath}`
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)
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 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)
const now = yield* Clock.currentTimeMillis
const previous = Option.getOrUndefined(Schema.decodeUnknownOption(Refresh)(yield* kv.get(key)))
const refresh =
input.refresh === "daily" ? !previous || now - previous.attemptedAt >= refreshInterval : input.refresh
const status = !reuse ? ("cloned" as const) : refresh ? ("refreshed" as const) : ("cached" as const)
if (status !== "cached") {
// Record attempts before network work so offline/auth failures don't retry on every prompt.
yield* kv.set(key, { ...previous, attemptedAt: now })
if (status === "cloned") {
yield* git.repo
@@ -193,34 +206,37 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | Git.Service | EffectFl
.pipe(Effect.mapError((error) => new ResetFailedError({ repository, message: error.message })))
}
const checkout = yield* git.repo.discover(AbsolutePath.make(localPath))
yield* kv.set(key, { attemptedAt: now, refreshedAt: yield* Clock.currentTimeMillis })
}
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) }),
),
)
}),
})
}),
)
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
}),
key,
)
.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],
deps: [EffectFlock.node, FSUtil.node, Git.node, Global.node, KV.node],
})
function errorMessage(error: unknown) {
@@ -3,7 +3,7 @@ import type { Agent } from "@opencode-ai/schema/agent"
import type { Model } from "@opencode-ai/schema/model"
import type { RelativePath } from "@opencode-ai/schema/schema"
import type { Snapshot } from "@opencode-ai/schema/snapshot"
import { Clock, Effect, Iterable } from "effect"
import { Effect, Fiber, Iterable } from "effect"
import { isArrayNonEmpty, isReadonlyArrayNonEmpty } from "effect/Array"
import { Bus } from "../../bus.js"
import { SessionEvent } from "../event.js"
@@ -130,7 +130,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
readonly ordinal: number
readonly values: string[]
pending: string
publishedAt?: number
timer?: Fiber.Fiber<void>
state?: Record<string, unknown>
}
const chunks = new Map<string, Fragment>()
@@ -143,36 +143,46 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
chunks.set(id, { ordinal, values: [], pending: "", state })
return Effect.succeed(ordinal)
})
const publishDelta = Effect.fnUntraced(function* (id: string, force = false) {
const publishDelta = Effect.fnUntraced(function* (id: string) {
if (!delta) return undefined
const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} delta before start: ${id}`))
if (!current.pending) return undefined
const now = yield* Clock.currentTimeMillis
if (!force && current.publishedAt === undefined) {
current.publishedAt = now
return undefined
}
if (!force && current.publishedAt !== undefined && now - current.publishedAt < deltaBatchInterval)
return undefined
yield* delta(id, current.pending, current.ordinal)
const value = current.pending
// New chunks can arrive while the timer is publishing this batch.
current.pending = ""
current.publishedAt = now
yield* delta(id, value, current.ordinal)
return undefined
})
}, Effect.uninterruptible)
const append = Effect.fnUntraced(function* (id: string, value: string, state?: Record<string, unknown>) {
const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} delta before start: ${id}`))
current.values.push(value)
if (delta) current.pending += value
if (state !== undefined) current.state = { ...current.state, ...state }
yield* publishDelta(id)
if (current.pending && !current.timer) {
// Own the trailing flush in the provider fiber, even if no more chunks arrive.
current.timer = yield* Effect.gen(function* () {
while (current.pending) {
yield* Effect.sleep(deltaBatchInterval)
yield* publishDelta(id)
}
}).pipe(
Effect.ensuring(
Effect.sync(() => {
current.timer = undefined
}),
),
Effect.forkChild({ startImmediately: true }),
)
}
return current.ordinal
})
const end = Effect.fnUntraced(function* (id: string, state?: Record<string, unknown>, value?: string) {
const current = chunks.get(id)
if (!current) return yield* Effect.die(new Error(`${name} end before start: ${id}`))
yield* publishDelta(id, true)
if (current.timer) yield* Fiber.interrupt(current.timer)
yield* publishDelta(id)
yield* ended(
id,
value ?? current.values.join(""),
+14 -7
View File
@@ -10,6 +10,7 @@ import { PluginSupervisor } from "../plugin/supervisor-service.js"
import { Shell } from "../shell.js"
import { ShellResult } from "../shell/result.js"
import { Skill } from "../skill.js"
import { Reference } from "../reference.js"
import {
BusyError,
CompactionConflictError,
@@ -34,6 +35,7 @@ import { SessionStore } from "./store.js"
export type Services =
| PluginSupervisor.Service
| Reference.Service
| SessionPrompt.Service
| SessionRevert.Service
| Shell.Service
@@ -167,18 +169,22 @@ export const make = Effect.fn("Session.make")(function* (servicesFor: (ref: Loca
delivery: input.delivery ?? "steer",
})
if (existing) return existing
const item = yield* restore(
SessionPrompt.Service.use((preparation) => preparation.prepare({ sessionID, messageID, input })).pipe(
Effect.provide(servicesFor(session.location)),
),
const prepared = yield* restore(
Effect.gen(function* () {
const preparation = yield* SessionPrompt.Service
const references = yield* Reference.Service
return { item: yield* preparation.prepare({ sessionID, messageID, input }), references }
}).pipe(Effect.provide(servicesFor(session.location))),
)
// Commit a staged revert only after preparation succeeds, before admitting new work.
if (session.revert) yield* SessionRevert.commit(bus, session)
return yield* admission.admit({
const admitted = yield* admission.admit({
id: messageID,
sessionID: session.id,
item,
item: prepared.item,
})
yield* prepared.references.refresh()
return admitted
}).pipe(
Effect.catchTag("SessionInbox.LifecycleConflict", () => new PromptConflictError({ sessionID, messageID })),
)
@@ -272,7 +278,8 @@ export const make = Effect.fn("Session.make")(function* (servicesFor: (ref: Loca
sessionID: SessionSchema.ID,
input: { id?: SessionMessage.ID; delivery?: SessionInbox.Delivery },
) {
yield* get(sessionID)
const session = yield* get(sessionID)
if (session.revert) yield* SessionRevert.commit(bus, session)
const inputID = input.id ?? SessionMessage.ID.create()
const admitted = yield* admission
.admitCompaction({
+5 -1
View File
@@ -384,7 +384,11 @@ const layer = () =>
command.timeoutFiber = runFork(
Effect.sleep(Duration.millis(duration)).pipe(
Effect.flatMap(() =>
finish("timeout", undefined, handle.kill().pipe(Effect.catch(() => Effect.void))),
finish(
"timeout",
undefined,
handle.kill({ forceKillAfter: Duration.seconds(3) }).pipe(Effect.catch(() => Effect.void)),
),
),
),
)

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