mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-31 05:56:19 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
174d263890 | ||
|
|
5ec29e7a87 | ||
|
|
3c6b85acf7 | ||
|
|
50e77f66fd | ||
|
|
d484f070d1 | ||
|
|
8890294bf0 | ||
|
|
a1925de0c1 | ||
|
|
24e826d06b |
@@ -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> &",
|
||||
"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")
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "አዶ",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Икона",
|
||||
|
||||
@@ -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": "আইকন",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "އައިކޮން",
|
||||
|
||||
@@ -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": "ངོས་དཔར།",
|
||||
|
||||
@@ -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": "Εικονίδιο",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "نماد",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "סמל",
|
||||
|
||||
@@ -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": "आइकन",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Պատկերակ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 リポジトリを作成中...",
|
||||
|
||||
@@ -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": "ხატულა",
|
||||
|
||||
@@ -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": "រូបតំណាង",
|
||||
|
||||
@@ -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 저장소 생성 중...",
|
||||
|
||||
@@ -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": "ໄອຄອນ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Икона",
|
||||
|
||||
@@ -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": "Дүрс",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "သင်္ကေတ",
|
||||
|
||||
@@ -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": "आइकन",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "آئکن",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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ă",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "නිරූපකය",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Икона",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Нишона",
|
||||
|
||||
@@ -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 รีโพซิทอรี...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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": "Назва",
|
||||
|
||||
@@ -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": "آئیکن",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 仓库...",
|
||||
|
||||
@@ -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 儲存庫...",
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) }]
|
||||
})
|
||||
|
||||
|
||||
@@ -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(""),
|
||||
|
||||
@@ -53,29 +53,58 @@ export const Plugin = {
|
||||
messageID: context.messageID,
|
||||
id: context.id,
|
||||
}
|
||||
const target = yield* mutation.resolve({ path: input.path })
|
||||
const external = target.externalDirectory
|
||||
if (external)
|
||||
yield* permission.assert({
|
||||
...LocationMutation.externalDirectoryPermission(external),
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source,
|
||||
const authorize = (target: LocationMutation.Target, authorizeExternal = true) =>
|
||||
Effect.gen(function* () {
|
||||
if (target.externalDirectory && authorizeExternal)
|
||||
yield* permission.assert({
|
||||
...LocationMutation.externalDirectoryPermission(target.externalDirectory),
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source,
|
||||
})
|
||||
yield* permission.assert({
|
||||
action: name,
|
||||
resources: [target.resource],
|
||||
save: ["*"],
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source,
|
||||
})
|
||||
})
|
||||
const resource = target.resource
|
||||
const absolute = AbsolutePath.make(target.absolute)
|
||||
yield* permission.assert({
|
||||
action: name,
|
||||
resources: [resource],
|
||||
save: ["*"],
|
||||
sessionID: context.sessionID,
|
||||
agent: context.agent,
|
||||
source,
|
||||
})
|
||||
const content = yield* reader.read(absolute, resource, { offset: input.offset, limit: input.limit }).pipe(
|
||||
const read = (target: LocationMutation.Target) =>
|
||||
reader.read(AbsolutePath.make(target.absolute), target.resource, {
|
||||
offset: input.offset,
|
||||
limit: input.limit,
|
||||
})
|
||||
|
||||
const requested = yield* mutation.resolve({ path: input.path })
|
||||
yield* authorize(requested)
|
||||
const result = yield* read(requested).pipe(
|
||||
Effect.map((content) => ({ content, target: requested, path: input.path })),
|
||||
Effect.catchIf(
|
||||
(error) => error instanceof Environment.NotFound,
|
||||
() => missing(input.path, target.absolute),
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const alternate = yield* alternatePath(requested.absolute).pipe(
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
if (!alternate) return yield* missing(input.path, requested.absolute)
|
||||
const target = yield* mutation.resolve({ path: alternate, kind: "file" })
|
||||
// The candidate is a sibling under the external directory already approved above.
|
||||
yield* authorize(target, false)
|
||||
const content = yield* read(target).pipe(
|
||||
Effect.catchIf(
|
||||
(error) => error instanceof Environment.NotFound,
|
||||
() => missing(input.path, requested.absolute),
|
||||
),
|
||||
)
|
||||
if (content.type === "list-page") return yield* missing(input.path, requested.absolute)
|
||||
return {
|
||||
content,
|
||||
target,
|
||||
path: join(dirname(input.path), basename(alternate)),
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
// After a successful read, discover nearby AGENTS.md walking up to the Location
|
||||
@@ -84,14 +113,14 @@ export const Plugin = {
|
||||
// is discovered); for a file it starts at the file's dirname. External reads are
|
||||
// skipped, and discovery failures never fail the read.
|
||||
yield* Effect.gen(function* () {
|
||||
if (target.externalDirectory !== undefined) return
|
||||
const resolved = yield* fs.resolve(target.absolute)
|
||||
if (result.target.externalDirectory !== undefined) return
|
||||
const resolved = yield* fs.resolve(result.target.absolute)
|
||||
const root = yield* fs.resolve(location.directory)
|
||||
// up() searches its stop directory, so the Location-root AGENTS.md (already
|
||||
// supplied by core initial instructions) is dropped by the dirname filter.
|
||||
const discovered = yield* fs.up({
|
||||
targets: [FILENAME],
|
||||
start: content.type === "list-page" ? resolved : dirname(resolved),
|
||||
start: result.content.type === "list-page" ? resolved : dirname(resolved),
|
||||
stop: root,
|
||||
})
|
||||
const candidates = (yield* Effect.forEach(discovered, fs.resolve)).filter(
|
||||
@@ -104,17 +133,17 @@ export const Plugin = {
|
||||
Effect.catchDefect(() => Effect.void),
|
||||
)
|
||||
if (
|
||||
content.type === "file" &&
|
||||
content.encoding === "base64" &&
|
||||
!ReadToolFileSystem.MEDIA_MIMES.has(content.mime)
|
||||
result.content.type === "file" &&
|
||||
result.content.encoding === "base64" &&
|
||||
!ReadToolFileSystem.MEDIA_MIMES.has(result.content.mime)
|
||||
)
|
||||
return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError({ resource }))
|
||||
return content
|
||||
return yield* Effect.fail(new ReadToolFileSystem.BinaryFileError({ resource: result.target.resource }))
|
||||
return { output: result.content, path: result.path }
|
||||
}).pipe(
|
||||
Effect.map((output) => ({
|
||||
output,
|
||||
content: toModelContent(input.path, input.offset, output),
|
||||
metadata: { truncated: output.type === "file" ? false : output.truncated },
|
||||
Effect.map((result) => ({
|
||||
output: result.output,
|
||||
content: toModelContent(result.path, input.offset, result.output),
|
||||
metadata: { truncated: result.output.type === "file" ? false : result.output.truncated },
|
||||
})),
|
||||
Effect.mapError((error) => {
|
||||
if (error instanceof ToolFailure) return error
|
||||
@@ -133,6 +162,15 @@ export const Plugin = {
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
const alternatePath = Effect.fn("ReadTool.alternatePath")(function* (absolute: string) {
|
||||
const base = basename(absolute).replace(/[\u00a0\u202f]/g, " ")
|
||||
const matches = (yield* reader.list(AbsolutePath.make(dirname(absolute)))).filter(
|
||||
(entry) => entry.type === "file" && entry.name.replace(/[\u00a0\u202f]/g, " ") === base,
|
||||
)
|
||||
if (matches.length !== 1) return
|
||||
return join(dirname(absolute), matches[0].name)
|
||||
})
|
||||
|
||||
const missing = Effect.fn("ReadTool.missing")(function* (input: string, absolute: string) {
|
||||
const base = basename(input).toLowerCase()
|
||||
const suggestions = yield* fs.readDirectory(dirname(absolute)).pipe(
|
||||
|
||||
@@ -101,6 +101,7 @@ export class ListPage extends Schema.Class<ListPage>("ReadTool.ListPage")({
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
readonly list: (path: AbsolutePath) => ReturnType<Files["list"]>
|
||||
readonly read: (
|
||||
path: AbsolutePath,
|
||||
resource: string,
|
||||
@@ -378,7 +379,10 @@ const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const environment = yield* Environment.Service
|
||||
return Service.of({ read: (path, resource, page) => read(environment.files, path, resource, page) })
|
||||
return Service.of({
|
||||
list: environment.files.list,
|
||||
read: (path, resource, page) => read(environment.files, path, resource, page),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ import { TestClock } from "effect/testing"
|
||||
const sessionID = Session.ID.make("ses_tool_event_test")
|
||||
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
|
||||
|
||||
const capture = (providerMetadataKey = "anthropic", options?: { readonly interruptProgress?: boolean }) => {
|
||||
const capture = (
|
||||
providerMetadataKey = "anthropic",
|
||||
options?: { readonly interruptProgress?: boolean; readonly beforeTextDelta?: Effect.Effect<void> },
|
||||
) => {
|
||||
const published: Array<{ readonly type: string; readonly data: unknown }> = []
|
||||
const bus: Pick<Bus.Interface, "publish"> = {
|
||||
publish: (definition, data) => {
|
||||
@@ -40,6 +43,8 @@ const capture = (providerMetadataKey = "anthropic", options?: { readonly interru
|
||||
})
|
||||
return event
|
||||
})
|
||||
if (definition.type === SessionEvent.Text.Delta.type && options?.beforeTextDelta)
|
||||
return options.beforeTextDelta.pipe(Effect.andThen(publish))
|
||||
return definition.type === SessionEvent.Tool.Progress.type && options?.interruptProgress
|
||||
? publish.pipe(Effect.andThen(Effect.interrupt))
|
||||
: publish
|
||||
@@ -323,6 +328,30 @@ test("reasoning state from start, empty delta, and end is merged", async () => {
|
||||
})
|
||||
})
|
||||
|
||||
for (const kind of ["text", "reasoning"] as const) {
|
||||
it.effect(`publishes the trailing ${kind} chunk while the provider is paused`, () =>
|
||||
Effect.gen(function* () {
|
||||
const { published, publisher } = capture()
|
||||
yield* publisher.publish(LLMEvent[kind === "text" ? "textStart" : "reasoningStart"]({ id: kind }))
|
||||
yield* publisher.publish(
|
||||
LLMEvent[kind === "text" ? "textDelta" : "reasoningDelta"]({ id: kind, text: "Running the comm" }),
|
||||
)
|
||||
yield* TestClock.adjust("100 millis")
|
||||
expect(
|
||||
published.filter((event) => event.type === `session.${kind}.delta`).map((event) => event.data),
|
||||
).toMatchObject([{ delta: "Running the comm" }])
|
||||
|
||||
yield* publisher.publish(LLMEvent[kind === "text" ? "textDelta" : "reasoningDelta"]({ id: kind, text: "and." }))
|
||||
yield* TestClock.adjust("100 millis")
|
||||
expect(
|
||||
published.filter((event) => event.type === `session.${kind}.delta`).map((event) => event.data),
|
||||
).toMatchObject([{ delta: "Running the comm" }, { delta: "and." }])
|
||||
expect(published.some((event) => event.type === `session.${kind}.ended.1`)).toBe(false)
|
||||
yield* publisher.flush()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("batches text deltas and flushes pending text before the terminal event", () =>
|
||||
Effect.gen(function* () {
|
||||
const { published, publisher } = capture()
|
||||
@@ -341,15 +370,44 @@ it.effect("batches text deltas and flushes pending text before the terminal even
|
||||
yield* TestClock.adjust("99 millis")
|
||||
expect(published.filter((event) => event.type === "session.text.delta")).toHaveLength(0)
|
||||
yield* TestClock.adjust("1 millis")
|
||||
yield* publisher.publish(LLMEvent.textDelta({ id: "text", text: " four" }))
|
||||
expect(published.filter((event) => event.type === "session.text.delta").map((event) => event.data)).toMatchObject([
|
||||
{ delta: "one two three four" },
|
||||
{ delta: "one two three" },
|
||||
])
|
||||
|
||||
yield* publisher.publish(LLMEvent.textDelta({ id: "text", text: " four" }))
|
||||
yield* publisher.publish(LLMEvent.textDelta({ id: "text", text: " five" }))
|
||||
yield* publisher.publish(LLMEvent.textEnd({ id: "text" }))
|
||||
expect(published.slice(-2).map((event) => event.type)).toEqual(["session.text.delta", "session.text.ended.1"])
|
||||
expect(published.at(-2)?.data).toMatchObject({ delta: " five" })
|
||||
expect(published.at(-2)?.data).toMatchObject({ delta: " four five" })
|
||||
const count = published.length
|
||||
yield* TestClock.adjust("1 second")
|
||||
expect(published).toHaveLength(count)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("retains new chunks and orders text-end behind an in-flight timer publication", () =>
|
||||
Effect.gen(function* () {
|
||||
const entered = yield* Deferred.make<void>()
|
||||
const release = yield* Deferred.make<void>()
|
||||
const { published, publisher } = capture("anthropic", {
|
||||
beforeTextDelta: Deferred.succeed(entered, undefined).pipe(Effect.andThen(Deferred.await(release))),
|
||||
})
|
||||
yield* publisher.publish(LLMEvent.textStart({ id: "text" }))
|
||||
yield* publisher.publish(LLMEvent.textDelta({ id: "text", text: "comm" }))
|
||||
yield* TestClock.adjust("100 millis")
|
||||
yield* Deferred.await(entered)
|
||||
yield* publisher.publish(LLMEvent.textDelta({ id: "text", text: "and." }))
|
||||
const ending = yield* publisher
|
||||
.publish(LLMEvent.textEnd({ id: "text" }))
|
||||
.pipe(Effect.forkChild({ startImmediately: true }))
|
||||
expect(published.some((event) => event.type === "session.text.ended.1")).toBe(false)
|
||||
yield* Deferred.succeed(release, undefined)
|
||||
yield* Fiber.join(ending)
|
||||
expect(published.slice(-3)).toMatchObject([
|
||||
{ type: "session.text.delta", data: { delta: "comm" } },
|
||||
{ type: "session.text.delta", data: { delta: "and." } },
|
||||
{ type: "session.text.ended.1", data: { text: "command." } },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -5541,6 +5541,29 @@ describe("SessionRunnerLLM", () => {
|
||||
])
|
||||
})
|
||||
|
||||
scenario("broadcasts pending text while the provider stream is paused", function* (s) {
|
||||
const paused = yield* Deferred.make<void>()
|
||||
yield* s.admit("Check before running a command")
|
||||
yield* s.llm.push(
|
||||
Stream.concat(
|
||||
Stream.fromIterable([
|
||||
LLMEvent.textStart({ id: "text" }),
|
||||
LLMEvent.textDelta({ id: "text", text: "Checking the project." }),
|
||||
]),
|
||||
Stream.fromEffect(Deferred.succeed(paused, undefined)).pipe(Stream.flatMap(() => Stream.never)),
|
||||
),
|
||||
)
|
||||
const deltas = yield* s.bus
|
||||
.subscribe(SessionEvent.Text.Delta)
|
||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true }))
|
||||
const running = yield* s.resume.pipe(Effect.forkScoped)
|
||||
yield* Deferred.await(paused)
|
||||
yield* TestClock.adjust("100 millis")
|
||||
expect(deltas.pollUnsafe()).toBeDefined()
|
||||
expect(Array.from(yield* Fiber.join(deltas)).map((event) => event.data.delta)).toEqual(["Checking the project."])
|
||||
yield* Fiber.interrupt(running)
|
||||
})
|
||||
|
||||
for (const kind of fragmentKinds) {
|
||||
scenario(
|
||||
kind === "tool input"
|
||||
|
||||
@@ -46,8 +46,10 @@ const readCalls: {
|
||||
input: AbsolutePath
|
||||
page: ReadToolFileSystem.PageInput
|
||||
}[] = []
|
||||
const listCalls: AbsolutePath[] = []
|
||||
let resolveFailure: unknown
|
||||
let directoryEntries: string[] = []
|
||||
let directoryEntryDetails: Environment.DirEntry[] = []
|
||||
let readResult: ReadToolFileSystem.FileContent | ReadToolFileSystem.TextPage | ReadToolFileSystem.ListPage = {
|
||||
type: "file",
|
||||
uri: "file:///README.md",
|
||||
@@ -57,12 +59,18 @@ let readResult: ReadToolFileSystem.FileContent | ReadToolFileSystem.TextPage | R
|
||||
mime: "text/plain",
|
||||
}
|
||||
let readFailure: ReadToolFileSystem.ReadError | undefined
|
||||
let readOverride: ReadToolFileSystem.Interface["read"] | undefined
|
||||
const reader = Layer.succeed(
|
||||
ReadToolFileSystem.Service,
|
||||
ReadToolFileSystem.Service.of({
|
||||
read: (input, _resource, page = {}) => {
|
||||
list: (input) => {
|
||||
listCalls.push(input)
|
||||
return Effect.succeed(directoryEntryDetails)
|
||||
},
|
||||
read: (input, resource, page = {}) => {
|
||||
readCalls.push({ input, page })
|
||||
if (resolveFailure !== undefined) return Effect.die(resolveFailure)
|
||||
if (readOverride) return readOverride(input, resource, page)
|
||||
if (readFailure !== undefined) return Effect.fail(readFailure)
|
||||
return Effect.succeed(readResult)
|
||||
},
|
||||
@@ -155,9 +163,11 @@ describe("ReadTool", () => {
|
||||
beforeEach(() => {
|
||||
assertions.length = 0
|
||||
readCalls.length = 0
|
||||
listCalls.length = 0
|
||||
allow = true
|
||||
resolveFailure = undefined
|
||||
directoryEntries = []
|
||||
directoryEntryDetails = []
|
||||
readResult = {
|
||||
type: "file",
|
||||
uri: "file:///README.md",
|
||||
@@ -167,6 +177,7 @@ describe("ReadTool", () => {
|
||||
mime: "text/plain",
|
||||
}
|
||||
readFailure = undefined
|
||||
readOverride = undefined
|
||||
})
|
||||
|
||||
it.effect("registers, authorizes, and reads through the location filesystem", () =>
|
||||
@@ -202,6 +213,7 @@ describe("ReadTool", () => {
|
||||
page: { offset: undefined, limit: undefined },
|
||||
},
|
||||
])
|
||||
expect(listCalls).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -674,6 +686,88 @@ describe("ReadTool", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("recovers a unique file whose non-breaking spaces differ", () =>
|
||||
Effect.gen(function* () {
|
||||
const requested = "Screenshot 2026-08-27 3.15 PM.png"
|
||||
const recovered = "Screenshot 2026-08-27 3.15\u202fPM.png"
|
||||
const requestedAbsolute = path.join(process.cwd(), requested)
|
||||
const recoveredAbsolute = path.join(process.cwd(), recovered)
|
||||
directoryEntryDetails = [{ name: recovered, type: "file" }]
|
||||
readOverride = (input) =>
|
||||
input === requestedAbsolute
|
||||
? Effect.fail(new Environment.NotFound({ path: requestedAbsolute }))
|
||||
: Effect.succeed(readResult)
|
||||
const registry = yield* Tool.Service
|
||||
|
||||
const result = yield* executeTool(registry, {
|
||||
sessionID,
|
||||
...toolIdentity,
|
||||
call: { type: "tool-call", id: "call-recovered-path", name: "read", input: { path: requested } },
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "completed",
|
||||
content: [{ type: "text", text: `Read file ${recovered}, lines 1-1\n1: hello` }],
|
||||
})
|
||||
expect(assertions).toMatchObject([
|
||||
{ action: "read", resources: [requested] },
|
||||
{ action: "read", resources: [recovered] },
|
||||
])
|
||||
expect(readCalls.map((call) => call.input)).toEqual([
|
||||
AbsolutePath.make(requestedAbsolute),
|
||||
AbsolutePath.make(recoveredAbsolute),
|
||||
])
|
||||
expect(listCalls).toEqual([AbsolutePath.make(process.cwd())])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not recover ambiguous files", () =>
|
||||
Effect.gen(function* () {
|
||||
const requested = "report final.txt"
|
||||
const requestedAbsolute = path.join(process.cwd(), requested)
|
||||
readFailure = new Environment.NotFound({ path: requestedAbsolute })
|
||||
directoryEntryDetails = [
|
||||
{ name: "report\u00a0final.txt", type: "file" },
|
||||
{ name: "report\u202ffinal.txt", type: "file" },
|
||||
]
|
||||
const registry = yield* Tool.Service
|
||||
|
||||
expect(
|
||||
yield* executeTool(registry, {
|
||||
sessionID,
|
||||
...toolIdentity,
|
||||
call: { type: "tool-call", id: "call-ambiguous-path", name: "read", input: { path: requested } },
|
||||
}),
|
||||
).toMatchObject({ status: "error", error: { message: `File not found: ${requested}` } })
|
||||
expect(assertions).toHaveLength(1)
|
||||
expect(readCalls).toHaveLength(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not recover a directory", () =>
|
||||
Effect.gen(function* () {
|
||||
const requested = "report final"
|
||||
const recovered = "report\u00a0final"
|
||||
const requestedAbsolute = path.join(process.cwd(), requested)
|
||||
directoryEntryDetails = [{ name: recovered, type: "directory" }]
|
||||
readOverride = (input) =>
|
||||
input === requestedAbsolute
|
||||
? Effect.fail(new Environment.NotFound({ path: requestedAbsolute }))
|
||||
: Effect.succeed(new ReadToolFileSystem.ListPage({ type: "list-page", entries: [], truncated: false }))
|
||||
const registry = yield* Tool.Service
|
||||
|
||||
expect(
|
||||
yield* executeTool(registry, {
|
||||
sessionID,
|
||||
...toolIdentity,
|
||||
call: { type: "tool-call", id: "call-directory-recovery", name: "read", input: { path: requested } },
|
||||
}),
|
||||
).toMatchObject({ status: "error", error: { message: `File not found: ${requested}` } })
|
||||
expect(assertions).toHaveLength(1)
|
||||
expect(readCalls).toHaveLength(1)
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lists a bounded directory page through read", () =>
|
||||
Effect.gen(function* () {
|
||||
readResult = new ReadToolFileSystem.ListPage({
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Effect } from "effect"
|
||||
import { BackgroundServiceState } from "./background-service-state"
|
||||
|
||||
test("new consumers receive the latest reconnected service", async () => {
|
||||
const initial = { url: "http://127.0.0.1:4100", username: "opencode", password: "first" }
|
||||
const replacement = { url: "http://127.0.0.1:4200", username: "opencode", password: "second" }
|
||||
const initial = { url: "http://127.0.0.1:4100", password: "first" }
|
||||
const replacement = { url: "http://127.0.0.1:4200", password: "second" }
|
||||
const service = await Effect.runPromise(
|
||||
BackgroundServiceState.make({ initial: Effect.succeed(initial), reconnect: Effect.succeed(replacement) }),
|
||||
)
|
||||
|
||||
@@ -52,14 +52,12 @@ const connect = Effect.fn("BackgroundService.connect")(function* (mode: "initial
|
||||
const url = new URL(service.url)
|
||||
if (url.hostname === "0.0.0.0") url.hostname = "127.0.0.1"
|
||||
yield* Effect.logInfo("v2 CLI background service ready", {
|
||||
username: service.auth.username,
|
||||
version,
|
||||
...endpoint(url.origin),
|
||||
})
|
||||
if (mode === "initial" && isolated && cli.binary) yield* cleanStages(cli.binary).pipe(Effect.orDie)
|
||||
return {
|
||||
url: url.origin,
|
||||
username: service.auth.username,
|
||||
password: service.auth.password,
|
||||
} satisfies ServerReadyData
|
||||
})
|
||||
|
||||
@@ -65,7 +65,6 @@ test("stops a running WSL server before replacing its CLI", async () => {
|
||||
},
|
||||
onExit: () => undefined,
|
||||
url: "http://127.0.0.1:4096",
|
||||
username: "opencode",
|
||||
password: "secret",
|
||||
}
|
||||
},
|
||||
@@ -77,6 +76,11 @@ test("stops a running WSL server before replacing its CLI", async () => {
|
||||
)
|
||||
controller.startConfiguredServers()
|
||||
await waitFor(() => controller.getState().servers[0]?.runtime.kind === "ready")
|
||||
expect(controller.getState().servers[0]?.runtime).toEqual({
|
||||
kind: "ready",
|
||||
url: "http://127.0.0.1:4096",
|
||||
password: "secret",
|
||||
})
|
||||
|
||||
await controller.installOpencode("Debian")
|
||||
|
||||
@@ -105,7 +109,6 @@ test("stops a sidecar that finishes starting after shutdown", async () => {
|
||||
},
|
||||
onExit: () => undefined,
|
||||
url: "http://127.0.0.1:4096",
|
||||
username: "opencode",
|
||||
password: "secret",
|
||||
})
|
||||
await waitFor(() => stopped.length === 1)
|
||||
@@ -194,7 +197,6 @@ function testControllerOptions(overrides: Partial<ControllerOptions> = {}): Cont
|
||||
stop: async () => undefined,
|
||||
onExit: () => undefined,
|
||||
url: "http://127.0.0.1:4096",
|
||||
username: "opencode",
|
||||
password: "secret",
|
||||
}),
|
||||
readServers: () => persistedServers,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user