mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-07 07:48:16 +00:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e89f9f43f8 | ||
|
|
1d610f90a0 | ||
|
|
eebe9d370f | ||
|
|
a2b6005627 | ||
|
|
a90ad8566d | ||
|
|
a7f3d570ae | ||
|
|
8b45c5674b | ||
|
|
56c2f432cc | ||
|
|
74678c4677 | ||
|
|
369aad308b | ||
|
|
d5cf9c2548 | ||
|
|
9c51d84f8f | ||
|
|
8b02c4ef04 | ||
|
|
e41256eb5a | ||
|
|
d1749770c7 | ||
|
|
9ee97d87a2 | ||
|
|
59e5953443 | ||
|
|
4a0f273fa7 | ||
|
|
36a38058fb | ||
|
|
ccb5162714 | ||
|
|
c2f87853c6 | ||
|
|
ed0e7acecd |
No files matched your search
@@ -506,6 +506,10 @@
|
||||
"name": "@opencode/gui-extensions",
|
||||
"version": "2.0.24",
|
||||
"dependencies": {
|
||||
"@betteroffice/docx": "0.4.3",
|
||||
"@betteroffice/fonts": "0.4.0",
|
||||
"@betteroffice/pptx": "0.2.0",
|
||||
"@betteroffice/xlsx": "0.3.0",
|
||||
"@dnd-kit/abstract": "0.5.0",
|
||||
"@dnd-kit/dom": "0.5.0",
|
||||
"@dnd-kit/solid": "0.5.0",
|
||||
@@ -1634,6 +1638,14 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
|
||||
|
||||
"@betteroffice/docx": ["@betteroffice/docx@0.4.3", "", { "peerDependencies": { "@betteroffice/fonts": "^0" }, "optionalPeers": ["@betteroffice/fonts"] }, "sha512-CEErXJH3iCsi2a61gTNkAofLCzSTcktLmp710gGhMwbBfOqwQ2yI7dRFBV3ElGa9JxkS03IfejvLX0sAvlTrUQ=="],
|
||||
|
||||
"@betteroffice/fonts": ["@betteroffice/fonts@0.4.0", "", { "peerDependencies": { "@betteroffice/fonts-cjk": "^0" }, "optionalPeers": ["@betteroffice/fonts-cjk"] }, "sha512-x1BVwYDAgY5R6TWI4t9NLguSiAEbH5GItN2ZlpBIFoRXTIJgjBsXd6sUxPGpexXOyY6KdhRjG8XvHjyAYxEGCw=="],
|
||||
|
||||
"@betteroffice/pptx": ["@betteroffice/pptx@0.2.0", "", {}, "sha512-pNPQ58tlA+pVibERbLdQNskvP7a0GIMf4jhkCU/gPp8043Ichf6gB3PSb4ra9sfqoIaWqOaZKCWyptovA2+Q0A=="],
|
||||
|
||||
"@betteroffice/xlsx": ["@betteroffice/xlsx@0.3.0", "", {}, "sha512-K2bHtdk/epAxsmvxJlIWBRndjP3ltQU0Oh3p4gcZd2n5/YgXglo3SmMYX4nQe8WEm/yvz91p+aIasKyXXQP9Wg=="],
|
||||
|
||||
"@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||
|
||||
"@brendonovich/vite-plugin-opencode": ["@brendonovich/vite-plugin-opencode@0.1.3", "", { "dependencies": { "@babel/core": "^7.29.0", "@opencode-ai/client": "0.0.0-beta-18050" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-iiIwlNoycOMUiaUzL1ZirLapCG3WY8cg9hSj4KgE/JYIWgqauoHikIlfUt7SdMqqGIkb4iZu6I1MxgDDJaRfBA=="],
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-mrsVbX15SgLsQWBknYKmHSBpWhE6BnyUrYzxIWL35js=",
|
||||
"aarch64-linux": "sha256-uskMkbG9jeGoal1mHV4rpkkvBb3TDBvQcDdQT/bP9y4=",
|
||||
"aarch64-darwin": "sha256-/8nVBK6lBRO34Id/jKOUeEaEHHYJhh8cFESWfhzzln0="
|
||||
"x86_64-linux": "sha256-vzF5I8/QHutYM6j0R73ghqBCRntL6/fegzivyDTO9Ro=",
|
||||
"aarch64-linux": "sha256-foy1hI+cewpLJvNqTZCCNTqV4tnQSDK55ZRs87MvnI8=",
|
||||
"aarch64-darwin": "sha256-wl6+Omz25VxLRMsh0rvFZCqBErk0o3mh9cjuUYsYAP4="
|
||||
}
|
||||
}
|
||||
@@ -507,6 +507,7 @@ const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenR
|
||||
typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
|
||||
? metadata.reasoningEncryptedContent
|
||||
: undefined
|
||||
if (part.text.length === 0 && !encryptedContent) return undefined
|
||||
return {
|
||||
type: "reasoning",
|
||||
...(id === undefined ? {} : { id }),
|
||||
|
||||
@@ -4011,6 +4011,46 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("drops empty reasoning items without summary or encrypted content", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
id: "req_reasoning_empty_shell",
|
||||
model,
|
||||
messages: [
|
||||
Message.user("What changed?"),
|
||||
Message.assistant([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: {
|
||||
openai: {
|
||||
itemId: "rs_6aa28a10c05cf9f566f44022:rs_01a08aeb51217b92a5b853a0cb5b20ca",
|
||||
reasoningEncryptedContent: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ type: "text", text: "The parser changed." },
|
||||
]),
|
||||
Message.user("Summarize it."),
|
||||
],
|
||||
providerOptions: { store: false },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.input).toEqual([
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "What changed?" }] },
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
status: "completed",
|
||||
content: [{ type: "output_text", text: "The parser changed." }],
|
||||
},
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Summarize it." }] },
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("assembles streamed function call input", () =>
|
||||
Effect.gen(function* () {
|
||||
const body = sseEvents(
|
||||
|
||||
@@ -282,13 +282,28 @@ story("restores the current URL each time the same submitted navigation is block
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("https://blocked.example/")
|
||||
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
|
||||
await expect(root.getByRole("alert")).toHaveText("Request failed")
|
||||
await expect(root.getByRole("alert")).toHaveText("Only web pages can open here.")
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
story("explains why a typed address cannot open instead of searching for it", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
|
||||
for (const [typed, reason] of [
|
||||
["ftp://example.com", "Only web pages can open here."],
|
||||
["https://user:pass@example.com", "Addresses with a user name or password can't open here."],
|
||||
]) {
|
||||
await address.fill(typed)
|
||||
await address.press("Enter")
|
||||
await expect(root.getByRole("alert")).toHaveText(reason)
|
||||
await expect(address).toHaveValue("https://alpha.example/")
|
||||
}
|
||||
})
|
||||
|
||||
story("shows a themed failure state for only the failed tab and allows retry", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Failed page", exact: true }).click()
|
||||
@@ -435,6 +450,9 @@ story("suggests visited pages and opens the first one that completes the typed a
|
||||
const options = page.getByRole("option")
|
||||
await expect(options).toHaveCount(1)
|
||||
await expect(options.first()).toHaveAttribute("aria-selected", "true")
|
||||
// Arrow keys that pick IME candidates leave the suggestions alone.
|
||||
await address.dispatchEvent("keydown", { key: "ArrowDown", isComposing: true })
|
||||
await expect(options.first()).toHaveAttribute("aria-selected", "true")
|
||||
await address.press("Enter")
|
||||
await expect(address).toHaveValue("http://localhost:5173/settings")
|
||||
|
||||
@@ -451,14 +469,39 @@ story("suggests visited pages and opens the first one that completes the typed a
|
||||
|
||||
story("lists recent pages on a blank tab and opens one", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
|
||||
await root.getByRole("button", { name: "Seed history", exact: true }).click()
|
||||
await root.getByRole("button", { name: "Blank page", exact: true }).click()
|
||||
await expect(root.getByText("Recent", { exact: true })).toBeVisible()
|
||||
// The globe at the field's start belongs to the field: a click there focuses it.
|
||||
await expect(address).not.toBeFocused()
|
||||
await root.locator('[data-component="browser-address"]').click({ position: { x: 16, y: 14 } })
|
||||
await expect(address).toBeFocused()
|
||||
await address.blur()
|
||||
|
||||
await root.getByRole("button", { name: /^Settings - Preview/ }).click()
|
||||
await expect(root.getByRole("combobox", { name: "Browser address", exact: true })).toHaveValue(
|
||||
"http://localhost:5173/settings",
|
||||
)
|
||||
await expect(address).toHaveValue("http://localhost:5173/settings")
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
// The opened page owns the field afterwards: an edit that is never submitted shows its URL again.
|
||||
await address.click()
|
||||
await address.fill("unsubmitted")
|
||||
await address.blur()
|
||||
await expect(address).toHaveValue("http://localhost:5173/settings")
|
||||
})
|
||||
|
||||
story("adds the whole page to the session and keeps the draft when the button is pressed again", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
const field = root.locator('[data-component="browser-address"]')
|
||||
const add = root.getByRole("button", { name: "Add page to session", exact: true })
|
||||
await field.hover()
|
||||
await add.click()
|
||||
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
|
||||
await editor.fill("Check the header layout")
|
||||
await field.hover()
|
||||
await add.click()
|
||||
await expect(editor).toHaveValue("Check the header layout")
|
||||
await editor.press("Enter")
|
||||
await expect(root.getByTestId("fixture-comments").getByRole("listitem")).toHaveText([/: Check the header layout$/])
|
||||
})
|
||||
|
||||
story("focuses the address field when the page uses the address shortcut", async ({ page }) => {
|
||||
@@ -478,12 +521,22 @@ story("shows the page's cookies in the site information and clears them", async
|
||||
await expect(site).toContainText("0 cookies in use")
|
||||
})
|
||||
|
||||
story("zooms the page from the browser options and shows its zoom", async ({ page }) => {
|
||||
story("zooms the page from the browser options in view and keeps its zoom in the address field", async ({ page }) => {
|
||||
const root = page.getByTestId("browser-pane-fixture")
|
||||
await root.getByRole("button", { name: "Browser options", exact: true }).click()
|
||||
const options = root.getByRole("button", { name: "Browser options", exact: true })
|
||||
await options.click()
|
||||
await page.getByRole("menuitem", { name: /^Zoom in/ }).click()
|
||||
await expect(root.getByText("Zoom: 110", { exact: true })).toBeVisible()
|
||||
// The menu covered the page with a still; it closes so the zoomed page shows at once.
|
||||
await expect(page.getByRole("menu")).toBeHidden()
|
||||
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
|
||||
|
||||
const zoom = root.getByRole("button", { name: "110%", exact: true })
|
||||
await expect(zoom).toBeVisible()
|
||||
await options.click()
|
||||
await expect(page.getByRole("menuitem", { name: /^Reset zoom/ })).toContainText("110%")
|
||||
await page.getByRole("menuitem", { name: /^Reset zoom/ }).click()
|
||||
await page.keyboard.press("Escape")
|
||||
await zoom.click()
|
||||
await expect(root.getByText("Zoom: 100", { exact: true })).toBeVisible()
|
||||
await expect(zoom).toBeHidden()
|
||||
})
|
||||
@@ -85,7 +85,7 @@ const router: Router = { routing: () => false, path: () => "/" }
|
||||
|
||||
const keybinds: Keybinds = { keybind: () => [], keys: (bind) => bind.split("+"), matches: () => false }
|
||||
|
||||
const servers: Servers = { list: () => [] }
|
||||
const servers: Servers = { list: () => [], get: () => undefined }
|
||||
|
||||
const workspaces: Workspaces = { on: () => () => undefined }
|
||||
|
||||
@@ -348,6 +348,7 @@ function standIn(overrides: Partial<Interface>): Interface {
|
||||
keybind: () => [],
|
||||
matches: () => false,
|
||||
servers: () => ["local"],
|
||||
server: () => undefined,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { holdRoute, seed, sessionHref } from "../utils/app"
|
||||
import { holdRoute, NO_PROVIDER, project, REMOTE_SERVER, seed, sessionHref } from "../utils/app"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { fixture, mockStressTimeline } from "../utils/session-fixture"
|
||||
import { mockRemoteServer } from "../utils/workspace"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
|
||||
test.use({ serviceWorkers: "block" })
|
||||
@@ -85,8 +87,11 @@ test("Home shows loaded sessions before the location request resolves", async ({
|
||||
test("Home and the directory picker load without newer browser APIs", async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
// Safari 16.6 has none of these APIs. Remove them before the web entry runs.
|
||||
// SAFETY: `Partial` only makes the static method optional so `delete` type-checks; the target is the real global.
|
||||
delete (Map as Partial<typeof Map>).groupBy
|
||||
// SAFETY: as above, for the real global `Promise`.
|
||||
delete (Promise as Partial<typeof Promise>).withResolvers
|
||||
// SAFETY: as above, for the real global `Promise`.
|
||||
delete (Promise as Partial<typeof Promise>).try
|
||||
})
|
||||
await openHome(page, { fileList: () => [] })
|
||||
@@ -101,6 +106,81 @@ test("Home and the directory picker load without newer browser APIs", async ({ p
|
||||
await expect(target).toBeVisible()
|
||||
})
|
||||
|
||||
test("adding a project to a signed-out server re-pairs it, then continues at the paired address", async ({ page }) => {
|
||||
// The saved address rejects the new token, so saving moves the server to the link's address.
|
||||
const paired = "http://127.0.0.1:4098"
|
||||
await mockRemoteServer(page, { name: "Remote", password: "old-password" })
|
||||
await mockOpenCodeServer(page, {
|
||||
server: paired,
|
||||
directory: "/remote/paired",
|
||||
project: project({ id: "proj_paired", directory: "/remote/paired" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
fileList: () => [],
|
||||
password: "session-token",
|
||||
pairing: { code: "one-time-code", token: "session-token" },
|
||||
})
|
||||
await openHome(page, { fileList: () => [] })
|
||||
const remote = page.locator("[data-home-row]").filter({ hasText: "Remote" })
|
||||
await expect(page.getByRole("button", { name: "Authenticate", exact: true })).toBeVisible()
|
||||
|
||||
await remote.getByRole("button", { name: "Add project", exact: true }).click()
|
||||
const editor = page.getByRole("dialog", { name: "Edit server" })
|
||||
await editor.getByLabel("Pairing link", { exact: true }).fill(`${paired}/auth/connect/one-time-code`)
|
||||
await editor.getByRole("button", { name: "Save", exact: true }).click()
|
||||
|
||||
// The folder picker reads the paired address; the removed one would reject the request.
|
||||
const picker = page.getByRole("dialog").filter({ has: page.getByRole("button", { name: "Select folder" }) })
|
||||
await expect(picker.getByRole("button", { name: "Select folder", exact: true })).toBeEnabled()
|
||||
await expect(picker.getByText("Unable to read this folder")).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("a server whose saved password changes works with the new password", async ({ page }) => {
|
||||
const accepted = { password: "old-password" }
|
||||
await mockOpenCodeServer(page, {
|
||||
server: REMOTE_SERVER,
|
||||
directory: "/remote/project",
|
||||
project: project({ id: "proj_remote", directory: "/remote/project" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
fileList: () => [],
|
||||
password: () => accepted.password,
|
||||
})
|
||||
// Saved with the password the server accepts until the test changes it.
|
||||
await seed(page, {
|
||||
storage: {
|
||||
"opencode.global.dat:server": {
|
||||
list: [{ type: "http", displayName: "Remote", http: { url: REMOTE_SERVER, password: "old-password" } }],
|
||||
},
|
||||
},
|
||||
})
|
||||
await openHome(page, { fileList: () => [] })
|
||||
const remote = page.locator("[data-home-row]").filter({ hasText: "Remote" })
|
||||
const picker = page.getByRole("dialog").filter({ has: page.getByRole("button", { name: "Select folder" }) })
|
||||
|
||||
// Use the server once, so its controller exists with the old password.
|
||||
await remote.getByRole("button", { name: "Add project", exact: true }).click()
|
||||
await expect(picker.getByRole("button", { name: "Select folder", exact: true })).toBeEnabled()
|
||||
await picker.getByRole("button", { name: "Cancel", exact: true }).click()
|
||||
await expect(picker).toHaveCount(0)
|
||||
|
||||
await remote.getByRole("button", { name: "More options", exact: true }).click()
|
||||
await page.getByRole("menuitem", { name: "Edit", exact: true }).click()
|
||||
const editor = page.getByRole("dialog", { name: "Edit server" })
|
||||
await editor.getByPlaceholder("password").fill("new-password")
|
||||
// The server now rejects the old password, as after `opencode service set password`.
|
||||
accepted.password = "new-password"
|
||||
await editor.getByRole("button", { name: "Save", exact: true }).click()
|
||||
await expect(editor).toHaveCount(0)
|
||||
|
||||
// The picker reads the folder through the server's controller, which must use the saved password now.
|
||||
await remote.getByRole("button", { name: "Add project", exact: true }).click()
|
||||
await expect(picker.getByRole("button", { name: "Select folder", exact: true })).toBeEnabled()
|
||||
await expect(picker.getByText("Unable to read this folder")).toHaveCount(0)
|
||||
})
|
||||
|
||||
const recovery = "C:/OpenCode/Worktrees/project-menu-recovery"
|
||||
|
||||
const worktree =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import type { ConfigEntry, OpenCodeEvent, WorktreeDirectory } from "@opencode/client/promise"
|
||||
import { NO_PROVIDER, REMOTE_SERVER, SERVER, holdRoute, project, session } from "../utils/app"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { mockRemoteServer, mockWorkspace, openSettings, type WorkspaceInput } from "../utils/workspace"
|
||||
|
||||
const directory = "C:/Projects/settings-demo"
|
||||
@@ -463,7 +464,7 @@ test("worktrees follow inventory events, wait for session counts, and delete by
|
||||
worktrees: () => inventory,
|
||||
onWorktreeRemove: (body) => {
|
||||
inventory.splice(
|
||||
inventory.findIndex((item) => item.directory === (body as { directory: string }).directory),
|
||||
inventory.findIndex((item) => item.directory === body.directory),
|
||||
1,
|
||||
)
|
||||
},
|
||||
@@ -499,6 +500,7 @@ test("worktrees follow inventory events, wait for session counts, and delete by
|
||||
// Home never listed this session, so only the new worktree's directory read can show it.
|
||||
view.sessions.push(session({ id: "ses_discovered", directory: discovered, title: "Discovered session", projectID }))
|
||||
inventory.push({ directory: discovered, strategy: "git" })
|
||||
// SAFETY: a worktree.updated event carries only the project ID, which is all the app reads from it.
|
||||
await view.push([
|
||||
{ id: "evt_settings_worktree_updated", created: Date.now(), type: "worktree.updated", data: { projectID } },
|
||||
] as OpenCodeEvent[])
|
||||
@@ -582,7 +584,9 @@ test.describe("worktrees prefetch", () => {
|
||||
|
||||
test("server Worktrees hover only prefetches metadata", async ({ page }) => {
|
||||
const { settings } = await open(page)
|
||||
const calls = { projects: 0, worktrees: [] as string[], refreshes: [] as string[] }
|
||||
|
||||
const calls = { projects: 0, worktrees: new Array<string>(), refreshes: new Array<string>() }
|
||||
|
||||
page.on("request", (request) => {
|
||||
if (new URL(request.url()).pathname === "/api/worktree/refresh")
|
||||
calls.refreshes.push(request.postDataJSON().projectID)
|
||||
@@ -754,7 +758,9 @@ test("the add server dialog keeps focus above fullscreen settings", async ({ pag
|
||||
await settings.getByRole("button", { name: "Add server" }).click()
|
||||
|
||||
const editor = page.getByRole("dialog", { name: "Add server" })
|
||||
await expect(editor.getByPlaceholder("http://localhost:4096")).toBeFocused()
|
||||
await expect(editor.getByLabel("Pairing link", { exact: true })).toBeFocused()
|
||||
await expect(editor.getByPlaceholder("password")).toHaveCount(0)
|
||||
await editor.getByRole("button", { name: "Use password", exact: true }).click()
|
||||
await expect(editor.getByPlaceholder("username")).toHaveCount(0)
|
||||
const name = editor.getByPlaceholder("Localhost", { exact: true })
|
||||
await name.click()
|
||||
@@ -766,6 +772,80 @@ test("the add server dialog keeps focus above fullscreen settings", async ({ pag
|
||||
await expect(settings).toBeVisible()
|
||||
})
|
||||
|
||||
test("the add server dialog pairs from a one-time link and explains a spent one", async ({ page }) => {
|
||||
const paired = "http://127.0.0.1:4098"
|
||||
await mockRemoteServer(page, { directory: "/remote/settings-demo" })
|
||||
await mockOpenCodeServer(page, {
|
||||
server: paired,
|
||||
directory: "/remote/paired",
|
||||
project: project({ id: "proj_paired", directory: "/remote/paired" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
password: "session-token",
|
||||
pairing: { code: "one-time-code", token: "session-token" },
|
||||
})
|
||||
const { settings } = await open(page)
|
||||
const link = `${paired}/auth/connect/one-time-code`
|
||||
|
||||
const addServer = async () => {
|
||||
await settings.locator('[data-component="settings-nav-group-header"]').filter({ hasText: "Servers" }).hover()
|
||||
await settings.getByRole("button", { name: "Add server" }).click()
|
||||
const editor = page.getByRole("dialog", { name: "Add server" })
|
||||
await editor.getByLabel("Pairing link", { exact: true }).fill(link)
|
||||
await editor.getByRole("button", { name: "Add server", exact: true }).click()
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
const added = await addServer()
|
||||
await expect(added).toBeHidden()
|
||||
await expect(settings.getByText(paired, { exact: true })).toBeVisible()
|
||||
await settings.getByRole("button", { name: "Back to settings" }).click()
|
||||
|
||||
const spent = await addServer()
|
||||
await expect(spent.getByRole("alert")).toHaveText(
|
||||
"This pairing link expired or was already used. Run opencode pair to get a new one.",
|
||||
)
|
||||
})
|
||||
|
||||
test("a pairing code with several addresses keeps the first one that works with its token", async ({ page }) => {
|
||||
// Both addresses reach one server. The second spends the code first, so the first answers 401, and the token works on
|
||||
// it as on every address of that server.
|
||||
const first = "http://127.0.0.1:4098"
|
||||
const second = "http://127.0.0.1:4099"
|
||||
await mockRemoteServer(page, { directory: "/remote/settings-demo" })
|
||||
|
||||
for (const [server, pairing] of [
|
||||
[first, { code: "spent-by-the-other-address", token: "session-token" }],
|
||||
[second, { code: "one-time-code", token: "session-token" }],
|
||||
] as const) {
|
||||
await mockOpenCodeServer(page, {
|
||||
server,
|
||||
directory: "/remote/paired",
|
||||
project: project({ id: "proj_paired", directory: "/remote/paired" }),
|
||||
provider: NO_PROVIDER,
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
password: "session-token",
|
||||
pairing,
|
||||
})
|
||||
}
|
||||
|
||||
const { settings } = await open(page)
|
||||
await settings.locator('[data-component="settings-nav-group-header"]').filter({ hasText: "Servers" }).hover()
|
||||
await settings.getByRole("button", { name: "Add server" }).click()
|
||||
const editor = page.getByRole("dialog", { name: "Add server" })
|
||||
await editor
|
||||
.getByLabel("Pairing link", { exact: true })
|
||||
.fill(JSON.stringify({ code: "one-time-code", urls: [first, second] }))
|
||||
await editor.getByRole("button", { name: "Add server", exact: true }).click()
|
||||
|
||||
await expect(editor).toBeHidden()
|
||||
await expect(settings.getByText(first, { exact: true })).toBeVisible()
|
||||
await expect(settings.getByText(second, { exact: true })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("the tab layout preference switches to vertical tabs and survives reload", async ({ page }) => {
|
||||
const { settings } = await open(page)
|
||||
const layout = settings.locator('[data-action="settings-tab-layout"]')
|
||||
|
||||
@@ -56,71 +56,6 @@ test("tab strip keeps draft tabs as wide as session tabs and navigates on mouse
|
||||
await expectPath(page, sessionHref(b.id))
|
||||
})
|
||||
|
||||
test("inactive session attention dots share the close control without marking the avatar or active tab", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockWorkspace(page, {
|
||||
name: "Tabs",
|
||||
sessions: [a, b],
|
||||
sessionPermissions: {
|
||||
[b.id]: [
|
||||
{ id: "permission-tab-b", sessionID: b.id, action: "shell", resources: ["git status"], metadata: {}, save: [] },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
await page.goto(sessionHref(a.id))
|
||||
const tabA = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(a.id)}"])`)
|
||||
const tabB = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(b.id)}"])`)
|
||||
const dot = tabB.locator('[data-slot="tab-unread-dot"]')
|
||||
const close = tabB.getByRole("button", { name: "Close tab", exact: true })
|
||||
await expect(tabA.locator('[data-slot="tab-title"]')).toHaveText(a.title)
|
||||
await expect(tabB.locator('[data-slot="tab-title"]')).toHaveText(b.title)
|
||||
await page.mouse.move(0, 0)
|
||||
await expect(dot).toBeVisible()
|
||||
await expect(tabB.locator('[data-slot="tab-avatar-unread-dot"]')).toBeHidden()
|
||||
await expect(close).toHaveCSS("opacity", "0")
|
||||
await expect(tabB.locator('[data-slot="project-avatar-unread-dot"]')).toHaveCount(0)
|
||||
await expect
|
||||
.poll(() =>
|
||||
dot.evaluate((element) => {
|
||||
const dot = element.getBoundingClientRect()
|
||||
const slot = element.parentElement!.getBoundingClientRect()
|
||||
|
||||
return (
|
||||
Math.abs(dot.x + dot.width / 2 - slot.x - slot.width / 2) +
|
||||
Math.abs(dot.y + dot.height / 2 - slot.y - slot.height / 2)
|
||||
)
|
||||
}),
|
||||
)
|
||||
.toBeLessThan(1)
|
||||
|
||||
await tabB.hover()
|
||||
await expect(dot).toBeHidden()
|
||||
await expect(close).toHaveCSS("opacity", "1")
|
||||
await page.mouse.move(0, 0)
|
||||
await tabB.locator("[data-titlebar-tab-link]").focus()
|
||||
await page.keyboard.press("Tab")
|
||||
await expect(close).toBeFocused()
|
||||
await expect(dot).toBeHidden()
|
||||
await expect(close).toHaveCSS("opacity", "1")
|
||||
await close.blur()
|
||||
await expect(dot).toBeVisible()
|
||||
|
||||
await tabB.locator("[data-titlebar-tab-link]").click()
|
||||
await expectPath(page, sessionHref(b.id))
|
||||
await expect(tabB).toHaveAttribute("data-active", "true")
|
||||
await expect(dot).toHaveCount(0)
|
||||
await tabA.locator("[data-titlebar-tab-link]").click()
|
||||
await expectPath(page, sessionHref(a.id))
|
||||
await page.mouse.move(0, 0)
|
||||
await expect(dot).toBeVisible()
|
||||
await tabB.hover()
|
||||
await close.click()
|
||||
await expect(tabB).toHaveCount(0)
|
||||
await expectPath(page, sessionHref(a.id))
|
||||
})
|
||||
|
||||
test("a tab does not reopen its title editor while a rename is saving", async ({ page }) => {
|
||||
await mockWorkspace(page, { name: "Tabs", sessions: [a, b] })
|
||||
await page.goto(sessionHref(a.id))
|
||||
@@ -163,112 +98,43 @@ test("keyboard navigation follows the visible tab order and skips unresolved tab
|
||||
})
|
||||
|
||||
for (const row of [
|
||||
{ tabLayout: "horizontal", width: 360, hasTouch: false, direction: "ltr" },
|
||||
{ tabLayout: "vertical", width: 390, hasTouch: true, direction: "rtl" },
|
||||
{ tabLayout: "horizontal", width: 360 },
|
||||
{ tabLayout: "vertical", width: 390 },
|
||||
]) {
|
||||
test.describe(`mobile drawer (${row.tabLayout})`, () => {
|
||||
test.use({ hasTouch: row.hasTouch })
|
||||
test(`mobile drawer exposes close controls and navigates between tabs (${row.tabLayout})`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: row.width, height: 720 })
|
||||
await mockWorkspace(page, {
|
||||
name: "Tabs",
|
||||
sessions: [a, b, c],
|
||||
sessionPermissions: {
|
||||
[b.id]: [
|
||||
{
|
||||
id: "permission-mobile-b",
|
||||
sessionID: b.id,
|
||||
action: "shell",
|
||||
resources: ["git status"],
|
||||
metadata: {},
|
||||
save: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
seed: { settings: { appearance: { tabLayout: row.tabLayout } } },
|
||||
})
|
||||
await page.goto(sessionHref(a.id))
|
||||
await page.locator("html").evaluate((element, direction) => element.setAttribute("dir", direction), row.direction)
|
||||
await page.getByRole("button", { name: "Tabs", exact: true }).click()
|
||||
|
||||
const drawer = page.locator('[data-slot="mobile-tabs-drawer"]')
|
||||
const tabA = drawer.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(a.id)}"])`)
|
||||
const tabB = drawer.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(b.id)}"])`)
|
||||
await expect(tabA).toHaveAttribute("data-active", "true")
|
||||
await expect(tabA.locator('[data-slot="tab-close"]')).toBeVisible()
|
||||
await expect(tabB.locator('[data-slot="tab-close"]')).toBeVisible()
|
||||
await expect(tabB.locator('[data-slot="tab-unread-dot"]')).toBeVisible()
|
||||
await expect(tabB.locator('[data-slot="tab-unread-slot"]')).toHaveCSS("width", "20px")
|
||||
await expect(tabB.locator('[data-slot="tab-unread-slot"]')).toHaveCSS("height", "20px")
|
||||
await expect
|
||||
.poll(() =>
|
||||
tabB.evaluate((element) => {
|
||||
const dot = element.querySelector('[data-slot="tab-unread-slot"]')!.getBoundingClientRect()
|
||||
const close = element.querySelector('[data-slot="tab-close"]')!.getBoundingClientRect()
|
||||
|
||||
return dot.right <= close.left || dot.left >= close.right
|
||||
}),
|
||||
)
|
||||
.toBe(true)
|
||||
await expect(tabB.locator('[data-slot="tab-avatar-unread-dot"]')).toBeHidden()
|
||||
await expect(tabB.getByRole("button", { name: "Close tab", exact: true })).toHaveCSS("opacity", "1")
|
||||
await expect(page.locator('[data-slot="vertical-tabs-sidebar"]')).toHaveCount(0)
|
||||
|
||||
await tabB.locator(`a[href="${sessionHref(b.id)}"]`).click()
|
||||
|
||||
await expectPath(page, sessionHref(b.id))
|
||||
await expect(page.getByRole("dialog", { name: "Tabs", exact: true })).toBeHidden()
|
||||
|
||||
if (row.tabLayout !== "vertical") return
|
||||
await page.setViewportSize({ width: 1280, height: 720 })
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-slot="vertical-tabs-sidebar"]')
|
||||
.locator(`[data-titlebar-tab-link][href="${sessionHref(b.id)}"]`),
|
||||
).toBeVisible()
|
||||
await expect(page.locator('[data-slot="titlebar-tabs"]')).toHaveCount(0)
|
||||
test(`mobile drawer exposes close controls and navigates between tabs (${row.tabLayout})`, async ({ page }) => {
|
||||
await page.setViewportSize({ width: row.width, height: 720 })
|
||||
await mockWorkspace(page, {
|
||||
name: "Tabs",
|
||||
sessions: [a, b, c],
|
||||
seed: { settings: { appearance: { tabLayout: row.tabLayout } } },
|
||||
})
|
||||
await page.goto(sessionHref(a.id))
|
||||
await page.getByRole("button", { name: "Tabs", exact: true }).click()
|
||||
|
||||
const drawer = page.locator('[data-slot="mobile-tabs-drawer"]')
|
||||
const tabA = drawer.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(a.id)}"])`)
|
||||
const tabB = drawer.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(b.id)}"])`)
|
||||
await expect(tabA).toHaveAttribute("data-active", "true")
|
||||
await expect(tabA.locator('[data-slot="tab-close"]')).toBeVisible()
|
||||
await expect(tabB.locator('[data-slot="tab-close"]')).toBeVisible()
|
||||
await expect(page.locator('[data-slot="vertical-tabs-sidebar"]')).toHaveCount(0)
|
||||
|
||||
await tabB.locator(`a[href="${sessionHref(b.id)}"]`).click()
|
||||
|
||||
await expectPath(page, sessionHref(b.id))
|
||||
await expect(page.getByRole("dialog", { name: "Tabs", exact: true })).toBeHidden()
|
||||
|
||||
if (row.tabLayout !== "vertical") return
|
||||
await page.setViewportSize({ width: 1280, height: 720 })
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-slot="vertical-tabs-sidebar"]')
|
||||
.locator(`[data-titlebar-tab-link][href="${sessionHref(b.id)}"]`),
|
||||
).toBeVisible()
|
||||
await expect(page.locator('[data-slot="titlebar-tabs"]')).toHaveCount(0)
|
||||
})
|
||||
}
|
||||
|
||||
test("icon-only inactive tabs retain their avatar and attention badge instead of showing a close overlay", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockWorkspace(page, {
|
||||
name: "CompactTabs",
|
||||
sessions: [
|
||||
a,
|
||||
b,
|
||||
...Array.from({ length: 30 }, (_, index) => ({ id: `ses_compact_${index}`, title: `Compact ${index}` })),
|
||||
],
|
||||
sessionPermissions: {
|
||||
[b.id]: [
|
||||
{
|
||||
id: "permission-compact-b",
|
||||
sessionID: b.id,
|
||||
action: "shell",
|
||||
resources: ["git status"],
|
||||
metadata: {},
|
||||
save: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
await page.goto(sessionHref(a.id))
|
||||
const tabB = page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(b.id)}"])`)
|
||||
await expect(tabB.locator('[data-slot="tab-title"]')).toBeHidden()
|
||||
await expect(tabB.locator('[data-component="project-avatar-v2"]')).toBeVisible()
|
||||
await expect(tabB.locator('[data-slot="tab-avatar-unread-dot"]')).toBeVisible()
|
||||
await expect(tabB.locator('[data-slot="tab-avatar-unread-dot"]')).toHaveCSS("width", "6px")
|
||||
await expect(tabB.locator('[data-slot="tab-avatar-unread-dot"]')).toHaveCSS("height", "6px")
|
||||
await expect(tabB.locator('[data-slot="tab-close"]')).toBeHidden()
|
||||
await tabB.hover()
|
||||
await expect(tabB.locator('[data-slot="tab-close"]')).toBeHidden()
|
||||
await tabB.locator("[data-titlebar-tab-link]").click()
|
||||
await expectPath(page, sessionHref(b.id))
|
||||
await expect(tabB.locator('[data-slot="tab-avatar-unread-dot"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("vertical tabs resize, scroll, show shortcut hints, and navigate", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 480 })
|
||||
const directory = "C:/OpenCode/Tabs"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"
|
||||
import { createServer, type ServerResponse } from "node:http"
|
||||
import type { AddressInfo } from "node:net"
|
||||
import { once } from "node:events"
|
||||
import { createHash } from "node:crypto"
|
||||
import { join, extname, relative, sep } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { build } from "vite"
|
||||
import { serviceWorker } from "../../vite.pwa"
|
||||
import { onDemandAsset, serviceWorker } from "../../vite.pwa"
|
||||
|
||||
type Site = {
|
||||
url: string
|
||||
@@ -127,14 +128,14 @@ const fixture = test.extend<{ site: Site }, { builds: Record<string, Record<stri
|
||||
|
||||
const file = builds[state.version][path]
|
||||
|
||||
const types: Record<string, string> = {
|
||||
".js": "text/javascript",
|
||||
".html": "text/html",
|
||||
".json": "application/json",
|
||||
".wasm": "application/wasm",
|
||||
}
|
||||
const types = new Map([
|
||||
[".js", "text/javascript"],
|
||||
[".html", "text/html"],
|
||||
[".json", "application/json"],
|
||||
[".wasm", "application/wasm"],
|
||||
])
|
||||
|
||||
response.setHeader("content-type", types[extname(path)] ?? "application/octet-stream")
|
||||
response.setHeader("content-type", types.get(extname(path)) ?? "application/octet-stream")
|
||||
|
||||
if (file) return void response.end(file)
|
||||
|
||||
@@ -145,9 +146,8 @@ const fixture = test.extend<{ site: Site }, { builds: Record<string, Record<stri
|
||||
|
||||
server.listen(0, "127.0.0.1")
|
||||
await once(server, "listening")
|
||||
const address = server.address()
|
||||
|
||||
if (!address || typeof address === "string") throw new Error("Expected a TCP address")
|
||||
// SAFETY: a server listening on a host and port reports an AddressInfo; only pipe servers report a string.
|
||||
const address = server.address() as AddressInfo
|
||||
|
||||
try {
|
||||
await use({
|
||||
@@ -284,9 +284,11 @@ fixture(
|
||||
await expect
|
||||
.poll(() =>
|
||||
replacement.evaluate(() => {
|
||||
const registration = (self as unknown as { registration: ServiceWorkerRegistration }).registration
|
||||
// This runs in the service worker, whose global scope holds its registration.
|
||||
const registration =
|
||||
"registration" in self && self.registration instanceof ServiceWorkerRegistration ? self.registration : undefined
|
||||
|
||||
return { waiting: !!registration.waiting, active: registration.active?.state }
|
||||
return { waiting: !!registration?.waiting, active: registration?.active?.state }
|
||||
}),
|
||||
)
|
||||
.toEqual({ waiting: false, active: "activated" })
|
||||
@@ -378,15 +380,22 @@ fixture("does not substitute cached HTML for API or missing asset navigations",
|
||||
expect(await asset?.text()).toBe("Not found")
|
||||
})
|
||||
|
||||
test("the production build precaches every deployable file", async ({ page, context }) => {
|
||||
test("the production build precaches every deployable file except on-demand Office assets", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const directory = new URL("../../dist/", import.meta.url)
|
||||
|
||||
const files = (await readdir(directory, { recursive: true, withFileTypes: true }))
|
||||
const deployable = (await readdir(directory, { recursive: true, withFileTypes: true }))
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => "/" + relative(fileURLToPath(directory), join(entry.parentPath, entry.name)).split(sep).join("/"))
|
||||
.filter((path) => !path.endsWith(".map") && !["/_headers", "/_redirects", "/sw.js"].includes(path))
|
||||
|
||||
const files = deployable.filter((path) => !onDemandAsset(path))
|
||||
|
||||
expect(files.length).toBeGreaterThan(1)
|
||||
// The Office engines ship but load on demand, so a build without them would make this exclusion vacuous.
|
||||
expect(deployable.some((path) => path.endsWith(".wasm") && onDemandAsset(path))).toBe(true)
|
||||
|
||||
const server = createServer(async (request, response) => {
|
||||
const path = new URL(request.url ?? "/", "http://localhost").pathname
|
||||
@@ -411,9 +420,8 @@ test("the production build precaches every deployable file", async ({ page, cont
|
||||
|
||||
server.listen(0, "127.0.0.1")
|
||||
await once(server, "listening")
|
||||
const address = server.address()
|
||||
|
||||
if (!address || typeof address === "string") throw new Error("Expected a TCP address")
|
||||
// SAFETY: a server listening on a host and port reports an AddressInfo; only pipe servers report a string.
|
||||
const address = server.address() as AddressInfo
|
||||
const url = `http://127.0.0.1:${address.port}`
|
||||
|
||||
try {
|
||||
|
||||
@@ -135,6 +135,8 @@ export interface MockServerConfig {
|
||||
strictDirectory?: boolean
|
||||
// Answers 401 UnauthorizedError unless a request carries this password; a function may change it mid-test.
|
||||
password?: Resolvable<string>
|
||||
// Serves GET /auth/connect/:code: `code` redeems once for `token` (send it as the password); others answer 401.
|
||||
pairing?: { code: string; token: string }
|
||||
}
|
||||
|
||||
export type MockPtyInfo = {
|
||||
@@ -401,6 +403,29 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
||||
})
|
||||
})
|
||||
|
||||
if (config.pairing) {
|
||||
const pairing = { ...config.pairing, redeemed: false }
|
||||
|
||||
await page.route(`${server}/auth/connect/*`, (route) => {
|
||||
if (route.request().method() === "OPTIONS") return route.fulfill({ status: 204, headers: corsHeaders })
|
||||
const code = decodeURIComponent(new URL(route.request().url()).pathname.split("/").at(-1) ?? "")
|
||||
|
||||
if (code !== pairing.code || pairing.redeemed) {
|
||||
return route.fulfill({
|
||||
status: 401,
|
||||
headers: corsHeaders,
|
||||
json: Schema.encodeSync(MockUnauthorized)(
|
||||
new MockUnauthorized({ message: "Pairing link expired or already used" }),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pairing.redeemed = true
|
||||
|
||||
return route.fulfill({ headers: corsHeaders, json: { token: pairing.token } })
|
||||
})
|
||||
}
|
||||
|
||||
if (config.pty) {
|
||||
const host = new URL(server).host
|
||||
await page.routeWebSocket(
|
||||
|
||||
@@ -31,7 +31,6 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
const settings = useSettingsSurface()
|
||||
const serverManagement = useServerActionsController()
|
||||
const global = useGlobal()
|
||||
const authenticate = ServerConnection.authenticate
|
||||
const revealProject = useRevealProject()
|
||||
const [_state, setState, _, ready] = persisted(Persist.global("home.servers"), HomeServersSchema, { collapsed: {} })
|
||||
|
||||
@@ -41,6 +40,23 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
{ initialValue: _state },
|
||||
)
|
||||
|
||||
function edit(conn: ServerConnection.Http, onSave?: (saved: ServerConnection.Http) => void) {
|
||||
void import("@/servers/connect/dialog").then(({ DialogServer }) => {
|
||||
void dialog.show(() => <DialogServer mode="edit" server={conn} onSave={(saved) => onSave?.(saved)} />)
|
||||
})
|
||||
}
|
||||
|
||||
// An expired pairing session or a changed password signs the app out of an HTTP server; signing in again edits it,
|
||||
// and the action that asked for sign-in continues once the dialog saves, as it does for extension servers. It continues
|
||||
// with the saved connection: the one it started with still carries the rejected credentials.
|
||||
function authenticate(conn: ServerConnection.Any, onConnected?: (conn: ServerConnection.Any) => void) {
|
||||
if (conn.type !== "http" || !home.server.health(conn)?.unauthorized)
|
||||
return ServerConnection.authenticate(conn, () => onConnected?.(conn))
|
||||
edit(conn, onConnected)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function directories(project: LocalProject) {
|
||||
return [project.worktree, ...(project.sandboxes ?? [])]
|
||||
}
|
||||
@@ -78,14 +94,10 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
remove: (conn: ServerConnection.Any) => serverManagement.connection.remove(ServerConnection.key(conn)),
|
||||
canHide: (conn: ServerConnection.Any) => serverManagement.connection.canHide(ServerConnection.key(conn)),
|
||||
hide: (conn: ServerConnection.Any) => serverManagement.connection.setHidden(ServerConnection.key(conn), true),
|
||||
edit: (conn: ServerConnection.Http) => {
|
||||
void import("@/servers/connect/dialog").then(({ DialogServer }) => {
|
||||
void dialog.show(() => <DialogServer mode="edit" server={conn} />)
|
||||
})
|
||||
},
|
||||
edit: (conn: ServerConnection.Http) => edit(conn),
|
||||
authenticate: (conn: ServerConnection.Any) => authenticate(conn),
|
||||
focus: (conn: ServerConnection.Any) => {
|
||||
if (authenticate(conn, () => home.selection.focusServer(conn))) return
|
||||
if (authenticate(conn, (next) => home.selection.focusServer(next))) return
|
||||
home.selection.focusServer(conn)
|
||||
},
|
||||
},
|
||||
@@ -94,12 +106,12 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
recentlyClosed: home.project.recentlyClosed,
|
||||
homedir: home.project.homedir,
|
||||
select: (conn: ServerConnection.Any, directory: string) => {
|
||||
if (authenticate(conn, () => home.project.select(conn, directory))) return
|
||||
if (authenticate(conn, (next) => home.project.select(next, directory))) return
|
||||
home.project.select(conn, directory)
|
||||
},
|
||||
add: home.project.add,
|
||||
openNewSession: (conn: ServerConnection.Any, directory: string) => {
|
||||
if (authenticate(conn, () => home.project.openProjectNewSession(conn, directory))) return
|
||||
if (authenticate(conn, (next) => home.project.openProjectNewSession(next, directory))) return
|
||||
home.project.openProjectNewSession(conn, directory)
|
||||
},
|
||||
canImportSession: !!platform.openAttachmentPickerDialog,
|
||||
@@ -119,6 +131,7 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
|
||||
const api = home.server.context(conn).sdk.api.session
|
||||
|
||||
// SAFETY: the payload is SessionTransfer.Data's own encoding, which the import endpoint decodes and validates.
|
||||
const imported = await api.import({
|
||||
...Schema.encodeSync(SessionTransfer.Data)(data),
|
||||
location: { directory: project.worktree },
|
||||
@@ -152,7 +165,7 @@ export function createHomeProjectsController(home: HomeController) {
|
||||
.forEach((directory) => notification.project.markViewed(directory))
|
||||
},
|
||||
choose: (conn: ServerConnection.Any) => {
|
||||
if (authenticate(conn, () => choose(conn))) return
|
||||
if (authenticate(conn, (next) => choose(next))) return
|
||||
|
||||
if (home.server.health(conn)?.healthy === false) return
|
||||
choose(conn)
|
||||
|
||||
@@ -33,6 +33,11 @@ const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whi
|
||||
|
||||
const serverContextMenuID = (server: ServerConnection.Any) => `server:${ServerConnection.key(server)}`
|
||||
|
||||
// Extension servers ask for sign-in themselves; an HTTP server that rejects its saved credentials needs a new
|
||||
// pairing link or password.
|
||||
const signInRequired = (server: ServerConnection.Any, health: ServerHealth | undefined) =>
|
||||
(server.type === "extension" && server.authenticationRequired) || (server.type === "http" && !!health?.unauthorized)
|
||||
|
||||
const projectContextMenuID = (server: ServerConnection.Any, directory: string) =>
|
||||
`project:${ServerConnection.key(server)}:${directory}`
|
||||
|
||||
@@ -158,7 +163,7 @@ export function HomeProjectsView(props: HomeProjectsViewProps) {
|
||||
}
|
||||
|
||||
function HomeProjectsPanel(props: HomeProjectsViewProps) {
|
||||
const [contextMenu, setContextMenu] = createStore({ open: undefined as string | undefined })
|
||||
const [contextMenu, setContextMenu] = createStore<{ open: string | undefined }>({ open: undefined })
|
||||
|
||||
const contextMenuProps = {
|
||||
contextMenuOpen: (id: string) => contextMenu.open === id,
|
||||
@@ -216,7 +221,9 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
|
||||
when={
|
||||
props.servers.length > 1 ||
|
||||
props.servers.some(
|
||||
(server) => server.type === "extension" && (server.authenticationRequired || server.connecting),
|
||||
(server) =>
|
||||
signInRequired(server, props.serverHealth(server)) ||
|
||||
(server.type === "extension" && server.connecting),
|
||||
)
|
||||
}
|
||||
fallback={
|
||||
@@ -253,7 +260,7 @@ function HomeProjectsPanel(props: HomeProjectsViewProps) {
|
||||
const healthy = () => !!props.serverHealth(item)?.healthy
|
||||
const hasProjects = () => projects().length > 0
|
||||
const collapsed = () => props.collapsed(item)
|
||||
const authentication = () => item.type === "extension" && item.authenticationRequired
|
||||
const authentication = () => signInRequired(item, props.serverHealth(item))
|
||||
const connecting = () => item.type === "extension" && item.connecting
|
||||
|
||||
return (
|
||||
@@ -358,7 +365,7 @@ function HomeServerRow(props: {
|
||||
health: ServerHealth | undefined
|
||||
}) {
|
||||
const healthy = () => !!props.health?.healthy
|
||||
const authentication = () => props.server.type === "extension" && props.server.authenticationRequired
|
||||
const authentication = () => signInRequired(props.server, props.health)
|
||||
const incompatible = () => !!props.health?.incompatible
|
||||
const canToggle = () => healthy() && props.projectsForServer(props.server).length > 0
|
||||
const contextMenuID = () => serverContextMenuID(props.server)
|
||||
|
||||
@@ -7,14 +7,23 @@ import type {
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useServerSDK } from "@/runtime/server/client"
|
||||
import { formatServerError } from "@/runtime/server/errors"
|
||||
import { useData } from "@/runtime/server/current"
|
||||
import { createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
|
||||
export type ProviderConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
|
||||
export type ProviderConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" | "external" }>
|
||||
|
||||
type Authorization = IntegrationOauthConnectOutput["data"]
|
||||
|
||||
type Polling = {
|
||||
generation: number
|
||||
timer?: ReturnType<typeof setTimeout>
|
||||
disposed: boolean
|
||||
// An attempt the server still considers open; cancelled when the dialog goes away.
|
||||
attempt?: Authorization
|
||||
}
|
||||
|
||||
// OpenCode Go and OpenCode Zen both bill through the OpenCode Console, so the
|
||||
// Console sign-in is the connection method for both providers.
|
||||
export const CONSOLE_INTEGRATION = "opencode"
|
||||
@@ -35,7 +44,7 @@ export function providerFormDefaults(fields: ProviderConnectMethod["form"]) {
|
||||
if (actual === undefined) return false
|
||||
|
||||
const equal = Array.isArray(actual)
|
||||
? typeof condition.value === "string" && actual.includes(condition.value)
|
||||
? actual.some((item) => item === condition.value)
|
||||
: actual === condition.value
|
||||
|
||||
return condition.op === "eq" ? equal : !equal
|
||||
@@ -43,7 +52,7 @@ export function providerFormDefaults(fields: ProviderConnectMethod["form"]) {
|
||||
|
||||
if (!active) return answer
|
||||
|
||||
return { ...answer, [field.key]: field.default }
|
||||
return Object.assign(answer, { [field.key]: field.default })
|
||||
}, {})
|
||||
}
|
||||
|
||||
@@ -74,9 +83,8 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
// Not createResource: the dialog is owned by whichever page opened it, so reading a pending
|
||||
// resource here would suspend that page's <Suspense> and blank the screen behind the dialog.
|
||||
const [integration, setIntegration] = createStore({
|
||||
const [integration, setIntegration] = createStore<{ loading: boolean; latest?: IntegrationInfo }>({
|
||||
loading: true,
|
||||
latest: undefined as IntegrationInfo | undefined,
|
||||
})
|
||||
|
||||
createEffect(
|
||||
@@ -100,7 +108,8 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
const methods = createMemo<ProviderConnectMethod[]>(() => {
|
||||
const values = integration.latest?.methods.filter(
|
||||
(method): method is ProviderConnectMethod => method.type === "key" || method.type === "oauth",
|
||||
(method): method is ProviderConnectMethod =>
|
||||
method.type === "key" || method.type === "oauth" || method.type === "external",
|
||||
)
|
||||
|
||||
if (values?.length) return [...values]
|
||||
@@ -108,14 +117,19 @@ export function createProviderConnectionController(options: {
|
||||
return [{ type: "key", label: language.t("provider.connect.method.apiKey") }]
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
methodIndex: undefined as number | undefined,
|
||||
authorization: undefined as Authorization | undefined,
|
||||
formAnswer: undefined as FormAnswer | undefined,
|
||||
const [store, setStore] = createStore<{
|
||||
methodIndex?: number
|
||||
authorization?: Authorization
|
||||
formAnswer?: FormAnswer
|
||||
// Nothing is in flight until a method is selected; `busy()` reads this, so a truthy initial
|
||||
// value would keep multi-method providers on the spinner instead of the method list.
|
||||
state: undefined as "pending" | "waiting" | "refreshing" | "ready" | "error" | "form" | undefined,
|
||||
error: undefined as string | undefined,
|
||||
state?: "pending" | "waiting" | "refreshing" | "ready" | "error" | "form"
|
||||
error?: string
|
||||
auto: boolean
|
||||
connected: boolean
|
||||
browserFailed: boolean
|
||||
statusFailed: boolean
|
||||
}>({
|
||||
auto: false,
|
||||
// The credential is stored; a retry only needs to reload the catalogs.
|
||||
connected: false,
|
||||
@@ -124,12 +138,9 @@ export function createProviderConnectionController(options: {
|
||||
statusFailed: false,
|
||||
})
|
||||
|
||||
const polling = {
|
||||
const polling: Polling = {
|
||||
generation: 0,
|
||||
timer: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
disposed: false,
|
||||
// An attempt the server still considers open; cancelled when the dialog goes away.
|
||||
attempt: undefined as Authorization | undefined,
|
||||
}
|
||||
|
||||
const currentMethod = createMemo(() =>
|
||||
@@ -206,8 +217,6 @@ export function createProviderConnectionController(options: {
|
||||
)
|
||||
}
|
||||
|
||||
const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error))
|
||||
|
||||
const cancelAttempt = () => {
|
||||
const attempt = polling.attempt
|
||||
polling.attempt = undefined
|
||||
@@ -276,7 +285,7 @@ export function createProviderConnectionController(options: {
|
||||
setStore("statusFailed", true)
|
||||
dispatch({
|
||||
type: "auth.error",
|
||||
error: isConsole() ? language.t("provider.connect.console.statusFailed") : errorMessage(result.error),
|
||||
error: isConsole() ? language.t("provider.connect.console.statusFailed") : formatServerError(result.error, language.t),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -358,8 +367,6 @@ export function createProviderConnectionController(options: {
|
||||
return
|
||||
}
|
||||
|
||||
if (selected.type !== "oauth") return
|
||||
|
||||
if (selected.form?.some((field) => field.type !== "string")) {
|
||||
dispatch({ type: "auth.error", error: language.t("provider.connect.error.unsupportedFields") })
|
||||
|
||||
@@ -368,11 +375,35 @@ export function createProviderConnectionController(options: {
|
||||
|
||||
dispatch({ type: "auth.pending" })
|
||||
|
||||
if (selected.type === "external") {
|
||||
const saved = await serverSDK.api.integration.connect
|
||||
.external({
|
||||
integrationID: options.provider(),
|
||||
methodID: selected.id,
|
||||
answer: Object.keys(merged).length ? merged : undefined,
|
||||
location: location(),
|
||||
})
|
||||
.then(() => ({ ok: true as const }))
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (polling.disposed || generation !== polling.generation) return
|
||||
|
||||
if (!saved.ok) {
|
||||
dispatch({ type: "auth.error", error: formatServerError(saved.error, language.t) })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await finish()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const result = await serverSDK.api.integration.oauth
|
||||
.connect({
|
||||
integrationID: options.provider(),
|
||||
methodID: selected.id,
|
||||
...(Object.keys(merged).length ? { answer: merged } : {}),
|
||||
answer: Object.keys(merged).length ? merged : undefined,
|
||||
location: location(),
|
||||
})
|
||||
.then((response) => {
|
||||
@@ -404,7 +435,7 @@ export function createProviderConnectionController(options: {
|
||||
if (!result.ok) {
|
||||
dispatch({
|
||||
type: "auth.error",
|
||||
error: isConsole() ? language.t("provider.connect.console.startFailed") : errorMessage(result.error),
|
||||
error: isConsole() ? language.t("provider.connect.console.startFailed") : formatServerError(result.error, language.t),
|
||||
})
|
||||
|
||||
return
|
||||
@@ -448,7 +479,7 @@ export function createProviderConnectionController(options: {
|
||||
integrationID: options.keyProvider?.() ?? options.provider(),
|
||||
location: location(),
|
||||
key,
|
||||
...(store.formAnswer ? { answer: store.formAnswer } : {}),
|
||||
answer: store.formAnswer,
|
||||
})
|
||||
await finish()
|
||||
}
|
||||
@@ -468,7 +499,8 @@ export function createProviderConnectionController(options: {
|
||||
.then(() => ({ ok: true as const }))
|
||||
.catch((error) => ({ ok: false as const, error }))
|
||||
|
||||
if (!result.ok) return errorMessage(result.error) || language.t("provider.connect.oauth.code.invalid")
|
||||
if (!result.ok)
|
||||
return formatServerError(result.error, language.t, language.t("provider.connect.oauth.code.invalid"))
|
||||
await finish()
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Badge } from "@opencode/ui/badge"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { List } from "@opencode/ui/list"
|
||||
import { RadioGroup, RadioItem } from "@opencode/ui/radio"
|
||||
import { Spinner } from "@opencode/ui/spinner"
|
||||
import { TextField } from "@opencode/ui/text-field"
|
||||
import { DialogBody, DialogHeader, DialogTitle, Dialog } from "@opencode/ui/dialog"
|
||||
@@ -61,7 +62,7 @@ type IntegrationForm = NonNullable<ProviderConnectMethod["form"]>[number]
|
||||
type StringForm = Extract<IntegrationForm, { type: "string" }>
|
||||
|
||||
export function useProviderConnectController() {
|
||||
const [store, setStore] = createStore({ selected: undefined as string | undefined })
|
||||
const [store, setStore] = createStore<{ selected?: string }>({})
|
||||
const reset = () => setStore("selected", undefined)
|
||||
|
||||
return {
|
||||
@@ -83,9 +84,13 @@ export const DialogConnectProvider: Component<{
|
||||
const fallback = useProviderConnectController()
|
||||
const controller = props.controller ?? fallback
|
||||
|
||||
const [state, setState] = createStore({
|
||||
const [state, setState] = createStore<{
|
||||
completed: boolean
|
||||
modelProvider?: { id: string; name: string }
|
||||
authorization: boolean
|
||||
chatgptWelcome: boolean
|
||||
}>({
|
||||
completed: false,
|
||||
modelProvider: undefined as { id: string; name: string } | undefined,
|
||||
authorization: false,
|
||||
chatgptWelcome: false,
|
||||
})
|
||||
@@ -216,10 +221,12 @@ function ProviderPicker(props: { directory?: string; onSelect: (provider: string
|
||||
const integrations = useIntegrations(() => props.directory)
|
||||
const language = useLanguage()
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
const [store, setStore] = createStore<{
|
||||
filter: string
|
||||
active?: string
|
||||
connecting?: string
|
||||
}>({
|
||||
filter: "",
|
||||
active: undefined as string | undefined,
|
||||
connecting: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const featured = ["opencode-go", "opencode", "anthropic", "openai", "google", "openrouter", "vercel"]
|
||||
@@ -421,16 +428,24 @@ function ProviderConnection(props: {
|
||||
const isConsole = CONSOLE_PROVIDERS.has(props.provider)
|
||||
const remote = isConsole && authServerName(sdk.server) !== undefined
|
||||
|
||||
const [state, setState] = createStore({
|
||||
const [state, setState] = createStore<{
|
||||
copied: boolean
|
||||
copyFailed: boolean
|
||||
firstConnection?: boolean
|
||||
models: boolean
|
||||
noModels: boolean
|
||||
catalogPending: boolean
|
||||
selectedModel: string
|
||||
collapsed: Record<string, boolean>
|
||||
}>({
|
||||
copied: false,
|
||||
copyFailed: false,
|
||||
firstConnection: undefined as boolean | undefined,
|
||||
models: false,
|
||||
noModels: false,
|
||||
// The workspace providers had not loaded when the wait ran out.
|
||||
catalogPending: false,
|
||||
selectedModel: "",
|
||||
collapsed: {} as Record<string, boolean>,
|
||||
collapsed: {},
|
||||
})
|
||||
|
||||
const controller = createProviderConnectionController({
|
||||
@@ -642,11 +657,22 @@ function ProviderConnection(props: {
|
||||
function AuthFormView() {
|
||||
const defaults = providerFormDefaults(controller.currentMethod()?.form)
|
||||
|
||||
const [formStore, setFormStore] = createStore({
|
||||
const [formStore, setFormStore] = createStore<{
|
||||
value: Record<string, string>
|
||||
index: number
|
||||
customOn: boolean
|
||||
customText: string
|
||||
}>({
|
||||
value: Object.fromEntries(
|
||||
Object.entries(defaults).flatMap(([key, value]) => (typeof value === "string" ? [[key, value]] : [])),
|
||||
) as Record<string, string>,
|
||||
(controller.currentMethod()?.form ?? []).flatMap((field) =>
|
||||
field.type === "string" && field.key in defaults && field.default !== undefined
|
||||
? [[field.key, field.default]]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
index: 0,
|
||||
customOn: false,
|
||||
customText: "",
|
||||
})
|
||||
|
||||
const fields = createMemo<StringForm[]>(() => {
|
||||
@@ -677,14 +703,18 @@ function ProviderConnection(props: {
|
||||
}
|
||||
})
|
||||
|
||||
// Visible defaults prefill the field but enter the answer only once that field is answered,
|
||||
// because the server rejects values for fields whose conditions are not met.
|
||||
const answer = (field: StringForm) => formStore.value[field.key] ?? field.default
|
||||
|
||||
const valid = createMemo(() => {
|
||||
const item = current()
|
||||
|
||||
if (!item || item.field.options) return false
|
||||
if (!item || (item.field.options && !item.field.custom)) return false
|
||||
|
||||
if (!item.field.required) return true
|
||||
|
||||
return (formStore.value[item.field.key] ?? "").trim().length > 0
|
||||
return (answer(item.field) ?? "").trim().length > 0
|
||||
})
|
||||
|
||||
async function next(index: number, value: Record<string, string>) {
|
||||
@@ -694,7 +724,7 @@ function ProviderConnection(props: {
|
||||
const next = fields().findIndex((field, i) => i > index && matches(field, value))
|
||||
|
||||
if (next !== -1) {
|
||||
setFormStore("index", next)
|
||||
setFormStore({ index: next, customOn: false, customText: "" })
|
||||
|
||||
return
|
||||
}
|
||||
@@ -706,9 +736,10 @@ function ProviderConnection(props: {
|
||||
e.preventDefault()
|
||||
const item = current()
|
||||
|
||||
if (!item || item.field.options) return
|
||||
if (!item || !valid()) return
|
||||
const value = answer(item.field)
|
||||
|
||||
if (!valid()) return
|
||||
if (value !== undefined) setFormStore("value", item.field.key, value)
|
||||
await next(item.index, formStore.value)
|
||||
}
|
||||
|
||||
@@ -730,6 +761,15 @@ function ProviderConnection(props: {
|
||||
return field
|
||||
})
|
||||
|
||||
const custom = () => {
|
||||
const field = select()
|
||||
|
||||
if (!field?.custom) return false
|
||||
const value = answer(field)
|
||||
|
||||
return formStore.customOn || (value !== undefined && !field.options?.some((option) => option.value === value))
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4 px-3">
|
||||
<Switch>
|
||||
@@ -738,7 +778,7 @@ function ProviderConnection(props: {
|
||||
type="text"
|
||||
label={text()?.title ?? ""}
|
||||
placeholder={text()?.placeholder}
|
||||
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
|
||||
value={text() ? (answer(text()!) ?? "") : ""}
|
||||
onChange={(value) => {
|
||||
const field = text()
|
||||
|
||||
@@ -750,6 +790,62 @@ function ProviderConnection(props: {
|
||||
{language.t("common.continue")}
|
||||
</Button>
|
||||
</Match>
|
||||
<Match when={select()?.custom && select()}>
|
||||
{(field) => (
|
||||
<>
|
||||
<div class="w-full flex flex-col gap-3">
|
||||
<div class="text-14-regular text-text-base">{field().title}</div>
|
||||
<RadioGroup
|
||||
label={field().title}
|
||||
hideLabel
|
||||
description={field().description}
|
||||
value={
|
||||
custom()
|
||||
? "custom"
|
||||
: String(field().options?.findIndex((option) => option.value === answer(field())))
|
||||
}
|
||||
onChange={(value) => {
|
||||
const option = field().options?.[Number(value)]
|
||||
|
||||
if (option) {
|
||||
if (custom()) setFormStore("customText", answer(field()) ?? "")
|
||||
setFormStore("customOn", false)
|
||||
setFormStore("value", field().key, option.value)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("customOn", true)
|
||||
setFormStore("value", field().key, formStore.customText)
|
||||
}}
|
||||
>
|
||||
<For each={field().options}>
|
||||
{(option, index) => (
|
||||
<RadioItem value={String(index())} label={option.label} description={option.description} />
|
||||
)}
|
||||
</For>
|
||||
<RadioItem value="custom" label={language.t("ui.messagePart.option.typeOwnAnswer")} />
|
||||
<Show when={custom()}>
|
||||
<div class="pl-6">
|
||||
<TextField
|
||||
ref={(input: HTMLInputElement) => queueMicrotask(() => input.focus())}
|
||||
type="text"
|
||||
label={language.t("ui.messagePart.option.typeOwnAnswer")}
|
||||
hideLabel
|
||||
placeholder={field().placeholder ?? language.t("ui.question.custom.placeholder")}
|
||||
value={answer(field()) ?? ""}
|
||||
onChange={(value) => setFormStore("value", field().key, value)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Button class="w-auto" type="submit" size="large" variant="contrast" disabled={!valid()}>
|
||||
{language.t("common.continue")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={item()?.field.options !== undefined}>
|
||||
<div class="w-full flex flex-col gap-1.5">
|
||||
<div class="text-14-regular text-text-base">{select()?.title}</div>
|
||||
@@ -758,7 +854,7 @@ function ProviderConnection(props: {
|
||||
class="px-3"
|
||||
items={select()?.options ?? []}
|
||||
key={(x) => x.value}
|
||||
current={select()?.options?.find((x) => x.value === formStore.value[select()!.key])}
|
||||
current={select()?.options?.find((x) => x.value === answer(select()!))}
|
||||
onSelect={(value) => {
|
||||
if (!value) return
|
||||
const field = select()
|
||||
@@ -876,9 +972,11 @@ function ProviderConnection(props: {
|
||||
let apiKey: HTMLInputElement | undefined
|
||||
const errorID = createUniqueId()
|
||||
|
||||
const [formStore, setFormStore] = createStore({
|
||||
const [formStore, setFormStore] = createStore<{
|
||||
value: string
|
||||
error?: string
|
||||
}>({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -890,16 +988,16 @@ function ProviderConnection(props: {
|
||||
|
||||
if (!(e.currentTarget instanceof HTMLFormElement)) return
|
||||
const value = new FormData(e.currentTarget).get("apiKey")
|
||||
const apiKey = typeof value === "string" ? value : ""
|
||||
const key = value instanceof File || value === null ? "" : value
|
||||
|
||||
if (!apiKey?.trim()) {
|
||||
if (!key.trim()) {
|
||||
setFormStore("error", language.t("provider.connect.apiKey.required"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setFormStore("error", undefined)
|
||||
await controller.auth.connectKey(apiKey)
|
||||
await controller.auth.connectKey(key)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -954,9 +1052,11 @@ function ProviderConnection(props: {
|
||||
let codeInput: HTMLInputElement | undefined
|
||||
const errorID = createUniqueId()
|
||||
|
||||
const [formStore, setFormStore] = createStore({
|
||||
const [formStore, setFormStore] = createStore<{
|
||||
value: string
|
||||
error?: string
|
||||
}>({
|
||||
value: "",
|
||||
error: undefined as string | undefined,
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
@@ -968,9 +1068,9 @@ function ProviderConnection(props: {
|
||||
|
||||
if (!(e.currentTarget instanceof HTMLFormElement)) return
|
||||
const value = new FormData(e.currentTarget).get("code")
|
||||
const code = typeof value === "string" ? value : ""
|
||||
const code = value instanceof File || value === null ? "" : value
|
||||
|
||||
if (!code?.trim()) {
|
||||
if (!code.trim()) {
|
||||
setFormStore("error", language.t("provider.connect.oauth.code.required"))
|
||||
|
||||
return
|
||||
|
||||
@@ -69,6 +69,7 @@ type Attached = {
|
||||
keybind: (command: string) => readonly string[]
|
||||
matches: (command: string, event: KeyboardEvent) => boolean
|
||||
servers: Accessor<readonly string[]>
|
||||
server: (id: string) => ServerRef | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,7 +270,10 @@ export function createHostApis() {
|
||||
keys: (bind) => formatKeybindParts(bind, language.t),
|
||||
matches: (command, event) => current()?.matches(command, event) ?? false,
|
||||
}),
|
||||
servers: () => ({ list: () => current()?.servers() ?? [] }),
|
||||
servers: () => ({
|
||||
list: () => current()?.servers() ?? [],
|
||||
get: (id) => current()?.server(id),
|
||||
}),
|
||||
workspaces: (_extension, _owner, _context, register) => ({
|
||||
on(_event, handler) {
|
||||
removed.add(handler)
|
||||
@@ -389,15 +393,28 @@ export function createExtensionAttachment(apis: HostApis) {
|
||||
|
||||
if (existing) return existing
|
||||
const key = ServerConnection.Key.make(id)
|
||||
// The controller starts on first use, not when the ref is made: reading `builtin` to find a server must not
|
||||
// connect to every listed server, such as one the runtime holds back because it rejects its credentials.
|
||||
let started: Accessor<ServerCtx> | undefined
|
||||
|
||||
const live = runWithOwner(owner, () =>
|
||||
createMemo<ServerCtx>((previous) => global.serverCtx(key) ?? previous, global.ensureServerCtx(conn)),
|
||||
)!
|
||||
const live = () => {
|
||||
started ??= runWithOwner(owner, () =>
|
||||
createMemo<ServerCtx>(
|
||||
(previous) => global.serverCtx(key) ?? previous,
|
||||
global.ensureServerCtx(connection(id) ?? conn),
|
||||
),
|
||||
)!
|
||||
|
||||
return started()
|
||||
}
|
||||
|
||||
// What the app's server list already says, without a controller.
|
||||
const listed = () => connection(id) ?? live().sdk.server
|
||||
|
||||
const ref: ServerRef = {
|
||||
id,
|
||||
get name() {
|
||||
return serverName(live().sdk.server) || id
|
||||
return serverName(listed()) || id
|
||||
},
|
||||
get url() {
|
||||
return live().sdk.url
|
||||
@@ -412,10 +429,10 @@ export function createExtensionAttachment(apis: HostApis) {
|
||||
return live().data
|
||||
},
|
||||
get local() {
|
||||
return ServerConnection.local(live().sdk.server)
|
||||
return ServerConnection.local(listed())
|
||||
},
|
||||
get builtin() {
|
||||
return ServerConnection.builtin(live().sdk.server)
|
||||
return ServerConnection.builtin(listed())
|
||||
},
|
||||
get compatible() {
|
||||
return !global.servers.health[key]?.incompatible
|
||||
@@ -783,6 +800,7 @@ export function createExtensionAttachment(apis: HostApis) {
|
||||
keybind: command.keybindParts,
|
||||
matches: command.matches,
|
||||
servers: () => global.servers.list().map(ServerConnection.key),
|
||||
server,
|
||||
// SAFETY: an extension names a page it contributed through `SettingsPage`, which settings lists as an extension tab.
|
||||
settings: (page) => surface.open(page as Parameters<typeof surface.open>[0]),
|
||||
layout: {
|
||||
|
||||
@@ -775,14 +775,12 @@ export const dict = {
|
||||
"server.connect.button": "መገናኘት",
|
||||
"server.connect.address.invalid": "የሚሰራ የኤችቲቲፒ ወይም የኤችቲቲፒኤስ አገልጋይ አድራሻ ያስገቡ።",
|
||||
"server.connect.failed": "መገናኘት አልተቻለም። የአገልጋዩን አድራሻ እና የይለፍ ቃል ያረጋግጡ እና ከዚያ እንደገና ይሞክሩ።",
|
||||
"server.connect.pair.description": "የግንኙነት ዝርዝሮችዎን ለማግኘት ይህንን ትዕዛዝ በኮምፒተርዎ ላይ ያሂዱ።",
|
||||
"server.connect.scan": "QR ኮድ ይቃኙ",
|
||||
"server.connect.scan.description": "ካሜራዎን opencode pair ወደሚያሳየው QR ኮድ ያነጣጥሩ።",
|
||||
"server.connect.scan.invalid": "ይህ የOpenCode ማጣመሪያ ኮድ አይደለም። opencode pair የሚያሳየውን ኮድ ይቃኙ።",
|
||||
"server.connect.camera": "ማጣመር ካሜራ",
|
||||
"server.connect.camera.starting": "ካሜራ በመክፈት ላይ...",
|
||||
"server.connect.mixedContent": "ከዚህ ኤችቲቲፒኤስ አገልጋይ ጋር ከኤችቲቲፒኤስ ገጽ ጋር መገናኘት አልተቻለም። በምትኩ የኤችቲቲፒኤስ አገልጋይ አድራሻ ይጠቀሙ።",
|
||||
"server.connect.camera.insecure": "QR ቅኝት ይህንን ገጽ በኤችቲቲፒኤስ ወይም በአካባቢያዊ አስተናጋጅ መክፈት ይጠይቃል።",
|
||||
"server.connect.camera.unavailable": "ለዚህ አሳሽ ምንም ካሜራ አይገኝም። የግንኙነት ዝርዝሮችዎን እራስዎ ያስገቡ።",
|
||||
"server.connect.camera.error": "ካሜራውን መክፈት አልተቻለም። የካሜራ መዳረሻ ይፍቀዱ ወይም የግንኙነት ዝርዝሮችዎን እራስዎ ያስገቡ።",
|
||||
"dialog.server.menu.remove": "አስወግድ",
|
||||
|
||||
@@ -833,14 +833,12 @@ export const dict = {
|
||||
"server.connect.button": "اتصال",
|
||||
"server.connect.address.invalid": "أدخل عنوان خادم HTTP أو HTTPS صالحًا.",
|
||||
"server.connect.failed": "تعذر الاتصال. تحقق من عنوان الخادم وكلمة المرور، ثم أعد المحاولة.",
|
||||
"server.connect.pair.description": "نفّذ هذا الأمر على حاسوبك للحصول على تفاصيل الاتصال.",
|
||||
"server.connect.scan": "مسح رمز QR",
|
||||
"server.connect.scan.description": "وجّه الكاميرا نحو رمز QR الذي يعرضه opencode pair.",
|
||||
"server.connect.scan.invalid": "هذا ليس رمز إقران OpenCode. امسح الرمز الذي يعرضه opencode pair.",
|
||||
"server.connect.camera": "كاميرا الإقران",
|
||||
"server.connect.camera.starting": "جارٍ فتح الكاميرا…",
|
||||
"server.connect.mixedContent": "تعذر الاتصال بخادم HTTP هذا من صفحة HTTPS. استخدم عنوان خادم HTTPS بدلًا منه.",
|
||||
"server.connect.camera.insecure": "يتطلب مسح رمز QR فتح هذه الصفحة عبر HTTPS أو على localhost.",
|
||||
"server.connect.camera.unavailable": "لا تتوفر كاميرا لهذا المتصفح. أدخل تفاصيل الاتصال يدويًا.",
|
||||
"server.connect.camera.error": "تعذر فتح الكاميرا. اسمح بالوصول إليها أو أدخل تفاصيل الاتصال يدويًا.",
|
||||
"dialog.server.menu.remove": "إزالة",
|
||||
|
||||
@@ -799,7 +799,6 @@ export const dict = {
|
||||
"server.connect.button": "Qoşulun",
|
||||
"server.connect.address.invalid": "Etibarlı HTTP və ya HTTPS server ünvanını daxil edin.",
|
||||
"server.connect.failed": "Bağlana bilmədik. Server ünvanını və şifrəni yoxlayın, sonra yenidən cəhd edin.",
|
||||
"server.connect.pair.description": "Bağlantı məlumatlarınızı almaq üçün bu əmri kompüterinizdə işlədin.",
|
||||
"server.connect.scan": "QR kodu skan edin",
|
||||
"server.connect.scan.description": "Kameranızı opencode pair tərəfindən göstərilən QR koda yönəldin.",
|
||||
"server.connect.scan.invalid": "Bu, OpenCode qoşulma kodu deyil. opencode pair tərəfindən göstərilən kodu skan edin.",
|
||||
@@ -807,8 +806,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera açılır…",
|
||||
"server.connect.mixedContent":
|
||||
"Bu HTTPS səhifəsindən bu HTTP serverinə qoşulmaq mümkün olmadı. Bunun əvəzinə HTTPS server ünvanından istifadə edin.",
|
||||
"server.connect.camera.insecure":
|
||||
"QR skan edilməsi üçün bu səhifəni HTTPS üzərindən və ya localhost-da açmaq tələb olunur.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Bu brauzer üçün heç bir kamera mövcud deyil. Bağlantı məlumatlarınızı əl ilə daxil edin.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -797,7 +797,6 @@ export const dict = {
|
||||
"server.connect.button": "Свързване",
|
||||
"server.connect.address.invalid": "Въведете валиден HTTP или HTTPS адрес на сървър.",
|
||||
"server.connect.failed": "Свързването е неуспешно. Проверете адреса и паролата на сървъра и опитайте отново.",
|
||||
"server.connect.pair.description": "Изпълнете тази команда на компютъра си, за да получите данните за връзка.",
|
||||
"server.connect.scan": "Сканиране на QR код",
|
||||
"server.connect.scan.description": "Насочете камерата към QR кода, показан от opencode pair.",
|
||||
"server.connect.scan.invalid": "Това не е код за сдвояване с OpenCode. Сканирайте кода, показан от opencode pair.",
|
||||
@@ -805,8 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Отваряне на камерата…",
|
||||
"server.connect.mixedContent":
|
||||
"Не може да се осъществи връзка с този HTTP сървър от HTTPS страница. Използвайте HTTPS адрес на сървъра.",
|
||||
"server.connect.camera.insecure":
|
||||
"Сканирането на QR код изисква тази страница да е отворена чрез HTTPS или на localhost.",
|
||||
"server.connect.camera.unavailable": "Няма достъпна камера за този браузър. Въведете ръчно данните за връзка.",
|
||||
"server.connect.camera.error":
|
||||
"Камерата не можа да бъде отворена. Разрешете достъпа до нея или въведете ръчно данните за връзка.",
|
||||
|
||||
@@ -797,7 +797,6 @@ export const dict = {
|
||||
"server.connect.button": "সংযোগ করুন",
|
||||
"server.connect.address.invalid": "একটি বৈধ HTTP বা HTTPS সার্ভার ঠিকানা লিখুন৷",
|
||||
"server.connect.failed": "সংযোগ করা যায়নি। সার্ভার ঠিকানা এবং পাসওয়ার্ড পরীক্ষা করুন, তারপর আবার চেষ্টা করুন.",
|
||||
"server.connect.pair.description": "আপনার সংযোগের বিবরণ পেতে আপনার কম্পিউটারে এই কমান্ডটি চালান।",
|
||||
"server.connect.scan": "QR কোড স্ক্যান করুন",
|
||||
"server.connect.scan.description": "আপনার ক্যামেরাকে ওপেনকোড পেয়ার দ্বারা দেখানো QR কোডে নির্দেশ করুন৷",
|
||||
"server.connect.scan.invalid": "এটি একটি OpenCode পেয়ারিং কোড নয়৷ ওপেনকোড পেয়ার দ্বারা দেখানো কোডটি স্ক্যান করুন।",
|
||||
@@ -805,7 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "ক্যামেরা খোলা হচ্ছে...",
|
||||
"server.connect.mixedContent":
|
||||
"একটি HTTPS পৃষ্ঠা থেকে এই HTTP সার্ভারের সাথে সংযোগ করা যায়নি৷ পরিবর্তে একটি HTTPS সার্ভার ঠিকানা ব্যবহার করুন৷",
|
||||
"server.connect.camera.insecure": "QR স্ক্যান করার জন্য এই পৃষ্ঠাটি HTTPS বা লোকালহোস্টে খুলতে হবে।",
|
||||
"server.connect.camera.unavailable": "এই ব্রাউজারে কোন ক্যামেরা উপলব্ধ নেই৷ ম্যানুয়ালি আপনার সংযোগ বিশদ লিখুন.",
|
||||
"server.connect.camera.error":
|
||||
"ক্যামেরা খুলতে পারিনি। ক্যামেরা অ্যাক্সেসের অনুমতি দিন বা ম্যানুয়ালি আপনার সংযোগের বিবরণ লিখুন।",
|
||||
|
||||
@@ -22,7 +22,6 @@ export const dict = {
|
||||
"server.connect.button": "Conectar",
|
||||
"server.connect.address.invalid": "Insira um endereço de servidor HTTP ou HTTPS válido.",
|
||||
"server.connect.failed": "Não foi possível conectar. Verifique o endereço e a senha do servidor e tente novamente.",
|
||||
"server.connect.pair.description": "Execute este comando em seu computador para obter os detalhes de sua conexão.",
|
||||
"server.connect.scan": "Digitalize o código QR",
|
||||
"server.connect.scan.description": "Aponte sua câmera para o código QR mostrado por opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -31,7 +30,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Abrindo a câmera…",
|
||||
"server.connect.mixedContent":
|
||||
"Não foi possível conectar-se a este servidor HTTP a partir de uma página HTTPS. Use um endereço de servidor HTTPS.",
|
||||
"server.connect.camera.insecure": "A varredura QR requer a abertura desta página em HTTPS ou no host local.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Nenhuma câmera está disponível para este navegador. Insira os detalhes da sua conexão manualmente.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -862,7 +862,6 @@ export const dict = {
|
||||
"server.connect.button": "Poveži se",
|
||||
"server.connect.address.invalid": "Unesite ispravnu HTTP ili HTTPS adresu servera.",
|
||||
"server.connect.failed": "Povezivanje nije uspjelo. Provjerite adresu servera i lozinku, pa pokušajte ponovo.",
|
||||
"server.connect.pair.description": "Pokrenite ovu naredbu na računaru da biste dobili podatke za povezivanje.",
|
||||
"server.connect.scan": "Skeniraj QR kod",
|
||||
"server.connect.scan.description": "Usmjerite kameru prema QR kodu koji prikazuje opencode pair.",
|
||||
"server.connect.scan.invalid": "Ovo nije OpenCode kod za uparivanje. Skenirajte kod koji prikazuje opencode pair.",
|
||||
@@ -870,8 +869,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otvaranje kamere…",
|
||||
"server.connect.mixedContent":
|
||||
"Nije se moguće povezati s ovim HTTP serverom s HTTPS stranice. Umjesto toga koristite HTTPS adresu servera.",
|
||||
"server.connect.camera.insecure":
|
||||
"Za skeniranje QR koda ovu stranicu morate otvoriti putem HTTPS-a ili na localhostu.",
|
||||
"server.connect.camera.unavailable": "Ovom pregledniku nije dostupna kamera. Ručno unesite podatke za povezivanje.",
|
||||
"server.connect.camera.error":
|
||||
"Kameru nije bilo moguće otvoriti. Dozvolite pristup kameri ili ručno unesite podatke za povezivanje.",
|
||||
|
||||
@@ -796,8 +796,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Introduïu una adreça de servidor HTTP o HTTPS vàlida.",
|
||||
"server.connect.failed":
|
||||
"No s'ha pogut connectar. Comproveu l'adreça i la contrasenya del servidor i torneu-ho a provar.",
|
||||
"server.connect.pair.description":
|
||||
"Executeu aquesta ordre al vostre ordinador per obtenir els detalls de la vostra connexió.",
|
||||
"server.connect.scan": "Escaneja el codi QR",
|
||||
"server.connect.scan.description": "Apunteu la càmera al codi QR que mostra opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -806,7 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Obertura de la càmera...",
|
||||
"server.connect.mixedContent":
|
||||
"No s'ha pogut connectar a aquest servidor HTTP des d'una pàgina HTTPS. Utilitzeu una adreça de servidor HTTPS.",
|
||||
"server.connect.camera.insecure": "L'exploració de QR requereix obrir aquesta pàgina a HTTPS o a localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"No hi ha cap càmera disponible per a aquest navegador. Introduïu els detalls de connexió manualment.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -806,7 +806,6 @@ export const dict = {
|
||||
"server.connect.button": "Připojit",
|
||||
"server.connect.address.invalid": "Zadejte platnou adresu serveru HTTP nebo HTTPS.",
|
||||
"server.connect.failed": "Připojení se nezdařilo. Zkontrolujte adresu serveru a heslo a zkuste to znovu.",
|
||||
"server.connect.pair.description": "Spuštěním tohoto příkazu v počítači získáte údaje pro připojení.",
|
||||
"server.connect.scan": "Naskenovat QR kód",
|
||||
"server.connect.scan.description": "Namiřte kameru na QR kód zobrazený příkazem opencode pair.",
|
||||
"server.connect.scan.invalid": "Toto není párovací kód OpenCode. Naskenujte kód zobrazený příkazem opencode pair.",
|
||||
@@ -814,7 +813,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otevírání kamery…",
|
||||
"server.connect.mixedContent":
|
||||
"K tomuto serveru HTTP se nelze připojit ze stránky HTTPS. Použijte adresu serveru HTTPS.",
|
||||
"server.connect.camera.insecure": "Skenování QR kódu vyžaduje otevření této stránky přes HTTPS nebo na localhostu.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Pro tento prohlížeč není dostupná žádná kamera. Zadejte údaje pro připojení ručně.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -23,7 +23,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Indtast en gyldig HTTP- eller HTTPS-serveradresse.",
|
||||
"server.connect.failed":
|
||||
"Kunne ikke oprette forbindelse. Tjek serveradressen og adgangskoden, og prøv derefter igen.",
|
||||
"server.connect.pair.description": "Kør denne kommando på din computer for at få dine forbindelsesdetaljer.",
|
||||
"server.connect.scan": "Scan QR kode",
|
||||
"server.connect.scan.description": "Ret dit kamera mod QR-koden vist af opencode pair.",
|
||||
"server.connect.scan.invalid": "Dette er ikke en OpenCode-parringskode. Scan koden vist af opencode pair.",
|
||||
@@ -31,7 +30,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Åbner kamera...",
|
||||
"server.connect.mixedContent":
|
||||
"Kunne ikke oprette forbindelse til denne HTTP-server fra en HTTPS-side. Brug i stedet en HTTPS-serveradresse.",
|
||||
"server.connect.camera.insecure": "QR-scanning kræver, at denne side åbnes over HTTPS eller på localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Intet kamera er tilgængeligt for denne browser. Indtast dine forbindelsesoplysninger manuelt.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -27,8 +27,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Geben Sie eine gültige HTTP- oder HTTPS-Serveradresse ein.",
|
||||
"server.connect.failed":
|
||||
"Es konnte keine Verbindung hergestellt werden. Überprüfen Sie die Serveradresse und das Passwort und versuchen Sie es dann erneut.",
|
||||
"server.connect.pair.description":
|
||||
"Führen Sie diesen Befehl auf Ihrem Computer aus, um Ihre Verbindungsdetails abzurufen.",
|
||||
"server.connect.scan": "Scannen Sie den QR-Code",
|
||||
"server.connect.scan.description": "Richten Sie Ihre Kamera auf den von opencode pair angezeigten Code QR.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -37,8 +35,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera wird geöffnet…",
|
||||
"server.connect.mixedContent":
|
||||
"Es konnte keine Verbindung zu diesem HTTP-Server von einer HTTPS-Seite hergestellt werden. Verwenden Sie stattdessen eine HTTPS-Serveradresse.",
|
||||
"server.connect.camera.insecure":
|
||||
"Zum Scannen mit QR muss diese Seite über HTTPS oder auf localhost geöffnet werden.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Für diesen Browser ist keine Kamera verfügbar. Geben Sie Ihre Verbindungsdaten manuell ein.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -816,7 +816,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "ސައްހަ HTTP ނުވަތަ HTTPS ސަރވަރ އެޑްރެހެއް ލިޔެލާށެވެ.",
|
||||
"server.connect.failed":
|
||||
"ގުޅެވޭ ގޮތެއް ނުވިއެވެ. ސަރވަރ އެޑްރެހާއި ޕާސްވޯޑް ޗެކްކޮށްލުމަށްފަހު އަލުން މަސައްކަތް ކުރާށެވެ.",
|
||||
"server.connect.pair.description": "ކަނެކްޝަންގެ ތަފްސީލު ހޯދުމަށްޓަކައި ކޮމްޕިއުޓަރުގައި މި ކޮމާންޑް ހިންގާށެވެ.",
|
||||
"server.connect.scan": "QR ކޯޑް ސްކޭން ކުރާށެވެ",
|
||||
"server.connect.scan.description": "އޮޕަންކޯޑް ޖޯޑުން ދައްކާ QR ކޯޑަށް ކެމެރާ އިޝާރާތް ކުރާށެވެ.",
|
||||
"server.connect.scan.invalid": "މިއީ OpenCode ޖޯޑު ކޯޑެއް ނޫނެވެ. އޮޕަންކޯޑް ޕެއާ އިން ދައްކާ ކޯޑް ސްކޭން ކުރާށެވެ.",
|
||||
@@ -824,8 +823,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "ކެމެރާ ހުޅުވަނީ...",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS ޕޭޖަކުން މި HTTP ސަރވަރ އާއި ގުޅޭ ގޮތެއް ނުވިއެވެ. އޭގެ ބަދަލުގައި HTTPS ސަރވަރ އެޑްރެހެއް ބޭނުން ކުރާށެވެ.",
|
||||
"server.connect.camera.insecure":
|
||||
"QR ސްކޭން ކުރުމަށް މި ޕޭޖް ހުޅުވަން ޖެހެނީ HTTPS ގެ މައްޗަށް ނުވަތަ ލޯކަލްހޯސްޓް އިންނެވެ.",
|
||||
"server.connect.camera.unavailable":
|
||||
"މި ބްރައުޒާއަށް އެއްވެސް ކެމެރާއެއް ނުލިބެއެވެ. ކަނެކްޝަންގެ ތަފްސީލުތައް އަމިއްލައަށް ލިޔެލާށެވެ.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -797,8 +797,6 @@ export const dict = {
|
||||
"server.connect.button": "མཐུད།",
|
||||
"server.connect.address.invalid": "ནུས་ཅན་ HTTP ཡང་ན་ HTTPS སར་བར་ཁ་བྱང་ཅིག་བཙུགས།",
|
||||
"server.connect.failed": "མཐུད་མ་ཚུགས། སར་བར་ཁ་བྱང་དང་ཆོག་ཡིག་ཞིབ་དཔྱད་འབད་ཞིནམ་ལས་ ལོག་སྟེ་འབད་རྩོལ་བསྐྱེད།",
|
||||
"server.connect.pair.description":
|
||||
"ཁྱོད་རའི་མཐུད་ལམ་རྒྱས་བཤད་ཚུ་ཐོབ་ནིའི་དོན་ལུ་ ཁྱོད་རའི་གློག་རིག་གུ་བརྡ་བཀོད་འདི་གཡོག་བཀོལ།",
|
||||
"server.connect.scan": "QR གསང་གྲངས་པར་བཤུས་",
|
||||
"server.connect.scan.description": "ཁྱོད་རའི་པར་ཆས་འདི་ opencode pair གིས་སྟོན་མི་ QR གསང་ཡིག་ལུ་སྟོན་དགོ།",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -807,7 +805,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "པར་ཆས་ཁ་ཕྱེ་དོ...",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS ཤོག་ལེབ་ཅིག་ལས་ HTTP སར་བར་འདི་ལུ་མཐུད་མ་ཚུགས། དེ་གི་ཚབ་ལུ་ HTTPS སར་བར་ཁ་བྱང་ཅིག་ལག་ལེན་འཐབ།",
|
||||
"server.connect.camera.insecure": "QR པར་ལེན་འབད་ནི་ལུ་ ཤོག་ལེབ་འདི་ HTTPS ཡང་ན་ localhost གུ་ཁ་ཕྱེ་དགོཔ་ཨིན།",
|
||||
"server.connect.camera.unavailable": "བརྡ་འཚོལ་འདི་ལུ་པར་ཆས་མེད། ཁྱོད་རའི་མཐུད་ལམ་རྒྱས་བཤད་ཚུ་ལག་ཐོག་ལས་བཙུགས།",
|
||||
"server.connect.camera.error":
|
||||
"པར་ཆས་ཁ་ཕྱེ་མ་ཚུགས། པར་ཆས་འཛུལ་སྤྱོད་འབད་བཅུགཔ་ ཡང་ན་ ཁྱོད་རའི་མཐུད་ལམ་རྒྱས་བཤད་ཚུ་ལག་ཐོག་ལས་བཙུགས།",
|
||||
|
||||
@@ -796,8 +796,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Εισαγάγετε μια έγκυρη διεύθυνση διακομιστή HTTP ή HTTPS.",
|
||||
"server.connect.failed":
|
||||
"Δεν ήταν δυνατή η σύνδεση. Ελέγξτε τη διεύθυνση του διακομιστή και τον κωδικό πρόσβασης και, στη συνέχεια, δοκιμάστε ξανά.",
|
||||
"server.connect.pair.description":
|
||||
"Εκτελέστε αυτήν την εντολή στον υπολογιστή σας για να λάβετε τα στοιχεία της σύνδεσής σας.",
|
||||
"server.connect.scan": "Σάρωση κωδικού QR",
|
||||
"server.connect.scan.description": "Στρέψτε την κάμερά σας στον κωδικό QR που εμφανίζεται από το opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -806,7 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Άνοιγμα κάμερας…",
|
||||
"server.connect.mixedContent":
|
||||
"Δεν ήταν δυνατή η σύνδεση σε αυτόν τον διακομιστή HTTP από μια σελίδα HTTPS. Χρησιμοποιήστε μια διεύθυνση διακομιστή HTTPS.",
|
||||
"server.connect.camera.insecure": "Η σάρωση QR απαιτεί άνοιγμα αυτής της σελίδας μέσω HTTPS ή localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Δεν υπάρχει κάμερα διαθέσιμη σε αυτό το πρόγραμμα περιήγησης. Εισαγάγετε τα στοιχεία της σύνδεσής σας με μη αυτόματο τρόπο.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -340,24 +340,41 @@ export const dict = {
|
||||
|
||||
"server.connect.title": "Connect to a server",
|
||||
"server.connect.description": "Enter your server address and password to get started.",
|
||||
"server.connect.description.pairing": "Paste the pairing link from the server, or scan its QR code.",
|
||||
"server.connect.link": "Pairing link",
|
||||
"server.connect.link.placeholder": "http://192.168.1.2:4096/auth/connect/…",
|
||||
"server.connect.link.invalid":
|
||||
"Paste the full pairing link from opencode pair, or from Settings → Pairing on the server's computer.",
|
||||
"server.connect.password.prompt": "No pairing link?",
|
||||
"server.connect.password.use": "Use password",
|
||||
"server.connect.password.hint": "To see it, run opencode service get password on the server's computer.",
|
||||
"server.connect.link.prompt": "Have a pairing link?",
|
||||
"server.connect.link.use": "Use pairing link",
|
||||
"server.connect.button": "Connect",
|
||||
"server.connect.address.invalid": "Enter a valid HTTP or HTTPS server address.",
|
||||
"server.connect.failed": "Could not connect. Check the server address and password, then try again.",
|
||||
"server.connect.pair.description": "Run this command on your computer to get your connection details.",
|
||||
"server.connect.pair.link": "Run this command on the server's computer to get a pairing link and QR code.",
|
||||
"server.connect.scan": "Scan QR code",
|
||||
"server.connect.scan.description": "Point your camera at the QR code shown by opencode pair.",
|
||||
"server.connect.scan.invalid": "This is not an OpenCode pairing code. Scan the code shown by opencode pair.",
|
||||
"server.connect.scan.failed": "This browser could not read the QR code. Enter your connection details manually.",
|
||||
"server.connect.link.expired": "This pairing link expired or was already used. Run opencode pair to get a new one.",
|
||||
"server.connect.link.unreachable":
|
||||
"Could not reach {{url}}. Check that this device can reach the server, then try again.",
|
||||
"server.connect.link.legacy":
|
||||
"This pairing code is from an older version of OpenCode. Update OpenCode on the server, then run opencode pair again.",
|
||||
"server.connect.camera": "Pairing camera",
|
||||
"server.connect.camera.starting": "Opening camera…",
|
||||
"server.connect.mixedContent":
|
||||
"Could not connect to this HTTP server from an HTTPS page. Use an HTTPS server address instead.",
|
||||
"server.connect.camera.insecure": "QR scanning requires opening this page over HTTPS or on localhost.",
|
||||
"server.connect.camera.native": "This page cannot use the camera over HTTP. Paste the pairing link above.",
|
||||
"server.connect.camera.unavailable":
|
||||
"No camera is available to this browser. Enter your connection details manually.",
|
||||
"server.connect.camera.error":
|
||||
"Could not open the camera. Allow camera access or enter your connection details manually.",
|
||||
"dialog.server.edit.title": "Edit server",
|
||||
"dialog.server.signedOut":
|
||||
"Your sign-in for this server is no longer valid. Paste a new pairing link, or enter the server password.",
|
||||
|
||||
"dialog.server.menu.edit": "Edit",
|
||||
"dialog.server.menu.default": "Set as default",
|
||||
@@ -662,6 +679,21 @@ export const dict = {
|
||||
"settings.about.description": "OpenCode, the open source coding agent",
|
||||
"settings.about.trademark": "OpenCode is a registered trademark of Anomaly Innovations, Inc.",
|
||||
"settings.about.typeset": "Typeset in Inter and IBM Plex Mono",
|
||||
"settings.about.notices.title": "Third-party notices",
|
||||
"settings.about.notices.description": "OpenCode includes the following open source software.",
|
||||
"settings.about.notices.license": "License text",
|
||||
"settings.about.notices.fonts.title": "Fonts in the Office previews",
|
||||
"settings.about.notices.fonts.description":
|
||||
"Each family is distributed under the SIL Open Font License 1.1, with the copyright lines below.",
|
||||
"settings.about.notices.eigenpal": "Portions of BetterOffice derive from it.",
|
||||
"settings.about.notices.loadFailed": "Could not load the third-party notices",
|
||||
"settings.about.notices.crates.title": "Rust crates in the Office engines",
|
||||
"settings.about.notices.crates.description":
|
||||
"BetterOffice compiles these crates into its Word, Excel and PowerPoint engines. Where a crate offers a choice of licenses, the highlighted license applies.",
|
||||
"settings.about.notices.crates.licenses": "License texts",
|
||||
"settings.about.notices.crates.count.one": "{{count}} crate",
|
||||
"settings.about.notices.crates.count.other": "{{count}} crates",
|
||||
"settings.about.notices.crates.appliesTo": "Applies to {{names}}",
|
||||
"settings.about.tagline": "AI can’t build great software, without you",
|
||||
"settings.about.copyright": "© Anomaly Innovations, Inc.",
|
||||
"settings.preferences.description": "Customize preferences and theme and default behavior",
|
||||
|
||||
@@ -23,7 +23,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Ingrese una dirección de servidor HTTP o HTTPS válida.",
|
||||
"server.connect.failed":
|
||||
"No se pudo conectar. Verifique la dirección y la contraseña del servidor y vuelva a intentarlo.",
|
||||
"server.connect.pair.description": "Ejecute este comando en su computadora para obtener los detalles de su conexión.",
|
||||
"server.connect.scan": "Escanea el código QR",
|
||||
"server.connect.scan.description": "Apunte su cámara al código QR mostrado por opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -32,7 +31,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Abriendo cámara...",
|
||||
"server.connect.mixedContent":
|
||||
"No se pudo conectar a este servidor HTTP desde una página HTTPS. Utilice una dirección de servidor HTTPS en su lugar.",
|
||||
"server.connect.camera.insecure": "El escaneo de QR requiere abrir esta página en HTTPS o en localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"No hay ninguna cámara disponible para este navegador. Ingrese los detalles de su conexión manualmente.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -775,7 +775,6 @@ export const dict = {
|
||||
"server.connect.button": "Ühendage",
|
||||
"server.connect.address.invalid": "Sisestage kehtiv serveri aadress HTTP või HTTPS.",
|
||||
"server.connect.failed": "Ühendust ei õnnestunud luua. Kontrollige serveri aadressi ja parooli ning proovige uuesti.",
|
||||
"server.connect.pair.description": "Ühenduse üksikasjade hankimiseks käivitage see käsk oma arvutis.",
|
||||
"server.connect.scan": "Skaneeri QR-kood",
|
||||
"server.connect.scan.description": "Suunake oma kaamera koodile QR, mida näitab opencode pair.",
|
||||
"server.connect.scan.invalid": "See ei ole OpenCode sidumiskood. Skannige koodi, mida näitab opencode pair.",
|
||||
@@ -783,7 +782,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kaamera avamine…",
|
||||
"server.connect.mixedContent":
|
||||
"Selle HTTP serveriga ei saanud HTTPS lehelt ühendust luua. Kasutage selle asemel HTTPS serveri aadressi.",
|
||||
"server.connect.camera.insecure": "QR skannimine nõuab selle lehe avamist HTTPS või localhost kaudu.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Selle brauseri jaoks pole kaamerat saadaval. Sisestage ühenduse üksikasjad käsitsi.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -795,7 +795,6 @@ export const dict = {
|
||||
"server.connect.button": "اتصال",
|
||||
"server.connect.address.invalid": "یک نشانی معتبر سرور HTTP یا HTTPS وارد کنید.",
|
||||
"server.connect.failed": "اتصال ممکن نشد. نشانی سرور و گذرواژه را بررسی کنید و دوباره تلاش کنید.",
|
||||
"server.connect.pair.description": "برای دریافت جزئیات اتصال، این فرمان را روی رایانهٔ خود اجرا کنید.",
|
||||
"server.connect.scan": "پویش کد QR",
|
||||
"server.connect.scan.description": "دوربین را بهسمت کد QR نمایشدادهشده توسط opencode pair بگیرید.",
|
||||
"server.connect.scan.invalid": "این کد جفتسازی OpenCode نیست. کدی را پویش کنید که opencode pair نشان میدهد.",
|
||||
@@ -803,7 +802,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "در حال باز کردن دوربین…",
|
||||
"server.connect.mixedContent":
|
||||
"اتصال به این سرور HTTP از صفحهٔ HTTPS ممکن نشد. بهجای آن از نشانی سرور HTTPS استفاده کنید.",
|
||||
"server.connect.camera.insecure": "پویش QR نیازمند باز کردن این صفحه از طریق HTTPS یا روی localhost است.",
|
||||
"server.connect.camera.unavailable": "هیچ دوربینی برای این مرورگر در دسترس نیست. جزئیات اتصال را دستی وارد کنید.",
|
||||
"server.connect.camera.error":
|
||||
"باز کردن دوربین ممکن نشد. دسترسی دوربین را مجاز کنید یا جزئیات اتصال را دستی وارد کنید.",
|
||||
|
||||
@@ -23,7 +23,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Anna kelvollinen HTTP- tai HTTPS-palvelinosoite.",
|
||||
"server.connect.failed":
|
||||
"Yhteyttä ei voitu muodostaa. Tarkista palvelimen osoite ja salasana ja yritä sitten uudelleen.",
|
||||
"server.connect.pair.description": "Suorita tämä komento tietokoneellasi saadaksesi yhteystiedot.",
|
||||
"server.connect.scan": "Skannaa QR-koodi",
|
||||
"server.connect.scan.description": "Suuntaa kamerasi QR-koodiin, joka näkyy opencode pair:ssä.",
|
||||
"server.connect.scan.invalid": "Tämä ei ole OpenCode-pariliitoskoodi. Skannaa opencode pair:n näyttämä koodi.",
|
||||
@@ -31,7 +30,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Avataan kameraa…",
|
||||
"server.connect.mixedContent":
|
||||
"Yhteyttä tähän HTTP-palvelimeen ei voitu muodostaa HTTPS-sivulta. Käytä sen sijaan HTTPS-palvelinosoitetta.",
|
||||
"server.connect.camera.insecure": "QR-skannaus vaatii tämän sivun avaamisen HTTPS:n kautta tai localhostissa.",
|
||||
"server.connect.camera.unavailable": "Kamera ei ole saatavilla tälle selaimelle. Syötä yhteystietosi manuaalisesti.",
|
||||
"server.connect.camera.error": "Kameraa ei voitu avata. Salli kameran käyttö tai anna yhteystietosi manuaalisesti.",
|
||||
"dialog.server.menu.remove": "Poista",
|
||||
|
||||
@@ -779,7 +779,6 @@ export const dict = {
|
||||
"server.connect.button": "Samband",
|
||||
"server.connect.address.invalid": "Skriva eina gylduga HTTP ella HTTPS ambætaraadressu.",
|
||||
"server.connect.failed": "Fekk ikki samband. Kanna ambætaraadressuna og loyniorðið, og royn síðani aftur.",
|
||||
"server.connect.pair.description": "Koyr hesa skipanina á tínari teldu fyri at fáa tínar sambandsupplýsingar.",
|
||||
"server.connect.scan": "Skanna QR-kotu",
|
||||
"server.connect.scan.description": "Peika myndatólið á QR-kotuna, sum opencode pair vísir.",
|
||||
"server.connect.scan.invalid": "Hetta er ikki ein OpenCode paringarkoda. Skanna kotuna, sum opencode pair vísir.",
|
||||
@@ -787,7 +786,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Opna myndatól...",
|
||||
"server.connect.mixedContent":
|
||||
"Kundi ikki seta samband við hendan HTTP ambætaran frá eini HTTPS síðu. Brúka eina HTTPS ambætaraadressu ístaðin.",
|
||||
"server.connect.camera.insecure": "QR-skanning krevur, at tú letur hesa síðuna upp yvir HTTPS ella á localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Einki myndatól er tøkt til henda kaga. Skriva tínar sambandsupplýsingar manuelt.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -23,8 +23,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Entrez une adresse de serveur HTTP ou HTTPS valide.",
|
||||
"server.connect.failed":
|
||||
"Impossible de se connecter. Vérifiez l'adresse et le mot de passe du serveur, puis réessayez.",
|
||||
"server.connect.pair.description":
|
||||
"Exécutez cette commande sur votre ordinateur pour obtenir les détails de votre connexion.",
|
||||
"server.connect.scan": "Scanner le code QR",
|
||||
"server.connect.scan.description": "Pointez votre appareil photo vers le code QR affiché par opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -33,7 +31,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Ouverture de la caméra…",
|
||||
"server.connect.mixedContent":
|
||||
"Impossible de se connecter à ce serveur HTTP à partir d'une page HTTPS. Utilisez plutôt une adresse de serveur HTTPS.",
|
||||
"server.connect.camera.insecure": "L'analyse QR nécessite l'ouverture de cette page sur HTTPS ou sur localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Aucune caméra n'est disponible pour ce navigateur. Saisissez manuellement vos informations de connexion.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -793,14 +793,12 @@ export const dict = {
|
||||
"server.connect.button": "התחברות",
|
||||
"server.connect.address.invalid": "יש להזין כתובת שרת HTTP או HTTPS תקינה.",
|
||||
"server.connect.failed": "לא ניתן להתחבר. יש לבדוק את כתובת השרת והסיסמה ולנסות שוב.",
|
||||
"server.connect.pair.description": "יש להריץ פקודה זו במחשב כדי לקבל את פרטי החיבור.",
|
||||
"server.connect.scan": "סריקת קוד QR",
|
||||
"server.connect.scan.description": "יש לכוון את המצלמה לקוד QR שמוצג על ידי opencode pair.",
|
||||
"server.connect.scan.invalid": "זה אינו קוד צימוד של OpenCode. יש לסרוק את הקוד שמוצג על ידי opencode pair.",
|
||||
"server.connect.camera": "מצלמת צימוד",
|
||||
"server.connect.camera.starting": "פותח את המצלמה…",
|
||||
"server.connect.mixedContent": "לא ניתן להתחבר לשרת HTTP זה מדף HTTPS. יש להשתמש בכתובת שרת HTTPS במקום זאת.",
|
||||
"server.connect.camera.insecure": "סריקת QR דורשת פתיחת דף זה דרך HTTPS או ב-localhost.",
|
||||
"server.connect.camera.unavailable": "אין מצלמה זמינה לדפדפן זה. יש להזין את פרטי החיבור ידנית.",
|
||||
"server.connect.camera.error": "לא ניתן לפתוח את המצלמה. יש לאפשר גישה למצלמה או להזין את פרטי החיבור ידנית.",
|
||||
"dialog.server.menu.remove": "הסרה",
|
||||
|
||||
@@ -797,7 +797,6 @@ export const dict = {
|
||||
"server.connect.button": "जोड़ना",
|
||||
"server.connect.address.invalid": "एक वैध HTTP या HTTPS सर्वर पता दर्ज करें।",
|
||||
"server.connect.failed": "कनेक्ट नहीं हो सका। सर्वर पता और पासवर्ड जांचें, फिर पुनः प्रयास करें।",
|
||||
"server.connect.pair.description": "अपना कनेक्शन विवरण प्राप्त करने के लिए इस कमांड को अपने कंप्यूटर पर चलाएँ।",
|
||||
"server.connect.scan": "QR कोड स्कैन करें",
|
||||
"server.connect.scan.description": "अपने कैमरे को ओपनकोड जोड़ी द्वारा दिखाए गए QR कोड पर इंगित करें।",
|
||||
"server.connect.scan.invalid": "यह कोई OpenCode युग्मन कोड नहीं है. ओपनकोड जोड़ी द्वारा दिखाए गए कोड को स्कैन करें।",
|
||||
@@ -805,7 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "कैमरा खुल रहा है...",
|
||||
"server.connect.mixedContent":
|
||||
"किसी HTTPS पेज से इस HTTP सर्वर से कनेक्ट नहीं किया जा सका। इसके बजाय HTTPS सर्वर पते का उपयोग करें।",
|
||||
"server.connect.camera.insecure": "QR स्कैनिंग के लिए इस पृष्ठ को HTTPS या लोकलहोस्ट पर खोलने की आवश्यकता है।",
|
||||
"server.connect.camera.unavailable":
|
||||
"इस ब्राउज़र पर कोई कैमरा उपलब्ध नहीं है. अपना कनेक्शन विवरण मैन्युअल रूप से दर्ज करें।",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -805,7 +805,6 @@ export const dict = {
|
||||
"server.connect.button": "Poveži se",
|
||||
"server.connect.address.invalid": "Unesite ispravnu HTTP ili HTTPS adresu poslužitelja.",
|
||||
"server.connect.failed": "Povezivanje nije uspjelo. Provjerite adresu poslužitelja i lozinku, pa pokušajte ponovno.",
|
||||
"server.connect.pair.description": "Pokrenite ovu naredbu na računalu da biste dobili podatke za povezivanje.",
|
||||
"server.connect.scan": "Skeniraj QR kod",
|
||||
"server.connect.scan.description": "Usmjerite kameru prema QR kodu koji prikazuje opencode pair.",
|
||||
"server.connect.scan.invalid": "Ovo nije OpenCode kod za uparivanje. Skenirajte kod koji prikazuje opencode pair.",
|
||||
@@ -813,8 +812,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otvaranje kamere…",
|
||||
"server.connect.mixedContent":
|
||||
"Nije se moguće povezati s ovim HTTP poslužiteljem s HTTPS stranice. Umjesto toga koristite HTTPS adresu poslužitelja.",
|
||||
"server.connect.camera.insecure":
|
||||
"Za skeniranje QR koda ovu stranicu morate otvoriti putem HTTPS-a ili na localračunalu domaćinu.",
|
||||
"server.connect.camera.unavailable": "Ovom pregledniku nije dostupna kamera. Ručno unesite podatke za povezivanje.",
|
||||
"server.connect.camera.error":
|
||||
"Kameru nije bilo moguće otvoriti. Dozvolite pristup kameri ili ručno unesite podatke za povezivanje.",
|
||||
|
||||
@@ -794,8 +794,6 @@ export const dict = {
|
||||
"server.connect.button": "Csatlakozás",
|
||||
"server.connect.address.invalid": "Adjon meg egy érvényes HTTP vagy HTTPS szervercímet.",
|
||||
"server.connect.failed": "Nem sikerült csatlakozni. Ellenőrizze a szerver címét és jelszavát, majd próbálja újra.",
|
||||
"server.connect.pair.description":
|
||||
"Futtassa ezt a parancsot a számítógépén a kapcsolat részleteinek megtekintéséhez.",
|
||||
"server.connect.scan": "QR-kód beolvasása",
|
||||
"server.connect.scan.description": "Irányítsa kameráját a opencode pair által mutatott QR kódra.",
|
||||
"server.connect.scan.invalid": "Ez nem egy OpenCode párosítási kód. Olvassa be a opencode pair által mutatott kódot.",
|
||||
@@ -803,7 +801,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera nyitása…",
|
||||
"server.connect.mixedContent":
|
||||
"Nem sikerült csatlakozni ehhez a HTTP szerverhez egy HTTPS oldalról. Használjon helyette HTTPS szervercímet.",
|
||||
"server.connect.camera.insecure": "A QR beolvasásához ezt az oldalt a HTTPS vagy a localhost oldalon kell megnyitni.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Ehhez a böngészőhöz nem érhető el kamera. Adja meg manuálisan a kapcsolat adatait.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -795,14 +795,12 @@ export const dict = {
|
||||
"server.connect.button": "Կապվել",
|
||||
"server.connect.address.invalid": "Մուտքագրեք վավեր HTTP կամ HTTPS սերվերի հասցե։",
|
||||
"server.connect.failed": "Չկապվեց. Ստուգեք սերվերի հասցեն և գաղտնաբառը, ապա փորձեք նորից։",
|
||||
"server.connect.pair.description": "Գործարկեք այս հրամանը ձեր համակարգչում՝ ստանալու ձեր կապի մանրամասները։",
|
||||
"server.connect.scan": "Սկանավորեք QR կոդը",
|
||||
"server.connect.scan.description": "Տեսախցիկն ուղղեք opencode pair-ի ցուցադրած QR կոդին։",
|
||||
"server.connect.scan.invalid": "Սա OpenCode-ի զուգակցման կոդ չէ։ Սկանավորեք opencode pair-ի ցուցադրած կոդը։",
|
||||
"server.connect.camera": "Զույգագրման վահանակը",
|
||||
"server.connect.camera.starting": "Բացվում է վահանակը…",
|
||||
"server.connect.mixedContent": "Այս HTTP սերվերին հնարավոր չէ միանալ HTTPS էջից։ Օգտագործեք HTTPS սերվերի հասցե։",
|
||||
"server.connect.camera.insecure": "QR սկանումը պահանջում է այս էջը բացել HTTPS-ով կամ տեղական հոստում։",
|
||||
"server.connect.camera.unavailable": "Այս զննարկչին վահանակ հասանելի չէ։ Մուտքագրեք ձեր կապի մանրամասները ձեռքով։",
|
||||
"server.connect.camera.error":
|
||||
"Չկարողացա բացել տեսախցիկը։ Թույլատրել տեսախցիկի մուտքը կամ ձեռքով ներմուծել ձեր կապի տվյալները։",
|
||||
|
||||
@@ -862,7 +862,6 @@ export const dict = {
|
||||
"server.connect.button": "Sambungkan",
|
||||
"server.connect.address.invalid": "Masukkan alamat server HTTP atau HTTPS yang valid.",
|
||||
"server.connect.failed": "Tidak dapat terhubung. Periksa alamat server dan kata sandi, lalu coba lagi.",
|
||||
"server.connect.pair.description": "Jalankan perintah ini di komputer Anda untuk mendapatkan detail koneksi Anda.",
|
||||
"server.connect.scan": "Pindai kode QR",
|
||||
"server.connect.scan.description": "Arahkan kamera Anda ke kode QR yang ditampilkan oleh opencode pair.",
|
||||
"server.connect.scan.invalid": "Ini bukan kode pemasangan OpenCode. Pindai kode yang ditampilkan oleh opencode pair.",
|
||||
@@ -870,7 +869,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Membuka kamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Tidak dapat terhubung ke server HTTP ini dari halaman HTTPS. Gunakan alamat server HTTPS sebagai gantinya.",
|
||||
"server.connect.camera.insecure": "Pemindaian QR memerlukan halaman ini dibuka melalui HTTPS atau di localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Tidak ada kamera yang tersedia untuk peramban ini. Masukkan detail koneksi Anda secara manual.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -782,7 +782,6 @@ export const dict = {
|
||||
"server.connect.button": "Tengdu",
|
||||
"server.connect.address.invalid": "Sláðu inn gilt HTTP eða HTTPS netfang netþjóns.",
|
||||
"server.connect.failed": "Ekki tókst að tengjast. Athugaðu netfang netþjóns og lykilorð, reyndu svo aftur.",
|
||||
"server.connect.pair.description": "Keyrðu þessa skipun á tölvunni þinni til að fá upplýsingar um tenginguna þína.",
|
||||
"server.connect.scan": "Skanna QR kóða",
|
||||
"server.connect.scan.description": "Beindu myndavélinni þinni að QR kóðanum sem opencode pair sýnir.",
|
||||
"server.connect.scan.invalid": "Þetta er ekki OpenCode pörunarkóði. Skannaðu kóðann sem opencode pair sýnir.",
|
||||
@@ -790,7 +789,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Opnar myndavél...",
|
||||
"server.connect.mixedContent":
|
||||
"Gat ekki tengst þessum HTTP netþjóni frá HTTPS síðu. Notaðu HTTPS netþjónsfang í staðinn.",
|
||||
"server.connect.camera.insecure": "QR skönnun krefst þess að opna þessa síðu yfir HTTPS eða á localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Engin myndavél er í boði fyrir þennan vafra. Sláðu inn tengiupplýsingar þínar handvirkt.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -22,8 +22,6 @@ export const dict = {
|
||||
"server.connect.button": "Connettiti",
|
||||
"server.connect.address.invalid": "Inserisci un indirizzo server HTTP o HTTPS valido.",
|
||||
"server.connect.failed": "Impossibile connettersi. Controlla l'indirizzo del server e la password, quindi riprova.",
|
||||
"server.connect.pair.description":
|
||||
"Esegui questo comando sul tuo computer per ottenere i dettagli della connessione.",
|
||||
"server.connect.scan": "Scansiona il codice QR",
|
||||
"server.connect.scan.description": "Punta la fotocamera sul codice QR mostrato da opencode pair.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -32,7 +30,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Apertura della fotocamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Impossibile connettersi a questo server HTTP da una pagina HTTPS. Utilizza invece un indirizzo server HTTPS.",
|
||||
"server.connect.camera.insecure": "La scansione QR richiede l'apertura di questa pagina su HTTPS o su localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Nessuna fotocamera è disponibile per questo browser. Inserisci manualmente i dettagli della tua connessione.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -808,7 +808,6 @@ export const dict = {
|
||||
"server.connect.button": "接続",
|
||||
"server.connect.address.invalid": "有効なHTTPまたはHTTPSサーバーアドレスを入力してください。",
|
||||
"server.connect.failed": "接続できませんでした。サーバーアドレスとパスワードを確認して、もう一度お試しください。",
|
||||
"server.connect.pair.description": "このコマンドをコンピューターで実行して接続情報を取得してください。",
|
||||
"server.connect.scan": "QRコードをスキャン",
|
||||
"server.connect.scan.description": "opencode pair に表示されたQRコードにカメラを向けてください。",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -817,7 +816,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "カメラを開いています…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPSページからこのHTTPサーバーに接続できません。代わりにHTTPSサーバーのアドレスを使用してください。",
|
||||
"server.connect.camera.insecure": "QRスキャンには、このページをHTTPSで開くか、ローカルホストで開く必要があります。",
|
||||
"server.connect.camera.unavailable": "このブラウザで使用可能なカメラがありません。接続情報を手動で入力してください。",
|
||||
"server.connect.camera.error":
|
||||
"カメラを開くことができません。カメラへのアクセスを許可するか、接続情報を手動で入力してください。",
|
||||
|
||||
@@ -791,7 +791,6 @@ export const dict = {
|
||||
"server.connect.button": "დაკავშირება",
|
||||
"server.connect.address.invalid": "შეიყვანეთ ვალიდური HTTP ან HTTPS სერვერის მისამართი.",
|
||||
"server.connect.failed": "ვერ მოხერხდა დაკავშირება. შეამოწმეთ სერვერის მისამართი და პაროლი, შემდეგ კვლავ სცადეთ.",
|
||||
"server.connect.pair.description": "ჩაატარეთ ეს ბრძანება თქვენს კომპიუტერზე, რათა მიიღოთ თქვენი კავშირის დეტალები.",
|
||||
"server.connect.scan": "სკანირება QR კოდით",
|
||||
"server.connect.scan.description": "მიმართეთ კამერა opencode pair-ის მიერ ნაჩვენებ QR კოდზე.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -800,7 +799,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "კამერის გახსნა…",
|
||||
"server.connect.mixedContent":
|
||||
"ვერ მოხერხდა დაკავშირება ამ HTTP სერვერთან HTTPS გვერდიდან. გამოიყენეთ HTTPS სერვერის მისამართი.",
|
||||
"server.connect.camera.insecure": "QR სკანირება საჭიროებს ამ გვერდის გახსნა HTTPS-ზე ან localhost-ზე.",
|
||||
"server.connect.camera.unavailable":
|
||||
"ამ ბრაუზერისთვის კამერა არ არის ხელმისაწვდომი. შეიყვანეთ თქვენი კავშირის დეტალები ხელით.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -791,7 +791,6 @@ export const dict = {
|
||||
"server.connect.button": "ភ្ជាប់",
|
||||
"server.connect.address.invalid": "បញ្ចូលអាសយដ្ឋានម៉ាស៊ីនមេ HTTP ឬ HTTPS ត្រឹមត្រូវ។",
|
||||
"server.connect.failed": "មិនអាចភ្ជាប់បានទេ។ សូមពិនិត្យអាសយដ្ឋានម៉ាស៊ីនមេ និងពាក្យសម្ងាត់ រួចព្យាយាមម្តងទៀត។",
|
||||
"server.connect.pair.description": "បើកកម្មង់នេះនៅលើកុំព្យូទ័ររបស់អ្នកដើម្បីទទួលព័ត៌មានការភ្ជាប់របស់អ្នក។",
|
||||
"server.connect.scan": "ស្កេនកូដ QR",
|
||||
"server.connect.scan.description": "បង្ខំកាមេរ៉ារបស់អ្នកទៅកូដ QR ដែលបង្ហាញដោយ opencode pair។",
|
||||
"server.connect.scan.invalid": "នេះមិនមែនជាកូដផ្គុំ OpenCode ទេ។ ស្កេនកូដដែលបង្ហាញដោយ opencode pair។",
|
||||
@@ -799,7 +798,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "កំពុងបើកកាមេរ៉ា…",
|
||||
"server.connect.mixedContent":
|
||||
"មិនអាចភ្ជាប់ទៅម៉ាស៊ីនបម្រើ HTTP នេះពីទំព័រ HTTPS។ សូមប្រើអាសយដ្ឋានម៉ាស៊ីនបម្រើ HTTPS ជំនួស។",
|
||||
"server.connect.camera.insecure": "ការស្កេន QR ត្រូវការបើកទំព័រនេះតាម HTTPS ឬនៅលើ localhost។",
|
||||
"server.connect.camera.unavailable":
|
||||
"មិនមានកាមេរ៉ាដើម្បីប្រើប្រាស់ជាមួយកម្មវិធីនេះទេ។ សូមបញ្ចូលព័ត៌មានការភ្ជាប់ដោយដៃ។",
|
||||
"server.connect.camera.error": "មិនអាចបើកកាមេរ៉ាបាន។ អនុញ្ញាតឱ្យចូលដំណើរការកាមេរ៉ា ឬបញ្ចូលព័ត៌មានការភ្ជាប់ដោយដៃ។",
|
||||
|
||||
@@ -803,7 +803,6 @@ export const dict = {
|
||||
"server.connect.button": "연결",
|
||||
"server.connect.address.invalid": "유효한 HTTP 또는 HTTPS 서버 주소를 입력하세요.",
|
||||
"server.connect.failed": "연결할 수 없습니다. 서버 주소와 비밀번호를 확인한 후 다시 시도하세요.",
|
||||
"server.connect.pair.description": "컴퓨터에서 이 명령어를 실행하여 연결 세부 정보를 확인하세요.",
|
||||
"server.connect.scan": "QR 코드 스캔",
|
||||
"server.connect.scan.description": "opencode pair에서 표시된 QR 코드를 카메라로 가리키세요.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -812,7 +811,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "카메라 열기…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS 페이지에서 이 HTTP 서버에 연결할 수 없습니다. 대신 HTTPS 서버 주소를 사용하세요.",
|
||||
"server.connect.camera.insecure": "QR 스캔을 위해서는 HTTPS를 통해 이 페이지를 열거나 로컬호스트에서 열어야 합니다.",
|
||||
"server.connect.camera.unavailable":
|
||||
"이 브라우저에서 사용할 수 있는 카메라가 없습니다. 연결 세부 정보를 수동으로 입력하세요.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -790,14 +790,12 @@ export const dict = {
|
||||
"server.connect.button": "ເຊື່ອມຕໍ່",
|
||||
"server.connect.address.invalid": "ໃສ່ທີ່ຢູ່ເຊີບເວີ HTTP ຫຼື HTTPS ທີ່ຖືກຕ້ອງ.",
|
||||
"server.connect.failed": "ບໍ່ສາມາດເຊື່ອມຕໍ່ໄດ້.ກວດເບິ່ງທີ່ຢູ່ເຊີບເວີແລະລະຫັດຜ່ານ, ແລ້ວລອງອີກຄັ້ງ.",
|
||||
"server.connect.pair.description": "ເຮັດຄໍາສັ່ງນີ້ໃນຄອມພິວເຕີຂອງທ່ານເພື່ອຮັບລາຍລະອຽດການເຊື່ອມຕໍ່.",
|
||||
"server.connect.scan": "ສະແກນ QR code",
|
||||
"server.connect.scan.description": "ຊີ້ກ້ອງຂອງທ່ານໄປທີ່ QR code ທີ່ສະແດງໂດຍຄູ່ opencode.",
|
||||
"server.connect.scan.invalid": "ນີ້ບໍ່ແມ່ນລະຫັດການຈັດຄູ່ OpenCode.ສະແກນລະຫັດທີ່ສະແດງໂດຍຄູ່ OpenCode.",
|
||||
"server.connect.camera": "ການຈັບຄູ່ກ້ອງ",
|
||||
"server.connect.camera.starting": "ກໍາລັງເປີດກ້ອງ...",
|
||||
"server.connect.mixedContent": "ບໍ່ສາມາດເຊື່ອມຕໍ່ກັບເຊີບເວີ HTTP ນີ້ຈາກຫນ້າ HTTPS ໄດ້.ໃຊ້ທີ່ຢູ່ເຊີບເວີ HTTPS ແທນ.",
|
||||
"server.connect.camera.insecure": "ການສະແກນ QR ຈໍາເປັນຕ້ອງເປີດຫນ້ານີ້ຜ່ານ HTTPS ຫຼືໃນ localhost.",
|
||||
"server.connect.camera.unavailable": "ບໍ່ມີກ້ອງຖ່າຍຮູບໃຫ້ໃຊ້ໃນເບຣາວເຊີນີ້.ໃສ່ລາຍລະອຽດການເຊື່ອມຕໍ່ຂອງທ່ານເອງ.",
|
||||
"server.connect.camera.error": "ບໍ່ສາມາດເປີດກ້ອງໄດ້.ອະນຸຍາດໃຫ້ເຂົ້າເຖິງກ້ອງ ຫຼື ປ້ອນລາຍລະອຽດການເຊື່ອມຕໍ່ຂອງທ່ານເອງ.",
|
||||
"dialog.server.menu.remove": "ລຶບ",
|
||||
|
||||
@@ -805,8 +805,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Įveskite galiojantį HTTP arba HTTPS serverio adresą.",
|
||||
"server.connect.failed":
|
||||
"Nepavyko prisijungti. Patikrinkite serverio adresą ir slaptažodį, tada bandykite dar kartą.",
|
||||
"server.connect.pair.description":
|
||||
"Paleiskite šią komandą savo kompiuteryje, kad gautumėte išsamią ryšio informaciją.",
|
||||
"server.connect.scan": "Nuskaitykite QR kodą",
|
||||
"server.connect.scan.description": "Nukreipkite fotoaparatą į QR kodą, rodomą opencode pair.",
|
||||
"server.connect.scan.invalid": "Tai nėra OpenCode susiejimo kodas. Nuskaitykite kodą, kurį rodo opencode pair.",
|
||||
@@ -814,7 +812,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Atidaroma kamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Nepavyko prisijungti prie šio HTTP serverio iš HTTPS puslapio. Vietoj to naudokite HTTPS serverio adresą.",
|
||||
"server.connect.camera.insecure": "Norint nuskaityti QR, reikia atidaryti šį puslapį per HTTPS arba localhost.",
|
||||
"server.connect.camera.unavailable": "Šioje naršyklėje nėra fotoaparato. Įveskite savo ryšio duomenis rankiniu būdu.",
|
||||
"server.connect.camera.error":
|
||||
"Nepavyko atidaryti fotoaparato. Suteikite prieigą prie fotoaparato arba įveskite savo ryšio duomenis rankiniu būdu.",
|
||||
|
||||
@@ -792,7 +792,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Ievadiet derīgu HTTP vai HTTPS servera adresi.",
|
||||
"server.connect.failed":
|
||||
"Nevarēja izveidot savienojumu. Pārbaudiet servera adresi un paroli, pēc tam mēģiniet vēlreiz.",
|
||||
"server.connect.pair.description": "Palaidiet šo komandu datorā, lai iegūtu informāciju par savienojumu.",
|
||||
"server.connect.scan": "Skenējiet QR kodu",
|
||||
"server.connect.scan.description": "Pavērsiet kameru pret QR kodu, ko parāda opencode pair.",
|
||||
"server.connect.scan.invalid": "Šis nav kods OpenCode savienošanai pārī. Skenējiet kodu, ko parāda opencode pair.",
|
||||
@@ -800,7 +799,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Tiek atvērta kamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Nevarēja izveidot savienojumu ar šo HTTP serveri no HTTPS lapas. Tā vietā izmantojiet HTTPS servera adresi.",
|
||||
"server.connect.camera.insecure": "Lai skenētu QR, šī lapa ir jāatver, izmantojot HTTPS vai localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Šai pārlūkprogrammai nav pieejama neviena kamera. Manuāli ievadiet savienojuma informāciju.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -795,8 +795,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Внесете валидна HTTP или HTTPS адреса на серверот.",
|
||||
"server.connect.failed":
|
||||
"Не можеше да се поврзе. Проверете ја адресата на серверот и лозинката, потоа обидете се повторно.",
|
||||
"server.connect.pair.description":
|
||||
"Извршете ја оваа команда на вашиот компјутер за да ги добиете деталите за вашата конекција.",
|
||||
"server.connect.scan": "Скенирај QR код",
|
||||
"server.connect.scan.description": "Насочете ја вашата камера кон QR кодот прикажан од opencode pair.",
|
||||
"server.connect.scan.invalid": "Ова не е OpenCode паринг код. Скенирајте го кодот прикажан од opencode pair.",
|
||||
@@ -804,7 +802,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Отворање на камерата…",
|
||||
"server.connect.mixedContent":
|
||||
"Не можевте да се поврзете на овој HTTP сервер од HTTPS страница. Користете HTTPS сервер адреса наместо тоа.",
|
||||
"server.connect.camera.insecure": "Скенирањето на QR бара отворање на оваа страница преку HTTPS или на localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Нема камера достапна за овој прелистувач. Внесете ги деталите за вашата конекција рачно.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -797,8 +797,6 @@ export const dict = {
|
||||
"server.connect.button": "Холбох",
|
||||
"server.connect.address.invalid": "Хүчинтэй HTTP эсвэл HTTPS серверийн хаягийг оруулна уу.",
|
||||
"server.connect.failed": "Холбогдож чадсангүй. Серверийн хаяг ба нууц үгийг шалгаад дахин оролдоно уу.",
|
||||
"server.connect.pair.description":
|
||||
"Энэ командыг өөрийн компьютер дээр ажиллуулж, холболтын дэлгэрэнгүй мэдээллийг аваарай.",
|
||||
"server.connect.scan": "QR кодыг сканнердах",
|
||||
"server.connect.scan.description": "Камераа opencode pair-ийн харуулсан QR код руу чиглүүлнэ үү.",
|
||||
"server.connect.scan.invalid": "Энэ OpenCode холболтын код биш. opencode pair-ийн харуулсан кодыг уншуулна уу.",
|
||||
@@ -806,7 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Камер нээж байна…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS хуудсаас энэ HTTP серверт холбогдож чадсангүй. Үүний оронд HTTPS серверийн хаягийг ашигла.",
|
||||
"server.connect.camera.insecure": "QR сканнердах нь энэ хуудасыг HTTPS дээр эсвэл localhost дээр нээх шаардлагатай.",
|
||||
"server.connect.camera.unavailable": "Энэ хөтчид камер байхгүй байна. Холболтын мэдээллээ гар аргаар оруулна уу.",
|
||||
"server.connect.camera.error":
|
||||
"Камерыг нээж чадсангүй. Камер ашиглахыг зөвшөөрөх эсвэл холболтын дэлгэрэнгүй мэдээллээ гараар оруулна уу.",
|
||||
|
||||
@@ -795,7 +795,6 @@ export const dict = {
|
||||
"server.connect.button": "Sambung",
|
||||
"server.connect.address.invalid": "Masukkan alamat pelayan HTTP atau HTTPS yang sah.",
|
||||
"server.connect.failed": "Tidak dapat menyambung. Semak alamat pelayan dan kata laluan, kemudian cuba lagi.",
|
||||
"server.connect.pair.description": "Jalankan arahan ini pada komputer anda untuk mendapatkan butiran sambungan anda.",
|
||||
"server.connect.scan": "Imbas kod QR",
|
||||
"server.connect.scan.description": "Tujukan kamera anda ke kod QR yang dipaparkan oleh pasangan opencode.",
|
||||
"server.connect.scan.invalid": "Ini bukan kod pasangan OpenCode. Imbas kod yang ditunjukkan oleh pasangan opencode.",
|
||||
@@ -803,7 +802,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Membuka kamera…",
|
||||
"server.connect.mixedContent":
|
||||
"Tidak dapat sambung ke pelayan HTTP ini dari halaman HTTPS. Gunakan alamat pelayan HTTPS sebaliknya.",
|
||||
"server.connect.camera.insecure": "Imbasan QR memerlukan pembukaan halaman ini melalui HTTPS atau di localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Tiada kamera tersedia untuk pelayar ini. Masukkan butiran sambungan anda secara manual.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -808,7 +808,6 @@ export const dict = {
|
||||
"server.connect.button": "ချိတ်ဆက်မည်",
|
||||
"server.connect.address.invalid": "မှန်ကန်သော HTTP သို့မဟုတ် HTTPS server လိပ်စာကို ထည့်ပါ။",
|
||||
"server.connect.failed": "ချိတ်ဆက်၍ မရပါ။ Server လိပ်စာနှင့် စကားဝှက်ကို စစ်ဆေးပြီး ပြန်ကြိုးစားပါ။",
|
||||
"server.connect.pair.description": "ချိတ်ဆက်ရန်အသေးစိတ်ကို ရယူရန် သင့်ကွန်ပျူတာတွင် ဤ command ကို chạy ဆောင်ရန်။",
|
||||
"server.connect.scan": "QR ကုဒ် စကင်",
|
||||
"server.connect.scan.description": "opencode pair မှ ပြသထားသော QR ကုဒ်ကို သင်၏ ကင်မရာဖြင့် ညွှန်ပါ။",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -816,8 +815,6 @@ export const dict = {
|
||||
"server.connect.camera": "ကင်မရာကို အစုံပေါင်းခြင်း",
|
||||
"server.connect.camera.starting": "ကင်မရာဖွင့်နေသည်…",
|
||||
"server.connect.mixedContent": "HTTPS စာမျက်နှာမှ ဤ HTTP ဆာဗာထံ ချိတ်ဆက်၍ မရနိုင်ပါ။ HTTPS ဆာဗာလိပ်စာကို အသုံးပြုပါ။",
|
||||
"server.connect.camera.insecure":
|
||||
"QR စကင်လုပ်ရန် HTTPS ဖြင့် သို့မဟုတ် localhost တွင် ဤစာမျက်နှာကို ဖွင့်ရန် လိုအပ်သည်။",
|
||||
"server.connect.camera.unavailable": "ဤဘရောက်ဇာတွင် ကင်မရာမရှိပါ။ ချိတ်ဆက်မှုအချက်အလက်များကို လက်ဖြင့်ထည့်ပါ။",
|
||||
"server.connect.camera.error":
|
||||
"ကင်မရာဖွင့်၍ မရနိုင်ပါ။ ကင်မရာခွင့်ပြုချက်ပေးပါ သို့မဟုတ် ချိတ်ဆက်မှုအချက်အလက်များကို လက်ဖြင့်ထည့်ပါ။",
|
||||
|
||||
@@ -797,7 +797,6 @@ export const dict = {
|
||||
"server.connect.button": "जडान गर्नुहोस्",
|
||||
"server.connect.address.invalid": "मान्य HTTP वा HTTPS सर्भर ठेगाना प्रविष्ट गर्नुहोस्।",
|
||||
"server.connect.failed": "जडान हुन सकेन। सर्भर ठेगाना र पासवर्ड जाँच गर्नुहोस्, त्यसपछि फेरि प्रयास गर्नुहोस्।",
|
||||
"server.connect.pair.description": "तपाइँको जडान विवरण प्राप्त गर्न तपाइँको कम्प्युटरमा यो आदेश चलाउनुहोस्।",
|
||||
"server.connect.scan": "QR कोड स्क्यान गर्नुहोस्",
|
||||
"server.connect.scan.description": "ओपनकोड जोडीद्वारा देखाइएको QR कोडमा आफ्नो क्यामेरा देखाउनुहोस्।",
|
||||
"server.connect.scan.invalid": "यो एक OpenCode जोडा कोड होइन। ओपनकोड जोडी द्वारा देखाइएको कोड स्क्यान गर्नुहोस्।",
|
||||
@@ -805,7 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "क्यामेरा खोल्दै…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS पृष्ठबाट यो HTTP सर्भरमा जडान गर्न सकिएन। यसको सट्टा HTTPS सर्भर ठेगाना प्रयोग गर्नुहोस्।",
|
||||
"server.connect.camera.insecure": "QR स्क्यानिङका लागि यो पृष्ठ HTTPS वा लोकलहोस्टमा खोल्न आवश्यक छ।",
|
||||
"server.connect.camera.unavailable":
|
||||
"यो ब्राउजरमा कुनै क्यामेरा उपलब्ध छैन। आफ्नो जडान विवरणहरू म्यानुअल रूपमा प्रविष्ट गर्नुहोस्।",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -23,7 +23,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Voer een geldig HTTP- of HTTPS-serveradres in.",
|
||||
"server.connect.failed":
|
||||
"Kan geen verbinding maken. Controleer het serveradres en het wachtwoord en probeer het opnieuw.",
|
||||
"server.connect.pair.description": "Voer deze opdracht uit op uw computer om uw verbindingsgegevens op te halen.",
|
||||
"server.connect.scan": "Scan de QR-code",
|
||||
"server.connect.scan.description": "Richt uw camera op de QR-code weergegeven door opencode pair.",
|
||||
"server.connect.scan.invalid": "Dit is geen OpenCode-koppelingscode. Scan de code weergegeven door opencode pair.",
|
||||
@@ -31,8 +30,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Camera openen…",
|
||||
"server.connect.mixedContent":
|
||||
"Kan geen verbinding maken met deze HTTP-server vanaf een HTTPS-pagina. Gebruik in plaats daarvan een HTTPS-serveradres.",
|
||||
"server.connect.camera.insecure":
|
||||
"Voor het scannen van QR moet deze pagina worden geopend via HTTPS of op localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Er is geen camera beschikbaar voor deze browser. Voer uw verbindingsgegevens handmatig in.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -26,7 +26,6 @@ export const dict = {
|
||||
"server.connect.button": "Koble til",
|
||||
"server.connect.address.invalid": "Skriv inn en gyldig HTTP- eller HTTPS-serveradresse.",
|
||||
"server.connect.failed": "Kunne ikke koble til. Sjekk serveradressen og passordet, og prøv på nytt.",
|
||||
"server.connect.pair.description": "Kjør denne kommandoen på datamaskinen for å få tilkoblingsdetaljer.",
|
||||
"server.connect.scan": "Skann QR-koden",
|
||||
"server.connect.scan.description": "Pek kameraet mot QR-koden vist av opencode pair.",
|
||||
"server.connect.scan.invalid": "Dette er ikke en OpenCode-paringskode. Skann koden vist av opencode pair.",
|
||||
@@ -34,7 +33,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Åpner kamera …",
|
||||
"server.connect.mixedContent":
|
||||
"Kunne ikke koble til denne HTTP-serveren fra en HTTPS-side. Bruk en HTTPS-serveradresse i stedet.",
|
||||
"server.connect.camera.insecure": "QR-skanning krever at denne siden åpnes over HTTPS eller på localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Ingen kamera er tilgjengelig for denne nettleseren. Skriv inn tilkoblingsdetaljene manuelt.",
|
||||
"server.connect.camera.error": "Kunne ikke åpne kameraet. Gi kameratilgang eller angi tilkoblingsdetaljene manuelt.",
|
||||
|
||||
@@ -803,7 +803,6 @@ export const dict = {
|
||||
"server.connect.button": "جڑیں۔",
|
||||
"server.connect.address.invalid": "ایک درست HTTP یا HTTPS سرور دا پتہ درج کرو۔",
|
||||
"server.connect.failed": "رابطہ نئیں ہو سکا۔ سرور دا پتہ اور پاس ورڈ چیک کرو، پھر دوبارہ کوشش کرو۔",
|
||||
"server.connect.pair.description": "اپنے کنکشن دی تفصیلات حاصل کرنے لئی اس کمانڈ کو اپنے کمپیوٹر پر چلائیں۔",
|
||||
"server.connect.scan": "QR کوڈ اسکین کرو۔",
|
||||
"server.connect.scan.description": "اپنے کیمرے کو اوپن کوڈ پیئر دے ذریعے دکھائے گئے QR کوڈ دی طرف پوائنٹ کرو۔",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -812,7 +811,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "کیمرہ کھل رہا ہے…",
|
||||
"server.connect.mixedContent":
|
||||
"ایک HTTPS صفحہ سے اس HTTP سرور سے مربوط نئیں ہو سکا۔ اس دے بجائے ایک HTTPS سرور دا پتہ استعمال کرو۔",
|
||||
"server.connect.camera.insecure": "QR اسکیننگ لئی اس صفحہ کو HTTPS پر یا لوکل ہوسٹ پر کھولنے دی ضرورت اے۔",
|
||||
"server.connect.camera.unavailable":
|
||||
"اس براؤزر پر کوئی کیمرہ دستیاب نئیں اے۔ اپنے کنکشن دی تفصیلات دستی طور پر درج کرو۔",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -820,7 +820,6 @@ export const dict = {
|
||||
"server.connect.button": "Połącz",
|
||||
"server.connect.address.invalid": "Wprowadź prawidłowy adres serwera HTTP lub HTTPS.",
|
||||
"server.connect.failed": "Nie można się połączyć. Sprawdź adres serwera i hasło, a następnie spróbuj ponownie.",
|
||||
"server.connect.pair.description": "Uruchom to polecenie na swoim komputerze, aby uzyskać szczegóły połączenia.",
|
||||
"server.connect.scan": "Zeskanuj kod QR",
|
||||
"server.connect.scan.description": "Skieruj kamerę na kod QR pokazany przez sparowanie opencode.",
|
||||
"server.connect.scan.invalid": "To nie jest kod parowania OpenCode. Zeskanuj kod pokazany przez sparowanie opencode.",
|
||||
@@ -828,7 +827,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otwieranie kamery…",
|
||||
"server.connect.mixedContent":
|
||||
"Nie można połączyć się z tym serwerem HTTP z strony HTTPS. Zamiast tego użyj adresu serwera HTTPS.",
|
||||
"server.connect.camera.insecure": "Skanowanie QR wymaga otwarcia tej strony przez HTTPS lub na localhost.",
|
||||
"server.connect.camera.unavailable": "Ta przeglądarka nie ma dostępu do kamery. Wprowadź dane połączenia ręcznie.",
|
||||
"server.connect.camera.error":
|
||||
"Nie udało się otworzyć kamery. Zezwól na dostęp do kamery lub wprowadź dane połączenia ręcznie.",
|
||||
|
||||
@@ -790,7 +790,6 @@ export const dict = {
|
||||
"server.connect.button": "Conectați",
|
||||
"server.connect.address.invalid": "Introduceți o adresă validă de server HTTP sau HTTPS.",
|
||||
"server.connect.failed": "Nu s-a putut conecta. Verificați adresa serverului și parola, apoi încercați din nou.",
|
||||
"server.connect.pair.description": "Rulați această comandă pe computer pentru a obține detaliile conexiunii.",
|
||||
"server.connect.scan": "Scanează codul QR",
|
||||
"server.connect.scan.description": "Îndreptați camera către codul QR afișat de opencode pair.",
|
||||
"server.connect.scan.invalid": "Acesta nu este un cod de asociere OpenCode. Scanați codul afișat de opencode pair.",
|
||||
@@ -798,7 +797,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Deschiderea camerei…",
|
||||
"server.connect.mixedContent":
|
||||
"Nu s-a putut conecta la acest server HTTP de pe o pagină HTTPS. Utilizați în schimb o adresă de server HTTPS.",
|
||||
"server.connect.camera.insecure": "Scanarea QR necesită deschiderea acestei pagini prin HTTPS sau pe localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Nicio cameră nu este disponibilă pentru acest browser. Introduceți manual detaliile conexiunii.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -876,7 +876,6 @@ export const dict = {
|
||||
"server.connect.button": "Подключиться",
|
||||
"server.connect.address.invalid": "Введите действительный адрес сервера HTTP или HTTPS.",
|
||||
"server.connect.failed": "Не удалось подключиться. Проверьте адрес сервера и пароль, затем попробуйте снова.",
|
||||
"server.connect.pair.description": "Выполните эту команду на вашем компьютере, чтобы получить данные подключения.",
|
||||
"server.connect.scan": "Сканировать QR-код",
|
||||
"server.connect.scan.description": "Наведите камеру на QR-код, показанный парой opencode.",
|
||||
"server.connect.scan.invalid": "Это не код пары OpenCode. Сканируйте код, показанный парой opencode.",
|
||||
@@ -884,8 +883,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Открытие камеры…",
|
||||
"server.connect.mixedContent":
|
||||
"Не удалось подключиться к этому HTTP-серверу с HTTPS-страницы. Используйте адрес HTTPS-сервера вместо этого.",
|
||||
"server.connect.camera.insecure":
|
||||
"Сканирование QR требует открытия этой страницы через HTTPS или на локальном хосте.",
|
||||
"server.connect.camera.unavailable": "Камера недоступна для этого браузера. Введите данные подключения вручную.",
|
||||
"server.connect.camera.error":
|
||||
"Не удалось открыть камеру. Разрешите доступ к камере или введите данные подключения вручную.",
|
||||
|
||||
@@ -792,7 +792,6 @@ export const dict = {
|
||||
"server.connect.button": "සම්බන්ධ කරන්න",
|
||||
"server.connect.address.invalid": "වලංගු HTTP හෝ HTTPS සේවාදායක ලිපිනයක් ඇතුළත් කරන්න.",
|
||||
"server.connect.failed": "සම්බන්ධ වීමට නොහැකි විය. සේවාදායක ලිපිනය සහ මුරපදය පරීක්ෂා කර නැවත උත්සාහ කරන්න.",
|
||||
"server.connect.pair.description": "ඔබගේ සම්බන්ධතා විස්තර ලබා ගැනීමට මෙම විධානය ඔබගේ පරිගණකයේ ක්රියාත්මක කරන්න.",
|
||||
"server.connect.scan": "QR කේතය පරිලෝකනය කරන්න",
|
||||
"server.connect.scan.description": "opencode pair මගින් පෙන්වන QR කේතය වෙත ඔබේ කැමරාව යොමු කරන්න.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -801,7 +800,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "කැමරාව විවෘත කරමින්...",
|
||||
"server.connect.mixedContent":
|
||||
"මෙම HTTP සේවාදායකයට HTTPS පිටුවකින් සම්බන්ධ වීමට නොහැකි විය. ඒ වෙනුවට HTTPS සේවාදායක ලිපිනයක් භාවිතා කරන්න.",
|
||||
"server.connect.camera.insecure": "QR ස්කෑන් කිරීම සඳහා මෙම පිටුව HTTPS හරහා හෝ localhost මත විවෘත කිරීම අවශ්ය වේ.",
|
||||
"server.connect.camera.unavailable": "මෙම බ්රවුසරයට කැමරාවක් නොමැත. ඔබගේ සම්බන්ධතා විස්තර අතින් ඇතුලත් කරන්න.",
|
||||
"server.connect.camera.error":
|
||||
"කැමරාව විවෘත කිරීමට නොහැකි විය. කැමරා ප්රවේශයට ඉඩ දෙන්න හෝ ඔබගේ සම්බන්ධතා විස්තර හස්තීයව ඇතුලත් කරන්න.",
|
||||
|
||||
@@ -815,7 +815,6 @@ export const dict = {
|
||||
"server.connect.button": "Pripojiť sa",
|
||||
"server.connect.address.invalid": "Zadajte platnú adresu servera s protokolom HTTP alebo HTTPS.",
|
||||
"server.connect.failed": "Nepodarilo sa pripojiť. Skontrolujte adresu servera a heslo a skúste znova.",
|
||||
"server.connect.pair.description": "Spustite tento príkaz na svojom počítači, aby ste získali údaje na pripojenie.",
|
||||
"server.connect.scan": "Naskenujte QR kód",
|
||||
"server.connect.scan.description": "Namierte kameru na QR kód zobrazený pomocou opencode pair.",
|
||||
"server.connect.scan.invalid": "Toto nie je párovací kód OpenCode. Naskenujte kód zobrazený pomocou opencode pair.",
|
||||
@@ -823,7 +822,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Otvorenie kamery…",
|
||||
"server.connect.mixedContent":
|
||||
"Nie je možné pripojiť sa k tomuto HTTP serveru z HTTPS stránky. Namiesto toho použite adresu HTTPS.",
|
||||
"server.connect.camera.insecure": "Na skenovanie QR je potrebné otvoriť túto stránku cez HTTPS alebo na localhoste.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Tento prehliadač nemá k dispozícii žiadnu kameru. Zadajte údaje na pripojenie ručne.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -815,7 +815,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Vnesite veljaven HTTP ali HTTPS naslov strežnika.",
|
||||
"server.connect.failed":
|
||||
"Povezave ni bilo mogoče vzpostaviti. Preverite naslov strežnika in geslo ter poskusite znova.",
|
||||
"server.connect.pair.description": "Za pridobitev podatkov o povezavi zaženite ta ukaz na svojem računalniku.",
|
||||
"server.connect.scan": "Skeniraj QR kodo",
|
||||
"server.connect.scan.description": "Usmerite kamero na QR kodo, ki jo prikaže opencode pair.",
|
||||
"server.connect.scan.invalid": "To ni OpenCode paritna koda. Skenirajte kodo, ki jo prikaže opencode pair.",
|
||||
@@ -823,7 +822,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Odpiranje kamere…",
|
||||
"server.connect.mixedContent":
|
||||
"Z HTTP strežnikom se ni mogoče povezati iz HTTPS strani. Uporabite HTTPS naslov strežnika.",
|
||||
"server.connect.camera.insecure": "Skeniranje QR zahteva odpiranje te strani preko HTTPS ali na localhost.",
|
||||
"server.connect.camera.unavailable": "Brskalnik nima na voljo kamere. Vnesite podatke o povezavi ročno.",
|
||||
"server.connect.camera.error":
|
||||
"Kamer ne morem odpreti. Dovolite dostop do kamere ali vnesite podatke o povezavi ročno.",
|
||||
|
||||
@@ -782,7 +782,6 @@ export const dict = {
|
||||
"server.connect.address.invalid": "Fut një adresë të vlefshme serveri HTTP ose HTTPS.",
|
||||
"server.connect.failed":
|
||||
"Nuk mund të lidhej. Kontrolloni adresën dhe fjalëkalimin e serverit, më pas provoni përsëri.",
|
||||
"server.connect.pair.description": "Ekzekutoni këtë komandë në kompjuterin tuaj për të marrë detajet e lidhjes suaj.",
|
||||
"server.connect.scan": "Skanoni kodin QR",
|
||||
"server.connect.scan.description": "Drejtoje kamerën drejt kodit QR të treguar nga opencode pair.",
|
||||
"server.connect.scan.invalid": "Ky nuk është një kod çiftimi OpenCode. Skanoni kodin e treguar nga opencode pair.",
|
||||
@@ -790,7 +789,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Hapja e kamerës…",
|
||||
"server.connect.mixedContent":
|
||||
"Nuk mund të lidhej me këtë server HTTP nga një faqe HTTPS. Në vend të kësaj, përdorni një adresë serveri HTTPS.",
|
||||
"server.connect.camera.insecure": "Skanimi QR kërkon hapjen e kësaj faqeje mbi HTTPS ose në localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Asnjë kamerë nuk disponohet për këtë shfletues. Futni manualisht detajet e lidhjes suaj.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -803,7 +803,6 @@ export const dict = {
|
||||
"server.connect.button": "Повежи се",
|
||||
"server.connect.address.invalid": "Унесите важећу HTTP или HTTPS адресу сервера.",
|
||||
"server.connect.failed": "Није могуће повезати се. Проверите адресу сервера и лозинку, па покушајте поново.",
|
||||
"server.connect.pair.description": "Покрените ову команду на рачунару да бисте добили податке за повезивање.",
|
||||
"server.connect.scan": "Скенирај QR код",
|
||||
"server.connect.scan.description": "Усмерите камеру на QR код који приказује opencode pair.",
|
||||
"server.connect.scan.invalid": "Ово није OpenCode код за повезивање. Скенирајте код који приказује opencode pair.",
|
||||
@@ -811,7 +810,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Отварање камере…",
|
||||
"server.connect.mixedContent":
|
||||
"Није могуће повезати се на овај HTTP сервер са HTTPS странице. Уместо тога користите HTTPS адресу сервера.",
|
||||
"server.connect.camera.insecure": "Скенирање QR кода захтева отварање ове странице преко HTTPS-а или на лоцалхосту.",
|
||||
"server.connect.camera.unavailable": "Овај прегледник нема доступну камеру. Унесите податке за повезивање ручно.",
|
||||
"server.connect.camera.error":
|
||||
"Није могуће отворити камеру. Дозволите приступ камери или унесите податке за повезивање ручно.",
|
||||
|
||||
@@ -22,7 +22,6 @@ export const dict = {
|
||||
"server.connect.button": "Anslut",
|
||||
"server.connect.address.invalid": "Ange en giltig HTTP- eller HTTPS-serveradress.",
|
||||
"server.connect.failed": "Kunde inte ansluta. Kontrollera serveradressen och lösenordet och försök sedan igen.",
|
||||
"server.connect.pair.description": "Kör det här kommandot på din dator för att få din anslutningsinformation.",
|
||||
"server.connect.scan": "Skanna QR-koden",
|
||||
"server.connect.scan.description": "Rikta kameran mot QR-koden som visas av opencode pair.",
|
||||
"server.connect.scan.invalid": "Detta är inte en OpenCode parningskod. Skanna koden som visas av opencode pair.",
|
||||
@@ -30,7 +29,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Öppnar kameran...",
|
||||
"server.connect.mixedContent":
|
||||
"Det gick inte att ansluta till denna HTTP-server från en HTTPS-sida. Använd en HTTPS-serveradress istället.",
|
||||
"server.connect.camera.insecure": "QR-skanning kräver att den här sidan öppnas över HTTPS eller på localhost.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Ingen kamera är tillgänglig för den här webbläsaren. Ange dina anslutningsuppgifter manuellt.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -796,7 +796,6 @@ export const dict = {
|
||||
"server.connect.button": "Пайваст кунед",
|
||||
"server.connect.address.invalid": "Суроғаи сервери дурусти HTTP ё HTTPS-ро ворид кунед.",
|
||||
"server.connect.failed": "Пайваст шуда натавонист. Суроғаи сервер ва паролро тафтиш кунед ва аз нав кӯшиш кунед.",
|
||||
"server.connect.pair.description": "Барои гирифтани тафсилоти пайвасти худ ин фармонро дар компютери худ иҷро кунед.",
|
||||
"server.connect.scan": "Рамзи QR-ро скан кунед",
|
||||
"server.connect.scan.description": "Камераро ба рамзи QR, ки opencode pair нишон медиҳад, равона кунед.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -805,8 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Кушодани камера…",
|
||||
"server.connect.mixedContent":
|
||||
"Аз саҳифаи HTTPS ба ин сервери HTTP пайваст шуда натавонист. Ба ҷои он суроғаи сервери HTTPS-ро истифода баред.",
|
||||
"server.connect.camera.insecure":
|
||||
"Сканкунии QR талаб мекунад, ки ин саҳифа тавассути HTTPS ё дар localhost кушода шавад.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Барои ин браузер ягон камера дастрас нест. Тафсилоти пайвасти худро дастӣ ворид кунед.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -852,14 +852,12 @@ export const dict = {
|
||||
"server.connect.button": "เชื่อมต่อ",
|
||||
"server.connect.address.invalid": "ใส่ที่อยู่เซิร์ฟเวอร์ HTTP หรือ HTTPS ที่ถูกต้อง",
|
||||
"server.connect.failed": "ไม่สามารถเชื่อมต่อได้ ตรวจสอบที่อยู่เซิร์ฟเวอร์และรหัสผ่าน จากนั้นลองใหม่อีกครั้ง",
|
||||
"server.connect.pair.description": "รันคำสั่งนี้บนคอมพิวเตอร์ของคุณเพื่อรับรายละเอียดการเชื่อมต่อของคุณ",
|
||||
"server.connect.scan": "สแกนรหัส QR",
|
||||
"server.connect.scan.description": "ชี้กล้องของคุณไปที่รหัส QR ที่แสดงโดยการจับคู่ opencode",
|
||||
"server.connect.scan.invalid": "นี่ไม่ใช่รหัสจับคู่ OpenCode สแกนรหัสที่แสดงโดย opencode pair",
|
||||
"server.connect.camera": "กำลังจับคู่กล้อง",
|
||||
"server.connect.camera.starting": "กำลังเปิดกล้อง…",
|
||||
"server.connect.mixedContent": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ HTTP จากหน้า HTTPS ใช้ที่อยู่เซิร์ฟเวอร์ HTTPS แทน",
|
||||
"server.connect.camera.insecure": "การสแกน QR ต้องเปิดหน้านี้ผ่าน HTTPS หรือบน localhost",
|
||||
"server.connect.camera.unavailable": "เบราว์เซอร์นี้ไม่สามารถใช้งานกล้องได้ กรอกรายละเอียดการเชื่อมต่อด้วยตนเอง",
|
||||
"server.connect.camera.error": "ไม่สามารถเปิดกล้องได้ อนุญาตการเข้าถึงกล้องหรือกรอกรายละเอียดการเชื่อมต่อด้วยตนเอง",
|
||||
"dialog.server.menu.remove": "ลบ",
|
||||
|
||||
@@ -791,7 +791,6 @@ export const dict = {
|
||||
"server.connect.button": "Birikdiriň",
|
||||
"server.connect.address.invalid": "Dogry HTTP ýa-da HTTPS serwer salgysyny giriziň.",
|
||||
"server.connect.failed": "Birikip bilmedim Serweriň salgysyny we parolyny barlaň, soňra gaýtadan synanyşyň.",
|
||||
"server.connect.pair.description": "Baglanyş maglumatlaryňyzy almak üçin bu buýrugy kompýuteriňizde işlediň.",
|
||||
"server.connect.scan": "QR koduny skanirläň",
|
||||
"server.connect.scan.description": "Kamerany opencode pair görkezýän QR koda gönükdiriň.",
|
||||
"server.connect.scan.invalid": "Bu OpenCode jübütleme kody däl. opencode pair görkezýän kody skanirläň.",
|
||||
@@ -799,7 +798,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamerany açmak…",
|
||||
"server.connect.mixedContent":
|
||||
"Bu HTTP serwerine HTTPS sahypasyndan birigip bilmedim. Munuň ýerine HTTPS serwer salgysyny ulanyň.",
|
||||
"server.connect.camera.insecure": "QR gözlemek bu sahypany HTTPS ýa-da ýerlihostda açmagy talap edýär.",
|
||||
"server.connect.camera.unavailable": "Bu brauzerde kamera ýok. Birikdiriş maglumatlaryňyzy el bilen giriziň.",
|
||||
"server.connect.camera.error":
|
||||
"Kamerany açyp bolmady Kamera girmegine rugsat beriň ýa-da baglanyşyk maglumatlaryňyzy el bilen giriziň.",
|
||||
|
||||
@@ -860,7 +860,6 @@ export const dict = {
|
||||
"server.connect.button": "Bağlan",
|
||||
"server.connect.address.invalid": "Geçerli bir HTTP veya HTTPS sunucu adresi girin.",
|
||||
"server.connect.failed": "Bağlantı kurulamadı. Sunucu adresini ve şifreyi kontrol edip tekrar deneyin.",
|
||||
"server.connect.pair.description": "Bağlantı ayrıntılarınızı almak için bilgisayarınızda bu komutu çalıştırın.",
|
||||
"server.connect.scan": "QR kodunu tarayın",
|
||||
"server.connect.scan.description": "Kameranızı opencode pair tarafından gösterilen QR koduna doğrultun.",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -869,7 +868,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera açılıyor…",
|
||||
"server.connect.mixedContent":
|
||||
"Bu HTTP sunucusuna bir HTTPS sayfasından bağlanılamadı. Bunun yerine HTTPS sunucu adresini kullanın.",
|
||||
"server.connect.camera.insecure": "QR taraması, bu sayfanın HTTPS veya localhost üzerinden açılmasını gerektirir.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Bu tarayıcıda kamera mevcut değil. Bağlantı ayrıntılarınızı manuel olarak girin.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -885,7 +885,6 @@ export const dict = {
|
||||
"server.connect.button": "Підключитися",
|
||||
"server.connect.address.invalid": "Введіть дійсну адресу сервера HTTP або HTTPS.",
|
||||
"server.connect.failed": "Не вдалося підключитися. Перевірте адресу сервера та пароль, потім повторіть спробу.",
|
||||
"server.connect.pair.description": "Запустіть цю команду на своєму комп’ютері, щоб отримати дані підключення.",
|
||||
"server.connect.scan": "Сканувати QR-код",
|
||||
"server.connect.scan.description": "Наведіть камеру на QR-код, показаний opencode pair.",
|
||||
"server.connect.scan.invalid": "Це не код підключення OpenCode. Відскануйте код, показаний opencode pair.",
|
||||
@@ -893,7 +892,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Відкриваємо камеру…",
|
||||
"server.connect.mixedContent":
|
||||
"Не вдалося підключитися до цього HTTP-сервера з HTTPS-сторінки. Використайте адресу HTTPS-сервера.",
|
||||
"server.connect.camera.insecure": "Сканування QR вимагає відкриття цієї сторінки через HTTPS або на localhost.",
|
||||
"server.connect.camera.unavailable": "У цьому веббраузері немає доступної камери. Введіть дані підключення вручну.",
|
||||
"server.connect.camera.error":
|
||||
"Не вдалося відкрити камеру. Дозвольте доступ до камери або введіть дані підключення вручну.",
|
||||
|
||||
@@ -804,7 +804,6 @@ export const dict = {
|
||||
"server.connect.button": "جڑیں۔",
|
||||
"server.connect.address.invalid": "ایک درست HTTP یا HTTPS سرور کا پتہ درج کریں۔",
|
||||
"server.connect.failed": "رابطہ نہیں ہو سکا۔ سرور کا پتہ اور پاس ورڈ چیک کریں، پھر دوبارہ کوشش کریں۔",
|
||||
"server.connect.pair.description": "اپنے کنکشن کی تفصیلات حاصل کرنے کے لیے اس کمانڈ کو اپنے کمپیوٹر پر چلائیں۔",
|
||||
"server.connect.scan": "QR کوڈ اسکین کریں۔",
|
||||
"server.connect.scan.description": "اپنے کیمرے کو اوپن کوڈ پیئر کے ذریعے دکھائے گئے QR کوڈ کی طرف پوائنٹ کریں۔",
|
||||
"server.connect.scan.invalid":
|
||||
@@ -813,7 +812,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "کیمرہ کھل رہا ہے…",
|
||||
"server.connect.mixedContent":
|
||||
"ایک HTTPS صفحہ سے اس HTTP سرور سے مربوط نہیں ہو سکا۔ اس کے بجائے ایک HTTPS سرور کا پتہ استعمال کریں۔",
|
||||
"server.connect.camera.insecure": "QR اسکیننگ کے لیے اس صفحہ کو HTTPS پر یا لوکل ہوسٹ پر کھولنے کی ضرورت ہے۔",
|
||||
"server.connect.camera.unavailable":
|
||||
"اس براؤزر پر کوئی کیمرہ دستیاب نہیں ہے۔ اپنے کنکشن کی تفصیلات دستی طور پر درج کریں۔",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -797,8 +797,6 @@ export const dict = {
|
||||
"server.connect.button": "Ulanish",
|
||||
"server.connect.address.invalid": "Yaroqli HTTP yoki HTTPS server manzilini kiriting.",
|
||||
"server.connect.failed": "Ulanib bo‘lmadi. Server manzili va parolni tekshiring, keyin qayta urinib ko'ring.",
|
||||
"server.connect.pair.description":
|
||||
"Ulanish ma'lumotlarini olish uchun ushbu buyruqni kompyuteringizda ishga tushiring.",
|
||||
"server.connect.scan": "QR kodini skanerlang",
|
||||
"server.connect.scan.description": "Kamerani opencode pair koʻrsatgan QR kodga qarating.",
|
||||
"server.connect.scan.invalid": "Bu OpenCode ulash kodi emas. opencode pair koʻrsatgan kodni skanerlang.",
|
||||
@@ -806,7 +804,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Kamera ochilmoqda…",
|
||||
"server.connect.mixedContent":
|
||||
"HTTPS sahifasidan ushbu HTTP serveriga ulanib boʻlmadi. Buning o'rniga HTTPS server manzilidan foydalaning.",
|
||||
"server.connect.camera.insecure": "QR skanerlash uchun bu sahifani HTTPS yoki localhost orqali ochish kerak.",
|
||||
"server.connect.camera.unavailable":
|
||||
"Bu brauzerda hech qanday kamera mavjud emas. Ulanish ma'lumotlarini qo'lda kiriting.",
|
||||
"server.connect.camera.error":
|
||||
|
||||
@@ -801,7 +801,6 @@ export const dict = {
|
||||
"server.connect.button": "Kết nối",
|
||||
"server.connect.address.invalid": "Nhập địa chỉ máy chủ HTTP hoặc HTTPS hợp lệ.",
|
||||
"server.connect.failed": "Không thể kết nối. Kiểm tra địa chỉ máy chủ và mật khẩu, sau đó thử lại.",
|
||||
"server.connect.pair.description": "Chạy lệnh này trên máy tính của bạn để lấy thông tin kết nối.",
|
||||
"server.connect.scan": "Quét mã QR",
|
||||
"server.connect.scan.description": "Chĩa camera của bạn vào mã QR được hiển thị bởi cặp opencode.",
|
||||
"server.connect.scan.invalid": "Đây không phải mã ghép cặp OpenCode. Quét mã được hiển thị bởi ghép cặp opencode.",
|
||||
@@ -809,7 +808,6 @@ export const dict = {
|
||||
"server.connect.camera.starting": "Đang mở camera…",
|
||||
"server.connect.mixedContent":
|
||||
"Không thể kết nối với máy chủ HTTP này từ trang HTTPS. Sử dụng địa chỉ máy chủ HTTPS thay vào đó.",
|
||||
"server.connect.camera.insecure": "Quét QR yêu cầu mở trang này qua HTTPS hoặc trên localhost.",
|
||||
"server.connect.camera.unavailable": "Trình duyệt này không có camera. Nhập chi tiết kết nối của bạn thủ công.",
|
||||
"server.connect.camera.error":
|
||||
"Không thể mở camera. Cho phép truy cập camera hoặc nhập chi tiết kết nối của bạn thủ công.",
|
||||
|
||||
@@ -849,14 +849,12 @@ export const dict = {
|
||||
"server.connect.button": "连接",
|
||||
"server.connect.address.invalid": "请输入有效的 HTTP 或 HTTPS 服务器地址。",
|
||||
"server.connect.failed": "无法连接。请检查服务器地址和密码,然后重试。",
|
||||
"server.connect.pair.description": "在您的计算机上运行此命令以获取连接详细信息。",
|
||||
"server.connect.scan": "扫描二维码",
|
||||
"server.connect.scan.description": "将摄像头对准 opencode pair 显示的二维码。",
|
||||
"server.connect.scan.invalid": "这不是 OpenCode 配对码。请扫描 opencode pair 显示的二维码。",
|
||||
"server.connect.camera": "正在配对摄像头",
|
||||
"server.connect.camera.starting": "正在打开摄像头…",
|
||||
"server.connect.mixedContent": "无法从 HTTPS 页面连接此 HTTP 服务器。请使用 HTTPS 服务器地址。",
|
||||
"server.connect.camera.insecure": "扫描二维码需要通过 HTTPS 打开此页面或在本地主机上打开。",
|
||||
"server.connect.camera.unavailable": "此浏览器无法使用摄像头。请手动输入连接详情。",
|
||||
"server.connect.camera.error": "无法打开摄像头。请允许摄像头访问或手动输入连接详情。",
|
||||
"dialog.server.menu.remove": "移除",
|
||||
|
||||
@@ -841,14 +841,12 @@ export const dict = {
|
||||
"server.connect.button": "連線",
|
||||
"server.connect.address.invalid": "請輸入有效的 HTTP 或 HTTPS 伺服器位址。",
|
||||
"server.connect.failed": "無法連線。請檢查伺服器位址和密碼,然後重試。",
|
||||
"server.connect.pair.description": "在您的電腦上執行此命令以取得連線詳細資料。",
|
||||
"server.connect.scan": "掃描 QR 碼",
|
||||
"server.connect.scan.description": "將攝影機對準 opencode pair 顯示的 QR 碼。",
|
||||
"server.connect.scan.invalid": "這不是 OpenCode 配對碼。請掃描 opencode pair 顯示的 QR 碼。",
|
||||
"server.connect.camera": "配對攝影機",
|
||||
"server.connect.camera.starting": "正在開啟攝影機…",
|
||||
"server.connect.mixedContent": "無法從 HTTPS 頁面連線至此 HTTP 伺服器。請改用 HTTPS 伺服器位址。",
|
||||
"server.connect.camera.insecure": "QR 碼掃描需要透過 HTTPS 或在 localhost 上開啟此頁面。",
|
||||
"server.connect.camera.unavailable": "此瀏覽器無可用的攝影機。請手動輸入您的連線資訊。",
|
||||
"server.connect.camera.error": "無法開啟攝影機。請允許攝影機存取或手動輸入您的連線資訊。",
|
||||
"dialog.server.menu.remove": "移除",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSimpleContext } from "@opencode/ui/context"
|
||||
import { Accessor, batch, createEffect, createMemo, createResource, createRoot, getOwner } from "solid-js"
|
||||
import { Accessor, batch, createEffect, createMemo, createResource, createRoot, getOwner, untrack } from "solid-js"
|
||||
import { createServerProjects, RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServers } from "./registry"
|
||||
import { pathKey } from "@/workspaces/path-key"
|
||||
import { useServerHealth } from "@/runtime/server/health"
|
||||
@@ -36,16 +36,32 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
||||
|
||||
const serverCtxs = new Map<ServerConnection.Key, ReturnType<typeof createServerController>>()
|
||||
const serverCtxDisposers = new Map<ServerConnection.Key, () => void>()
|
||||
// The credentials each controller started with, as plain values: the listed connection is a store proxy that
|
||||
// already reads the new password once the user saves it.
|
||||
const serverCtxCredentials = new Map<ServerConnection.Key, string>()
|
||||
|
||||
const owner = getOwner()
|
||||
|
||||
if (!owner) throw new Error("Global provider requires a Solid owner")
|
||||
|
||||
const ensureServerCtx = (conn: ServerConnection.Any) => {
|
||||
const key = ServerConnection.key(conn)
|
||||
const disposeServerCtx = (key: ServerConnection.Key) => {
|
||||
serverCtxDisposers.get(key)?.()
|
||||
serverCtxDisposers.delete(key)
|
||||
serverCtxCredentials.delete(key)
|
||||
serverCtxs.delete(key)
|
||||
}
|
||||
|
||||
const ensureServerCtx = (input: ServerConnection.Any) => {
|
||||
const key = ServerConnection.key(input)
|
||||
// Callers can hold an older copy of the connection; the listed one has the credentials the user saved last.
|
||||
const conn = untrack(() => server.list.find((item) => ServerConnection.key(item) === key)) ?? input
|
||||
const existing = serverCtxs.get(key)
|
||||
|
||||
if (existing) return existing
|
||||
if (existing && serverCtxCredentials.get(key) === credentials(conn)) return existing
|
||||
|
||||
// A controller keeps the credentials it started with, so a server signed in again under the same address needs
|
||||
// a new one.
|
||||
if (existing) disposeServerCtx(key)
|
||||
|
||||
const serverCtx = createRoot((dispose) => {
|
||||
serverCtxDisposers.set(key, dispose)
|
||||
@@ -54,6 +70,7 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
||||
}, owner)
|
||||
|
||||
serverCtxs.set(key, serverCtx)
|
||||
serverCtxCredentials.set(key, credentials(conn))
|
||||
|
||||
return serverCtx
|
||||
}
|
||||
@@ -69,11 +86,8 @@ export const { use: useGlobal, provider: GlobalProvider } = createSimpleContext(
|
||||
|
||||
createEffect(() => {
|
||||
for (const [key] of serverCtxs) {
|
||||
if (serverHealth[key]?.unauthorized || !server.list.find((conn) => ServerConnection.key(conn) === key)) {
|
||||
serverCtxDisposers.get(key)?.()
|
||||
serverCtxDisposers.delete(key)
|
||||
serverCtxs.delete(key)
|
||||
}
|
||||
if (serverHealth[key]?.unauthorized || !server.list.find((conn) => ServerConnection.key(conn) === key))
|
||||
disposeServerCtx(key)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -270,3 +284,8 @@ function isLocalHost(url: string) {
|
||||
|
||||
if (host === "localhost" || host === "127.0.0.1") return "local"
|
||||
}
|
||||
|
||||
/** What an HTTP server's controller authenticates with; other servers keep one controller per id. */
|
||||
function credentials(conn: ServerConnection.Any) {
|
||||
return conn.type === "http" ? `${conn.http.url}\n${conn.http.password ?? ""}` : ""
|
||||
}
|
||||
@@ -26,3 +26,8 @@ export function createCameraAvailability() {
|
||||
|
||||
return { supported, available, refetch: actions.refetch }
|
||||
}
|
||||
|
||||
// Pages served over plain HTTP cannot open the camera. QR codes carry JSON, not a link, so the pasted link is the way in.
|
||||
export function cameraHint() {
|
||||
return window.isSecureContext ? "server.connect.camera.unavailable" : "server.connect.camera.native"
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from "@opencode/ui/dialog"
|
||||
import { Divider } from "@opencode/ui/divider"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { TextInput } from "@opencode/ui/text-input"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { useMutation } from "@tanstack/solid-query"
|
||||
@@ -28,8 +29,10 @@ import { useTabs } from "@/shell/tabs/tabs"
|
||||
import { useCheckServerHealth } from "@/runtime/server/health"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { isMixedContent } from "./browser"
|
||||
import { createCameraAvailability } from "./camera"
|
||||
import type { Pairing } from "./pairing"
|
||||
import { cameraHint, createCameraAvailability } from "./camera"
|
||||
import { bareServerAddress, pairingLink, type Pairing } from "./pairing"
|
||||
import { useRedeemPairing } from "./redeem"
|
||||
import { ConnectMethodSwitch, type ConnectMethod } from "./method"
|
||||
import "@/settings/settings.css"
|
||||
|
||||
const PairingScanner = lazy(() => import("./scanner").then((module) => ({ default: module.PairingScanner })))
|
||||
@@ -47,9 +50,10 @@ export const DialogServer: Component<{
|
||||
const camera = createCameraAvailability()
|
||||
|
||||
const form = createFormController({
|
||||
// Close first: onSave may open the next dialog, such as the folder picker an interrupted action continues with.
|
||||
onSelect: (server) => {
|
||||
props.onSave?.(server)
|
||||
dialog.close()
|
||||
props.onSave?.(server)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -82,6 +86,14 @@ export const DialogServer: Component<{
|
||||
const title = () =>
|
||||
props.mode === "add" ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")
|
||||
|
||||
const error = () => (
|
||||
<Show when={form.state.error()}>
|
||||
<span id="dialog-server-error" class="settings-server-dialog-error" role="alert">
|
||||
{form.state.error()}
|
||||
</span>
|
||||
</Show>
|
||||
)
|
||||
|
||||
const submitLabel = () => {
|
||||
if (form.state.busy()) return language.t("dialog.server.add.checking")
|
||||
|
||||
@@ -106,36 +118,74 @@ export const DialogServer: Component<{
|
||||
form.scan.stop()
|
||||
void camera.refetch()
|
||||
}}
|
||||
redeem={form.scan.redeem}
|
||||
onScan={form.scan.complete}
|
||||
/>
|
||||
</Suspense>
|
||||
}
|
||||
>
|
||||
<div class="flex w-full min-w-0 flex-col gap-6">
|
||||
<Show when={form.state.signedOut()}>
|
||||
<p class="settings-server-dialog-notice" role="status">
|
||||
<Icon name="lock" size="small" />
|
||||
<span>{language.t("dialog.server.signedOut")}</span>
|
||||
</p>
|
||||
</Show>
|
||||
<Show
|
||||
when={form.state.method() === "password"}
|
||||
fallback={
|
||||
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||
<label for="dialog-server-link" class="settings-server-dialog-label">
|
||||
{language.t("server.connect.link")}
|
||||
</label>
|
||||
<TextInput
|
||||
id="dialog-server-link"
|
||||
type="text"
|
||||
appearance="large"
|
||||
class="!w-full self-stretch"
|
||||
dir="ltr"
|
||||
spellcheck={false}
|
||||
autocapitalize="off"
|
||||
value={form.state.link()}
|
||||
placeholder={language.t("server.connect.link.placeholder")}
|
||||
invalid={!!form.state.error()}
|
||||
disabled={form.state.busy()}
|
||||
autofocus
|
||||
aria-describedby={form.state.error() ? "dialog-server-error" : undefined}
|
||||
onInput={(event) => form.change.link(event.currentTarget.value)}
|
||||
onKeyDown={keyDown}
|
||||
/>
|
||||
{error()}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||
<label for="dialog-server-url" class="settings-server-dialog-label">
|
||||
{language.t("dialog.server.add.url")}
|
||||
</label>
|
||||
<TextInput
|
||||
id="dialog-server-url"
|
||||
type="text"
|
||||
appearance="large"
|
||||
class="!w-full self-stretch"
|
||||
value={form.state.value()}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
invalid={!!form.state.error()}
|
||||
disabled={form.state.busy()}
|
||||
autofocus
|
||||
aria-describedby={form.state.error() ? "dialog-server-error" : undefined}
|
||||
onInput={(event) => form.change.value(event.currentTarget.value)}
|
||||
onKeyDown={keyDown}
|
||||
/>
|
||||
{error()}
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||
<label class="settings-server-dialog-label">{language.t("dialog.server.add.url")}</label>
|
||||
<TextInput
|
||||
type="text"
|
||||
appearance="large"
|
||||
class="!w-full self-stretch"
|
||||
value={form.state.value()}
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
invalid={!!form.state.error()}
|
||||
disabled={form.state.busy()}
|
||||
autofocus
|
||||
aria-describedby={form.state.error() ? "dialog-server-error" : undefined}
|
||||
onInput={(event) => form.change.value(event.currentTarget.value)}
|
||||
onKeyDown={keyDown}
|
||||
/>
|
||||
<Show when={form.state.error()}>
|
||||
<span id="dialog-server-error" class="settings-server-dialog-error" role="alert">
|
||||
{form.state.error()}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||
<label class="settings-server-dialog-label">{language.t("dialog.server.add.name")}</label>
|
||||
<label for="dialog-server-name" class="settings-server-dialog-label">
|
||||
{language.t("dialog.server.add.name")}
|
||||
</label>
|
||||
<TextInput
|
||||
id="dialog-server-name"
|
||||
type="text"
|
||||
appearance="large"
|
||||
class="!w-full self-stretch"
|
||||
@@ -146,44 +196,49 @@ export const DialogServer: Component<{
|
||||
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>
|
||||
<Show when={props.mode === "add" && platform.platform === "web"}>
|
||||
<Show when={form.state.method() === "password"}>
|
||||
<div class="flex w-full min-w-0 flex-col gap-2">
|
||||
<label for="dialog-server-password" class="settings-server-dialog-label">
|
||||
{language.t("dialog.server.add.password")}
|
||||
</label>
|
||||
<TextInput
|
||||
id="dialog-server-password"
|
||||
type="password"
|
||||
appearance="large"
|
||||
class="!w-full self-stretch"
|
||||
value={form.state.password()}
|
||||
placeholder={language.t("dialog.server.add.passwordPlaceholder")}
|
||||
disabled={form.state.busy()}
|
||||
aria-describedby="dialog-server-password-hint"
|
||||
onInput={(event) => form.change.password(event.currentTarget.value)}
|
||||
onKeyDown={keyDown}
|
||||
/>
|
||||
<span id="dialog-server-password-hint" class="settings-server-dialog-hint">
|
||||
{language.t("server.connect.password.hint")}
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={form.state.method() === "link" && platform.platform === "web"}>
|
||||
<Show
|
||||
when={camera.available.latest || camera.available.loading}
|
||||
fallback={<span class="settings-server-dialog-hint">{language.t(cameraHint())}</span>}
|
||||
>
|
||||
<Button
|
||||
variant="neutral"
|
||||
size="large"
|
||||
class="!w-full self-stretch"
|
||||
disabled={form.state.busy() || !camera.available.latest}
|
||||
aria-describedby={
|
||||
!camera.available.latest && !camera.available.loading
|
||||
? "dialog-server-camera-unavailable"
|
||||
: undefined
|
||||
}
|
||||
onClick={form.scan.start}
|
||||
>
|
||||
{language.t("server.connect.scan")}
|
||||
</Button>
|
||||
<Show when={!camera.available.latest && !camera.available.loading}>
|
||||
<span id="dialog-server-camera-unavailable" class="settings-server-dialog-hint">
|
||||
{language.t(
|
||||
window.isSecureContext ? "server.connect.camera.unavailable" : "server.connect.camera.insecure",
|
||||
)}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
<ConnectMethodSwitch
|
||||
method={form.state.method()}
|
||||
disabled={form.state.busy()}
|
||||
onChange={form.change.method}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</DialogBody>
|
||||
@@ -208,15 +263,27 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
const global = useGlobal()
|
||||
const language = useLanguage()
|
||||
const checkServerHealth = useCheckServerHealth()
|
||||
const redeem = useRedeemPairing()
|
||||
const healthPreview = createServerHealthPreview(checkServerHealth)
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
mode: "list" as FormMode,
|
||||
originalUrl: undefined as string | undefined,
|
||||
const [store, setStore] = createStore<{
|
||||
mode: FormMode
|
||||
method: ConnectMethod
|
||||
originalUrl: string | undefined
|
||||
link: string
|
||||
values: ServerFormValues
|
||||
scanning: boolean
|
||||
error: string
|
||||
status: boolean | undefined
|
||||
}>({
|
||||
mode: "list",
|
||||
method: "link",
|
||||
originalUrl: undefined,
|
||||
link: "",
|
||||
values: { url: "", name: "", password: "" },
|
||||
scanning: false,
|
||||
error: "",
|
||||
status: undefined as boolean | undefined,
|
||||
status: undefined,
|
||||
})
|
||||
|
||||
onCleanup(healthPreview.cancel)
|
||||
@@ -225,7 +292,9 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
healthPreview.cancel()
|
||||
setStore({
|
||||
mode: "list",
|
||||
method: "link",
|
||||
originalUrl: undefined,
|
||||
link: "",
|
||||
values: { url: "", name: "", password: "" },
|
||||
scanning: false,
|
||||
error: "",
|
||||
@@ -250,24 +319,68 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
remove: (key) => server.remove(key),
|
||||
})
|
||||
|
||||
const request = useMutation(() => ({
|
||||
mutationFn: async () => {
|
||||
const normalized = normalizeServerUrl(store.values.url)
|
||||
// A pairing token works on every address of its server. Signing in again to the edited server keeps its address
|
||||
// (e.g. localhost) even when the link names another one (e.g. 127.0.0.1 from opencode pair).
|
||||
const paired = async (pairing: Pairing) => {
|
||||
const original = store.mode === "edit" ? editing() : undefined
|
||||
|
||||
if (!normalized) {
|
||||
reset()
|
||||
if (!original || original.http.url === pairing.url) return pairing
|
||||
const http = { url: original.http.url, password: pairing.password }
|
||||
|
||||
return (await checkServerHealth(http)).healthy ? http : pairing
|
||||
}
|
||||
|
||||
// Where the form points: a scanned or pasted pairing link wins, else the address and password fields.
|
||||
const target = async (): Promise<ServerConnection.HttpBase | undefined> => {
|
||||
if (store.method === "link") {
|
||||
const redeemed = await redeem(store.link)
|
||||
|
||||
if (redeemed && "error" in redeemed) return void setStore("error", redeemed.error)
|
||||
|
||||
if (redeemed) return paired(redeemed.pairing)
|
||||
// A server address typed here belongs to the password form.
|
||||
const address = bareServerAddress(store.link)
|
||||
|
||||
if (address) {
|
||||
setStore({ method: "password", link: "", values: { ...store.values, url: address } })
|
||||
preview()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return void setStore("error", language.t("server.connect.link.invalid"))
|
||||
}
|
||||
|
||||
// A pairing link pasted into the address field still pairs.
|
||||
const redeemed = await redeem(store.values.url)
|
||||
|
||||
if (redeemed && "error" in redeemed) return void setStore("error", redeemed.error)
|
||||
|
||||
if (redeemed) return paired(redeemed.pairing)
|
||||
const url = normalizeServerUrl(store.values.url)
|
||||
|
||||
if (!url) return void reset()
|
||||
|
||||
return { url, password: store.values.password || undefined }
|
||||
}
|
||||
|
||||
const request = useMutation(() => ({
|
||||
mutationFn: async () => {
|
||||
const http = await target()
|
||||
|
||||
if (!http) return
|
||||
const normalized = http.url
|
||||
const original = store.mode === "edit" ? editing() : undefined
|
||||
|
||||
if (store.mode === "edit" && !original) return
|
||||
const name = store.values.name.trim() || undefined
|
||||
const password = store.values.password || undefined
|
||||
const password = http.password
|
||||
|
||||
// Nothing changed: close, unless the server rejects these credentials, so saving checks them and the action that
|
||||
// asked for sign-in can continue.
|
||||
if (
|
||||
original?.type === "http" &&
|
||||
!global.servers.health[ServerConnection.key(original)]?.unauthorized &&
|
||||
normalized === original.http.url &&
|
||||
name === original.displayName &&
|
||||
password === original.http.password
|
||||
@@ -305,8 +418,8 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
if (normalized === original.http.url) add(connection)
|
||||
|
||||
if (normalized !== original.http.url) replace(ServerConnection.key(original), connection)
|
||||
options.onSelect?.(connection)
|
||||
reset()
|
||||
options.onSelect?.(connection)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -317,7 +430,17 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
},
|
||||
}))
|
||||
|
||||
const preview = () => void healthPreview.preview(store.values, (status) => setStore("status", status))
|
||||
const preview = () => {
|
||||
// A pairing link is not a server address until it is redeemed, and redeeming spends it.
|
||||
if (store.method === "link" || pairingLink(store.values.url)) {
|
||||
healthPreview.cancel()
|
||||
setStore("status", undefined)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
void healthPreview.preview(store.values, (status) => setStore("status", status))
|
||||
}
|
||||
|
||||
const change = (field: keyof ServerFormValues, value: string) => {
|
||||
if (request.isPending) return
|
||||
@@ -336,6 +459,8 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
reset()
|
||||
setStore({
|
||||
mode: "edit",
|
||||
// A signed-out server needs a new pairing link; otherwise editing starts from its address.
|
||||
method: global.servers.health[ServerConnection.key(connection)]?.unauthorized ? "link" : "password",
|
||||
originalUrl: connection.http.url,
|
||||
values: {
|
||||
url: connection.http.url,
|
||||
@@ -353,13 +478,9 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
request.mutate()
|
||||
}
|
||||
|
||||
const pair = (pairing: Pairing) => {
|
||||
const pair = (link: string) => {
|
||||
healthPreview.cancel()
|
||||
setStore({
|
||||
values: { ...store.values, url: pairing.url, password: pairing.password },
|
||||
scanning: false,
|
||||
error: "",
|
||||
})
|
||||
setStore({ method: "link", link, scanning: false, error: "" })
|
||||
request.mutate()
|
||||
}
|
||||
|
||||
@@ -376,14 +497,30 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
open: () => store.mode !== "list",
|
||||
adding: () => store.mode === "add",
|
||||
busy: () => request.isPending,
|
||||
method: () => store.method,
|
||||
link: () => store.link,
|
||||
value: () => store.values.url,
|
||||
name: () => store.values.name,
|
||||
password: () => store.values.password,
|
||||
scanning: () => store.scanning,
|
||||
error: () => store.error,
|
||||
status: () => store.status,
|
||||
signedOut: () => {
|
||||
const original = editing()
|
||||
|
||||
return !!original && !!global.servers.health[ServerConnection.key(original)]?.unauthorized
|
||||
},
|
||||
},
|
||||
change: {
|
||||
method: (method: ConnectMethod) => {
|
||||
if (request.isPending) return
|
||||
setStore({ method, error: "" })
|
||||
preview()
|
||||
},
|
||||
link: (value: string) => {
|
||||
if (request.isPending) return
|
||||
setStore({ link: value, error: "" })
|
||||
},
|
||||
value: (value: string) => change("url", value),
|
||||
name: (value: string) => change("name", value),
|
||||
password: (value: string) => change("password", value),
|
||||
@@ -392,6 +529,7 @@ function createFormController(options: { onSelect?: (server: ServerConnection.Ht
|
||||
start: () => setStore("scanning", true),
|
||||
stop: () => setStore("scanning", false),
|
||||
complete: pair,
|
||||
redeem,
|
||||
},
|
||||
start: { add: startAdd, edit: startEdit },
|
||||
reset,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
|
||||
export type ConnectMethod = "link" | "password"
|
||||
|
||||
// Deliberately quiet, like the Console's API key switch: a pairing link is the way in, the password is the fallback.
|
||||
export function ConnectMethodSwitch(props: {
|
||||
method: ConnectMethod
|
||||
disabled?: boolean
|
||||
onChange: (method: ConnectMethod) => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const next = () => (props.method === "link" ? "password" : "link")
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="server-connect-method"
|
||||
class="flex min-h-7 flex-wrap items-center justify-center gap-1 text-[13px]"
|
||||
>
|
||||
<span class="text-v2-text-text-faint">
|
||||
{language.t(props.method === "link" ? "server.connect.password.prompt" : "server.connect.link.prompt")}
|
||||
</span>
|
||||
<Button variant="ghost-muted" disabled={props.disabled} onClick={() => props.onChange(next())}>
|
||||
{language.t(props.method === "link" ? "server.connect.password.use" : "server.connect.link.use")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { pairingLink } from "./pairing"
|
||||
import { bareServerAddress, legacyPairingLink, pairingLink } from "./pairing"
|
||||
|
||||
describe("pairing link", () => {
|
||||
test("reads the server address and code from opencode pair links", () => {
|
||||
expect(pairingLink(" http://192.168.1.2:49374/auth/connect/abc_DEF-123 ")).toEqual({
|
||||
url: "http://192.168.1.2:49374",
|
||||
urls: ["http://192.168.1.2:49374"],
|
||||
code: "abc_DEF-123",
|
||||
})
|
||||
})
|
||||
|
||||
test("reads every server address from opencode pair QR codes", () => {
|
||||
expect(
|
||||
pairingLink(
|
||||
JSON.stringify({ code: "abc_DEF-123", urls: ["http://192.168.1.2:49374", "http://100.64.0.3:49374"] }),
|
||||
),
|
||||
).toEqual({
|
||||
urls: ["http://192.168.1.2:49374", "http://100.64.0.3:49374"],
|
||||
code: "abc_DEF-123",
|
||||
})
|
||||
})
|
||||
@@ -16,4 +27,31 @@ describe("pairing link", () => {
|
||||
expect(pairingLink("opencode-ios://auth/connect/abc")).toBeUndefined()
|
||||
expect(pairingLink("192.168.1.2:49374")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("rejects malformed QR payloads", () => {
|
||||
expect(pairingLink(JSON.stringify({ code: "abc", urls: [] }))).toBeUndefined()
|
||||
expect(pairingLink(JSON.stringify({ code: "a/b", urls: ["http://192.168.1.2:49374"] }))).toBeUndefined()
|
||||
expect(pairingLink(JSON.stringify({ code: "abc", urls: ["opencode-ios://host"] }))).toBeUndefined()
|
||||
expect(pairingLink(JSON.stringify({ urls: ["http://192.168.1.2:49374"] }))).toBeUndefined()
|
||||
expect(pairingLink("{not json")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("treats only a host without a path as a server address typed into the pairing link field", () => {
|
||||
expect(bareServerAddress("192.168.1.2:4096")).toBe("http://192.168.1.2:4096")
|
||||
expect(bareServerAddress("https://opencode.example.com/")).toBe("https://opencode.example.com")
|
||||
expect(bareServerAddress("localhost")).toBe("http://localhost")
|
||||
expect(bareServerAddress("devbox:4096")).toBe("http://devbox:4096")
|
||||
expect(bareServerAddress("[::1]:4096")).toBe("http://[::1]:4096")
|
||||
expect(bareServerAddress("abc_DEF-123")).toBeUndefined()
|
||||
expect(bareServerAddress("http://192.168.1.2:4096/auth/connect/")).toBeUndefined()
|
||||
expect(bareServerAddress("http://192.168.1.2:4096/auth/connect/…")).toBeUndefined()
|
||||
expect(bareServerAddress("http://192.168.1.2:4096/connect")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("recognizes the credential links that servers printed before one-time links", () => {
|
||||
expect(legacyPairingLink("http://192.168.1.2:49374/connect#eyJ1c2VybmFtZSI6Im9wZW5jb2RlIn0")).toBe(true)
|
||||
expect(legacyPairingLink("http://192.168.1.2:49374/connect?data=eyJ1c2VybmFtZSI6Im9wZW5jb2RlIn0")).toBe(true)
|
||||
expect(legacyPairingLink("http://192.168.1.2:49374/connect")).toBe(false)
|
||||
expect(legacyPairingLink("http://192.168.1.2:49374/auth/connect/abc")).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,6 @@
|
||||
import { OpenCode } from "@opencode/client/promise"
|
||||
import { isUnauthorizedError, OpenCode } from "@opencode/client/promise"
|
||||
import { Option, Schema } from "effect"
|
||||
import { checkServerHealth } from "@/runtime/server/health"
|
||||
import { normalizeServerUrl } from "@/runtime/server/registry"
|
||||
|
||||
export function serverAddress(value: string) {
|
||||
@@ -15,26 +17,116 @@ export function serverAddress(value: string) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
// Links printed by `opencode pair` carry a single-use code that the server exchanges for a session token.
|
||||
const CODE = /^[A-Za-z0-9_-]+$/
|
||||
|
||||
const decodePayload = Schema.decodeUnknownOption(
|
||||
Schema.fromJsonString(Schema.Struct({ code: Schema.String, urls: Schema.Array(Schema.String) })),
|
||||
)
|
||||
|
||||
// `opencode pair` prints links carrying a single-use code that the server exchanges for a session token.
|
||||
// Its QR code carries the same code with every reachable server address as {"code","urls"} JSON.
|
||||
export function pairingLink(value: string) {
|
||||
const url = URL.parse(value.trim())
|
||||
const trimmed = value.trim()
|
||||
|
||||
if (trimmed.startsWith("{")) {
|
||||
const payload = Option.getOrUndefined(decodePayload(trimmed))
|
||||
|
||||
if (!payload || !CODE.test(payload.code)) return
|
||||
const urls = [...new Set(payload.urls)].map(serverAddress)
|
||||
|
||||
if (urls.length === 0 || urls.some((url) => url === undefined)) return
|
||||
|
||||
return { urls: urls.filter((url) => url !== undefined), code: payload.code }
|
||||
}
|
||||
|
||||
const url = URL.parse(trimmed)
|
||||
|
||||
if (!url || (url.protocol !== "http:" && url.protocol !== "https:")) return
|
||||
const code = /^\/auth\/connect\/([A-Za-z0-9_-]+)$/.exec(url.pathname)?.[1]
|
||||
const code = /^\/auth\/connect\/([^/]+)$/.exec(url.pathname)?.[1]
|
||||
const address = serverAddress(url.origin)
|
||||
|
||||
if (!code || !address) return
|
||||
if (!code || !CODE.test(code) || !address) return
|
||||
|
||||
return { url: address, code }
|
||||
return { urls: [address], code }
|
||||
}
|
||||
|
||||
// A server address typed where a pairing link was expected: a host with no path, so a bare code or a cut-off link
|
||||
// is not mistaken for a server. Paths stay allowed in the password form, where servers behind a proxy prefix live.
|
||||
export function bareServerAddress(value: string) {
|
||||
const address = serverAddress(value)
|
||||
|
||||
if (!address) return
|
||||
const url = new URL(address)
|
||||
|
||||
if (url.pathname !== "/") return
|
||||
|
||||
if (url.hostname !== "localhost" && !url.hostname.includes(".") && !url.hostname.startsWith("[") && !url.port) return
|
||||
|
||||
return address
|
||||
}
|
||||
|
||||
// Servers before one-time links paired with `/connect#<credentials>` or `/connect?data=<credentials>`.
|
||||
export function legacyPairingLink(value: string) {
|
||||
const url = URL.parse(value.trim())
|
||||
|
||||
if (!url || (url.protocol !== "http:" && url.protocol !== "https:")) return false
|
||||
|
||||
return url.pathname === "/connect" && (url.hash.length > 1 || url.searchParams.has("data"))
|
||||
}
|
||||
|
||||
export type Pairing = { readonly url: string; readonly password: string }
|
||||
|
||||
export function redeemPairingLink(link: { url: string; code: string }) {
|
||||
return OpenCode.make({ baseUrl: link.url })
|
||||
.server.connect({ code: link.code })
|
||||
export type Redeemed =
|
||||
| { readonly type: "paired"; readonly pairing: Pairing }
|
||||
| { readonly type: "expired" }
|
||||
| { readonly type: "unreachable" }
|
||||
|
||||
// Every address may reach the same server, but the code is single-use, so at most one attempt succeeds.
|
||||
export function redeemPairingLink(link: { urls: ReadonlyArray<string>; code: string }) {
|
||||
const attempts = link.urls.map((url) =>
|
||||
OpenCode.make({ baseUrl: url })
|
||||
.server.connect({ code: link.code }, { signal: AbortSignal.timeout(5_000) })
|
||||
.then((session): Pairing => ({ url, password: session.token })),
|
||||
)
|
||||
|
||||
// Whether the first address answered at all: a 401 there means another address spent the code first.
|
||||
const first = attempts[0]?.then(
|
||||
() => true,
|
||||
(error) => isUnauthorizedError(error),
|
||||
)
|
||||
|
||||
return Promise.any(attempts)
|
||||
.then((pairing) => preferFirst(link.urls, pairing, first))
|
||||
.then(
|
||||
(session): Pairing => ({ url: link.url, password: session.token }),
|
||||
() => undefined,
|
||||
(pairing): Redeemed => ({ type: "paired", pairing }),
|
||||
// Only a server's answer proves the code is spent; addresses that could not be reached leave it valid.
|
||||
(error): Redeemed =>
|
||||
error instanceof AggregateError && error.errors.some(isUnauthorizedError)
|
||||
? { type: "expired" }
|
||||
: { type: "unreachable" },
|
||||
)
|
||||
}
|
||||
|
||||
// The token works on every address of the server, so keep the first one (the address the user chose in Pairing) when it
|
||||
// answers, even if a nearer address answered sooner: a local network address stops working away from that network. An
|
||||
// address that has not answered within a second, such as a local one seen from elsewhere, is not waited for.
|
||||
async function preferFirst(
|
||||
urls: ReadonlyArray<string>,
|
||||
pairing: Pairing,
|
||||
answered: Promise<boolean> | undefined,
|
||||
): Promise<Pairing> {
|
||||
const first = urls[0]
|
||||
|
||||
if (!first || first === pairing.url || !answered) return pairing
|
||||
|
||||
const reachable = await Promise.race([
|
||||
answered,
|
||||
new Promise<false>((resolve) => setTimeout(() => resolve(false), 1_000)),
|
||||
])
|
||||
|
||||
if (!reachable) return pairing
|
||||
const http = { url: first, password: pairing.password }
|
||||
const health = await checkServerHealth(http, globalThis.fetch, { signal: AbortSignal.timeout(5_000), retryCount: 0 })
|
||||
|
||||
return health.healthy ? http : pairing
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { isMixedContent } from "./browser"
|
||||
import { legacyPairingLink, pairingLink, redeemPairingLink, type Pairing } from "./pairing"
|
||||
|
||||
export type Redemption = { readonly pairing: Pairing } | { readonly error: string }
|
||||
|
||||
/** Redeems a pasted or scanned pairing link; resolves undefined when the value is not a pairing link at all. */
|
||||
export type RedeemPairing = (value: string) => Promise<Redemption | undefined>
|
||||
|
||||
/** The code of the last link a form redeemed, and the token the server returned for it. */
|
||||
type Redeemed = { code?: string; pairing?: Pairing }
|
||||
|
||||
// Pasted and scanned links share one redeem path so every surface explains a failure the same way.
|
||||
// Each form keeps the last token it redeemed: the code is spent, so a retry after a failed connection check reuses it,
|
||||
// whichever form the link takes (a scanned QR payload, or the link the scanner then shows).
|
||||
export function useRedeemPairing(): RedeemPairing {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const last: Redeemed = {}
|
||||
|
||||
return async (value) => {
|
||||
const link = pairingLink(value)
|
||||
|
||||
if (!link) return legacyPairingLink(value) ? { error: language.t("server.connect.link.legacy") } : undefined
|
||||
|
||||
// The same code again: reuse its token. An address the user edited since (another forwarded port, say) wins; the
|
||||
// token works on every address of the server.
|
||||
if (last.pairing && last.code === link.code) {
|
||||
const url = link.urls.includes(last.pairing.url) ? last.pairing.url : (link.urls[0] ?? last.pairing.url)
|
||||
|
||||
return { pairing: { url, password: last.pairing.password } }
|
||||
}
|
||||
|
||||
const result = await redeemPairingLink(link)
|
||||
|
||||
if (result.type === "paired") {
|
||||
last.code = link.code
|
||||
last.pairing = result.pairing
|
||||
|
||||
return { pairing: result.pairing }
|
||||
}
|
||||
|
||||
if (result.type === "expired") return { error: language.t("server.connect.link.expired") }
|
||||
|
||||
// A QR code can carry several addresses; the HTTPS page can reach none of them only when every one is plain HTTP.
|
||||
if (platform.platform === "web" && link.urls.every((url) => isMixedContent(location.href, url)))
|
||||
return { error: language.t("server.connect.mixedContent") }
|
||||
|
||||
return { error: language.t("server.connect.link.unreachable", { url: link.urls.join(", ") }) }
|
||||
}
|
||||
}
|
||||
@@ -38,4 +38,10 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
/* Keep the status readable over the live preview while a scanned code is redeemed. */
|
||||
.server-connect-video [role="status"][data-busy] {
|
||||
background: color-mix(in srgb, var(--v2-background-bg-deep) 72%, transparent);
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
}
|
||||
@@ -3,42 +3,69 @@ import { onCleanup, onMount, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { pairingLink, redeemPairingLink, type Pairing } from "./pairing"
|
||||
import { pairingLink } from "./pairing"
|
||||
import type { RedeemPairing } from "./redeem"
|
||||
import "./scanner.css"
|
||||
|
||||
export function PairingScanner(props: { onScan: (value: Pairing) => void; onCancel: () => void }) {
|
||||
// Redeems with the form's own redeem, so the form keeps the token and a failed connection check retries without the
|
||||
// spent code; the form gets back a link it can show, and fills it in.
|
||||
export function PairingScanner(props: { redeem: RedeemPairing; onScan: (link: string) => void; onCancel: () => void }) {
|
||||
const language = useLanguage()
|
||||
const [state, setState] = createStore({ error: "", ready: false })
|
||||
const [state, setState] = createStore({ error: "", ready: false, redeeming: false })
|
||||
const video = document.createElement("video")
|
||||
video.setAttribute("aria-label", language.t("server.connect.camera"))
|
||||
video.setAttribute("playsinline", "")
|
||||
video.muted = true
|
||||
|
||||
onMount(() => {
|
||||
// A code that failed usually stays in view, so skip it for a few seconds instead of redeeming it on every frame.
|
||||
// It is tried again afterwards: a code the server could not be reached for still works once the network is back.
|
||||
const rejected = new Map<string, number>()
|
||||
const scan = { disposed: false }
|
||||
|
||||
// QrScanner hides detached videos, so initialize only after this preview is mounted.
|
||||
const scanner = new QrScanner(
|
||||
video,
|
||||
(result) => {
|
||||
const link = pairingLink(result.data)
|
||||
if (state.redeeming || Date.now() < (rejected.get(result.data) ?? 0)) return
|
||||
setState({ redeeming: true, error: "" })
|
||||
void props.redeem(result.data).then((redeemed) => {
|
||||
if (scan.disposed) return
|
||||
setState("redeeming", false)
|
||||
|
||||
if (!link) {
|
||||
setState("error", language.t("server.connect.scan.invalid"))
|
||||
if (redeemed && "pairing" in redeemed) {
|
||||
scanner.stop()
|
||||
// A QR code carries {"code","urls"} JSON; the form shows the link for the address that paired instead.
|
||||
const code = pairingLink(result.data)?.code
|
||||
props.onScan(code ? new URL(`/auth/connect/${code}`, redeemed.pairing.url).href : result.data)
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
scanner.stop()
|
||||
void redeemPairingLink(link).then((redeemed) => {
|
||||
if (redeemed) return props.onScan(redeemed)
|
||||
setState("error", language.t("server.connect.link.expired"))
|
||||
rejected.set(result.data, Date.now() + 3000)
|
||||
setState("error", redeemed?.error ?? language.t("server.connect.scan.invalid"))
|
||||
})
|
||||
},
|
||||
{ preferredCamera: "environment", maxScansPerSecond: 10, returnDetailedScanResult: true },
|
||||
{
|
||||
preferredCamera: "environment",
|
||||
maxScansPerSecond: 10,
|
||||
returnDetailedScanResult: true,
|
||||
// QrScanner only logs a decoder that fails (for example, a worker the page may not start), so the preview
|
||||
// would look like it is still scanning.
|
||||
onDecodeError: (error) => {
|
||||
// The native BarcodeDetector engine reports an empty frame as "Scanner error: No QR code found".
|
||||
if (String(error).includes(QrScanner.NO_QR_CODE_FOUND) || state.redeeming) return
|
||||
setState("error", language.t("server.connect.scan.failed"))
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Terminal QR codes can be light-on-dark depending on the terminal theme.
|
||||
scanner.setInversionMode("both")
|
||||
onCleanup(() => scanner.destroy())
|
||||
onCleanup(() => {
|
||||
scan.disposed = true
|
||||
scanner.destroy()
|
||||
})
|
||||
void scanner.start().then(
|
||||
() => setState("ready", true),
|
||||
() => setState("error", language.t("server.connect.camera.error")),
|
||||
@@ -50,8 +77,10 @@ export function PairingScanner(props: { onScan: (value: Pairing) => void; onCanc
|
||||
<p>{language.t("server.connect.scan.description")}</p>
|
||||
<div class="server-connect-video">
|
||||
{video}
|
||||
<Show when={!state.ready && !state.error}>
|
||||
<span role="status">{language.t("server.connect.camera.starting")}</span>
|
||||
<Show when={(!state.ready && !state.error) || state.redeeming}>
|
||||
<span role="status" data-busy={state.redeeming ? "" : undefined}>
|
||||
{language.t(state.redeeming ? "dialog.server.add.checking" : "server.connect.camera.starting")}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={state.error}>
|
||||
|
||||
@@ -74,6 +74,17 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* The method switch is a quiet inline link, not a full-width action. */
|
||||
[data-component="server-connect-method"] button {
|
||||
min-height: 28px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.server-connect-hint {
|
||||
font-size: 12px;
|
||||
line-height: var(--line-height-compact);
|
||||
}
|
||||
|
||||
.server-connect-error {
|
||||
color: var(--v2-state-fg-danger);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import { Wordmark } from "@opencode/ui/wordmark"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { useCheckServerHealth } from "@/runtime/server/health"
|
||||
import { useServers } from "@/runtime/server/registry"
|
||||
import { pairingLink, redeemPairingLink, serverAddress } from "./pairing"
|
||||
import { ServerConnection, useServers } from "@/runtime/server/registry"
|
||||
import { bareServerAddress, serverAddress, type Pairing } from "./pairing"
|
||||
import { useRedeemPairing } from "./redeem"
|
||||
import { isMixedContent } from "./browser"
|
||||
import { createCameraAvailability } from "./camera"
|
||||
import { cameraHint, createCameraAvailability } from "./camera"
|
||||
import { ConnectMethodSwitch, type ConnectMethod } from "./method"
|
||||
import "./screen.css"
|
||||
|
||||
const PairingScanner = lazy(() => import("./scanner").then((module) => ({ default: module.PairingScanner })))
|
||||
@@ -20,55 +22,95 @@ export function ConnectServerScreen(props: { url?: string } = {}) {
|
||||
const platform = usePlatform()
|
||||
const servers = useServers()
|
||||
const check = useCheckServerHealth()
|
||||
const redeem = useRedeemPairing()
|
||||
const camera = createCameraAvailability()
|
||||
const [state, setState] = createStore({ url: props.url ?? "", password: "", error: "", scanning: false })
|
||||
|
||||
const connectionError = () =>
|
||||
const [state, setState] = createStore<{
|
||||
method: ConnectMethod
|
||||
link: string
|
||||
url: string
|
||||
password: string
|
||||
error: string
|
||||
scanning: boolean
|
||||
}>({
|
||||
method: "link",
|
||||
link: "",
|
||||
url: props.url ?? "",
|
||||
password: "",
|
||||
error: "",
|
||||
scanning: false,
|
||||
})
|
||||
|
||||
// A pairing token works on every address of its server. Signing in again to the server that signed out keeps its
|
||||
// address (e.g. localhost) even when the link names another one (e.g. 127.0.0.1 from opencode pair).
|
||||
const target = async (pairing: Pairing) => {
|
||||
if (!props.url || props.url === pairing.url) return pairing
|
||||
const http = { url: props.url, password: pairing.password }
|
||||
|
||||
return (await check(http)).healthy ? http : pairing
|
||||
}
|
||||
|
||||
const connect = async (http: ServerConnection.HttpBase) => {
|
||||
const result = await check(http)
|
||||
|
||||
if (!result.healthy) {
|
||||
setState("error", connectionError(http.url))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Signing in again keeps the name the user gave this server.
|
||||
const existing = servers.list.find(
|
||||
(server): server is ServerConnection.Http => server.type === "http" && server.http.url === http.url,
|
||||
)
|
||||
|
||||
servers.add(existing ? { ...existing, http } : { type: "http", http })
|
||||
}
|
||||
|
||||
const connectionError = (url: string) =>
|
||||
language.t(
|
||||
platform.platform === "web" && isMixedContent(location.href, state.url)
|
||||
platform.platform === "web" && isMixedContent(location.href, url)
|
||||
? "server.connect.mixedContent"
|
||||
: "server.connect.failed",
|
||||
)
|
||||
|
||||
const request = useMutation(() => ({
|
||||
mutationFn: async () => {
|
||||
const link = pairingLink(state.url)
|
||||
if (state.method === "link") {
|
||||
const redeemed = await redeem(state.link)
|
||||
|
||||
if (link) {
|
||||
const redeemed = await redeemPairingLink(link)
|
||||
if (redeemed && "error" in redeemed) return void setState("error", redeemed.error)
|
||||
|
||||
if (!redeemed) {
|
||||
setState("error", language.t("server.connect.link.expired"))
|
||||
if (redeemed) return connect(await target(redeemed.pairing))
|
||||
// A server address typed here belongs to the password form.
|
||||
const address = bareServerAddress(state.link)
|
||||
|
||||
return
|
||||
}
|
||||
if (address) return void setState({ method: "password", url: address, link: "", error: "" })
|
||||
|
||||
// Keep the token in the form so a failed connection check can retry without the spent code.
|
||||
setState({ url: link.url, password: redeemed.password })
|
||||
return void setState("error", language.t("server.connect.link.invalid"))
|
||||
}
|
||||
|
||||
// A pairing link pasted into the address field still pairs.
|
||||
const redeemed = await redeem(state.url)
|
||||
|
||||
if (redeemed && "error" in redeemed) return void setState("error", redeemed.error)
|
||||
|
||||
if (redeemed) return connect(await target(redeemed.pairing))
|
||||
const url = serverAddress(state.url)
|
||||
|
||||
if (!url) {
|
||||
setState("error", language.t("server.connect.address.invalid"))
|
||||
if (!url) return void setState("error", language.t("server.connect.address.invalid"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const http = { url, password: state.password || undefined }
|
||||
const result = await check(http)
|
||||
|
||||
if (!result.healthy) {
|
||||
setState("error", connectionError())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
servers.add({ type: "http", http })
|
||||
await connect({ url, password: state.password || undefined })
|
||||
},
|
||||
onError: () => setState("error", connectionError()),
|
||||
onError: () => setState("error", connectionError(state.method === "link" ? state.link : state.url)),
|
||||
}))
|
||||
|
||||
const submit = () => {
|
||||
if (request.isPending) return
|
||||
setState("error", "")
|
||||
request.mutate()
|
||||
}
|
||||
|
||||
return (
|
||||
<main data-component="connect-server" aria-labelledby="server-connect-title">
|
||||
<div class="server-connect-content">
|
||||
@@ -77,19 +119,22 @@ export function ConnectServerScreen(props: { url?: string } = {}) {
|
||||
</div>
|
||||
<header>
|
||||
<h1 id="server-connect-title">{language.t("server.connect.title")}</h1>
|
||||
<p>{language.t("server.connect.description")}</p>
|
||||
<p>
|
||||
{language.t(state.method === "link" ? "server.connect.description.pairing" : "server.connect.description")}
|
||||
</p>
|
||||
</header>
|
||||
<Show
|
||||
when={!state.scanning}
|
||||
fallback={
|
||||
<Suspense fallback={<p role="status">{language.t("server.connect.camera.starting")}</p>}>
|
||||
<PairingScanner
|
||||
redeem={redeem}
|
||||
onCancel={() => {
|
||||
setState("scanning", false)
|
||||
void camera.refetch()
|
||||
}}
|
||||
onScan={(pairing) => {
|
||||
setState({ url: pairing.url, password: pairing.password, error: "", scanning: false })
|
||||
onScan={(link) => {
|
||||
setState({ method: "link", link, error: "", scanning: false })
|
||||
request.mutate()
|
||||
}}
|
||||
/>
|
||||
@@ -99,78 +144,111 @@ export function ConnectServerScreen(props: { url?: string } = {}) {
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (request.isPending) return
|
||||
setState("error", "")
|
||||
request.mutate()
|
||||
submit()
|
||||
}}
|
||||
>
|
||||
<div class="server-connect-field">
|
||||
<label for="server-connect-url">{language.t("dialog.server.add.url")}</label>
|
||||
<TextInput
|
||||
id="server-connect-url"
|
||||
name="server"
|
||||
dir="ltr"
|
||||
type="text"
|
||||
inputMode="url"
|
||||
autocomplete="url"
|
||||
autocapitalize="off"
|
||||
spellcheck={false}
|
||||
required
|
||||
appearance="large"
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
value={state.url}
|
||||
disabled={request.isPending}
|
||||
aria-describedby={state.error ? "server-connect-error" : undefined}
|
||||
onInput={(event) => setState({ url: event.currentTarget.value, error: "" })}
|
||||
/>
|
||||
</div>
|
||||
<div class="server-connect-field">
|
||||
<label for="server-connect-password">{language.t("dialog.server.add.password")}</label>
|
||||
<TextInput
|
||||
id="server-connect-password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
appearance="large"
|
||||
value={state.password}
|
||||
disabled={request.isPending}
|
||||
onInput={(event) => setState({ password: event.currentTarget.value, error: "" })}
|
||||
/>
|
||||
</div>
|
||||
<Show
|
||||
when={state.method === "password"}
|
||||
fallback={
|
||||
<div class="server-connect-field">
|
||||
<label for="server-connect-link">{language.t("server.connect.link")}</label>
|
||||
<TextInput
|
||||
id="server-connect-link"
|
||||
name="link"
|
||||
dir="ltr"
|
||||
type="text"
|
||||
inputMode="url"
|
||||
autocapitalize="off"
|
||||
spellcheck={false}
|
||||
required
|
||||
appearance="large"
|
||||
placeholder={language.t("server.connect.link.placeholder")}
|
||||
value={state.link}
|
||||
disabled={request.isPending}
|
||||
aria-describedby={state.error ? "server-connect-error" : undefined}
|
||||
onInput={(event) => setState({ link: event.currentTarget.value, error: "" })}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="server-connect-field">
|
||||
<label for="server-connect-url">{language.t("dialog.server.add.url")}</label>
|
||||
<TextInput
|
||||
id="server-connect-url"
|
||||
name="server"
|
||||
dir="ltr"
|
||||
type="text"
|
||||
inputMode="url"
|
||||
autocomplete="url"
|
||||
autocapitalize="off"
|
||||
spellcheck={false}
|
||||
required
|
||||
appearance="large"
|
||||
placeholder={language.t("dialog.server.add.placeholder")}
|
||||
value={state.url}
|
||||
disabled={request.isPending}
|
||||
aria-describedby={state.error ? "server-connect-error" : undefined}
|
||||
onInput={(event) => setState({ url: event.currentTarget.value, error: "" })}
|
||||
/>
|
||||
</div>
|
||||
<div class="server-connect-field">
|
||||
<label for="server-connect-password">{language.t("dialog.server.add.password")}</label>
|
||||
<TextInput
|
||||
id="server-connect-password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
appearance="large"
|
||||
value={state.password}
|
||||
disabled={request.isPending}
|
||||
aria-describedby="server-connect-password-hint"
|
||||
onInput={(event) => setState({ password: event.currentTarget.value, error: "" })}
|
||||
/>
|
||||
<p id="server-connect-password-hint" class="server-connect-hint">
|
||||
{language.t("server.connect.password.hint")}
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={state.error}>
|
||||
<p id="server-connect-error" class="server-connect-error" role="alert">
|
||||
{state.error}
|
||||
</p>
|
||||
</Show>
|
||||
<Button type="submit" variant="contrast" size="large" disabled={request.isPending || !state.url.trim()}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contrast"
|
||||
size="large"
|
||||
disabled={request.isPending || !(state.method === "link" ? state.link : state.url).trim()}
|
||||
>
|
||||
{language.t(request.isPending ? "dialog.server.add.checking" : "server.connect.button")}
|
||||
</Button>
|
||||
</form>
|
||||
<Show when={platform.platform === "web"}>
|
||||
<Button
|
||||
variant="neutral"
|
||||
size="large"
|
||||
disabled={request.isPending || !camera.available.latest}
|
||||
aria-describedby={
|
||||
!camera.available.latest && !camera.available.loading ? "server-connect-camera-unavailable" : undefined
|
||||
}
|
||||
onClick={() => setState("scanning", true)}
|
||||
<Show when={state.method === "link" && platform.platform === "web"}>
|
||||
<Show
|
||||
when={camera.available.latest || camera.available.loading}
|
||||
fallback={<p>{language.t(cameraHint())}</p>}
|
||||
>
|
||||
{language.t("server.connect.scan")}
|
||||
</Button>
|
||||
<Show when={!camera.available.latest && !camera.available.loading}>
|
||||
<p id="server-connect-camera-unavailable">
|
||||
{language.t(
|
||||
window.isSecureContext ? "server.connect.camera.unavailable" : "server.connect.camera.insecure",
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
variant="neutral"
|
||||
size="large"
|
||||
disabled={request.isPending || !camera.available.latest}
|
||||
onClick={() => setState("scanning", true)}
|
||||
>
|
||||
{language.t("server.connect.scan")}
|
||||
</Button>
|
||||
</Show>
|
||||
</Show>
|
||||
<footer>
|
||||
<p>{language.t("server.connect.pair.description")}</p>
|
||||
<code dir="ltr">opencode pair</code>
|
||||
</footer>
|
||||
<ConnectMethodSwitch
|
||||
method={state.method}
|
||||
disabled={request.isPending}
|
||||
onChange={(method) => setState({ method, error: "" })}
|
||||
/>
|
||||
<Show when={state.method === "link"}>
|
||||
<footer>
|
||||
<p>{language.t("server.connect.pair.link")}</p>
|
||||
<code dir="ltr">opencode pair</code>
|
||||
</footer>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createResource } from "solid-js"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { ExternalLink } from "@/runtime/platform/external-link"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import legal from "./legal.svg"
|
||||
import anomalyBrush from "./anomaly-brush.svg"
|
||||
import { AnimatedWordmark } from "./animated-wordmark"
|
||||
@@ -31,6 +33,7 @@ const illustrators = ["usrnk1", "ludvigrask_", "arvsrn", "iamdavidhill"] as cons
|
||||
export function SettingsAbout(props: { active: boolean }) {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const dialog = useDialog()
|
||||
|
||||
const [otherContributors] = createResource(
|
||||
() => props.active || undefined,
|
||||
@@ -53,6 +56,30 @@ export function SettingsAbout(props: { active: boolean }) {
|
||||
</ExternalLink>,
|
||||
]
|
||||
|
||||
let noticesButton: HTMLButtonElement | undefined
|
||||
|
||||
const showNotices = async () => {
|
||||
// The license texts load only when someone reads them.
|
||||
const loaded = await import("./notices/dialog").catch(() => undefined)
|
||||
|
||||
if (!loaded) {
|
||||
showToast({ variant: "error", title: language.t("settings.about.notices.loadFailed") })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// dialog.show has no trigger for Kobalte to restore, so closing returns focus to the button by hand.
|
||||
void dialog.show(() => (
|
||||
<loaded.default
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!noticesButton?.isConnected) return
|
||||
event.preventDefault()
|
||||
noticesButton.focus({ preventScroll: true })
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="settings-about-content">
|
||||
<div class="settings-about-intro">
|
||||
@@ -90,6 +117,11 @@ export function SettingsAbout(props: { active: boolean }) {
|
||||
<p>{language.t("settings.about.description")}</p>
|
||||
<p>{language.t("settings.about.trademark")}</p>
|
||||
<p>{language.t("settings.about.typeset")}</p>
|
||||
<p>
|
||||
<button ref={noticesButton} type="button" class="settings-about-link" onClick={() => void showNotices()}>
|
||||
{language.t("settings.about.notices.title")}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>{language.t("settings.about.tagline")}</p>
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Elia Hilse, The OpenOOXML Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,2 @@
|
||||
BetterOffice
|
||||
Copyright 2026 Elia Hilse, The OpenOOXML Project
|
||||
@@ -0,0 +1,770 @@
|
||||
// Generated from the Cargo.lock of each published BetterOffice engine and the license files of the published
|
||||
// crate sources. Regenerate it when the @betteroffice packages update; do not edit it by hand.
|
||||
export const crates = [
|
||||
{
|
||||
name: "adler2",
|
||||
version: "2.0.1",
|
||||
license: "0BSD OR MIT OR Apache-2.0",
|
||||
repository: "https://github.com/oyvindln/adler2",
|
||||
copyright: ["Copyright (c) Jonas Schievink, oyvindln"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "allocator-api2",
|
||||
version: "0.2.21",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/zakarumych/allocator-api2",
|
||||
copyright: ["Copyright (c) Zakarum"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "arc-swap",
|
||||
version: "1.9.2",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/vorner/arc-swap",
|
||||
copyright: ["Copyright (c) 2017 arc-swap developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "arrayref",
|
||||
version: "0.3.9",
|
||||
license: "BSD-2-Clause",
|
||||
repository: "https://github.com/droundy/arrayref",
|
||||
copyright: ["Copyright (c) 2015 David Roundy <roundyd@physics.oregonstate.edu>"],
|
||||
texts: ["bsd-2-clause"],
|
||||
},
|
||||
{
|
||||
name: "arrayvec",
|
||||
version: "0.7.8",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/bluss/arrayvec",
|
||||
copyright: ['Copyright (c) Ulrik Sverdrup "bluss" 2015-2023'],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "async-lock",
|
||||
version: "3.4.2",
|
||||
license: "Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/smol-rs/async-lock",
|
||||
copyright: ["Copyright (c) Stjepan Glavina"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "base64",
|
||||
version: "0.22.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/marshallpierce/rust-base64",
|
||||
copyright: ["Copyright (c) 2015 Alice Maz"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "bitflags",
|
||||
version: "2.13.0",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/bitflags/bitflags",
|
||||
copyright: ["Copyright (c) 2014 The Rust Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "block-buffer",
|
||||
version: "0.10.4",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/RustCrypto/utils",
|
||||
copyright: ["Copyright (c) 2018-2019 The RustCrypto Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "bytemuck",
|
||||
version: "1.25.1",
|
||||
license: "Zlib OR Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/Lokathor/bytemuck",
|
||||
copyright: ['Copyright (c) 2019 Daniel "Lokathor" Gee.'],
|
||||
texts: ["mit-2"],
|
||||
},
|
||||
{
|
||||
name: "cfg-if",
|
||||
version: "1.0.4",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-lang/cfg-if",
|
||||
copyright: ["Copyright (c) 2014 Alex Crichton"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "concurrent-queue",
|
||||
version: "2.5.0",
|
||||
license: "Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/smol-rs/concurrent-queue",
|
||||
copyright: ["Copyright (c) Stjepan Glavina, Taiki Endo, John Nunley"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "core_maths",
|
||||
version: "0.1.1",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/robertbastian/core_maths",
|
||||
copyright: ["Copyright (c) 2024 Robert Bastian"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "crc32fast",
|
||||
version: "1.5.0",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/srijs/rust-crc32fast",
|
||||
copyright: ["Copyright (c) 2018 Sam Rijs, Alex Crichton and contributors"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "crossbeam-utils",
|
||||
version: "0.8.22",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/crossbeam-rs/crossbeam",
|
||||
copyright: ["Copyright (c) 2019 The Crossbeam Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "crypto-common",
|
||||
version: "0.1.7",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/RustCrypto/traits",
|
||||
copyright: ["Copyright (c) 2021 RustCrypto Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "dashmap",
|
||||
version: "6.2.1",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/xacrimon/dashmap",
|
||||
copyright: ["Copyright (c) 2019 Acrimon"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "digest",
|
||||
version: "0.10.7",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/RustCrypto/traits",
|
||||
copyright: ["Copyright (c) 2017 Artyom Pavlov"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "encoding_rs",
|
||||
version: "0.8.35",
|
||||
license: "(Apache-2.0 OR MIT) AND BSD-3-Clause",
|
||||
repository: "https://github.com/hsivonen/encoding_rs",
|
||||
copyright: ["Copyright Mozilla Foundation", "Copyright © WHATWG (Apple, Google, Mozilla, Microsoft)."],
|
||||
texts: ["mit", "bsd-3-clause"],
|
||||
},
|
||||
{
|
||||
name: "equivalent",
|
||||
version: "1.0.2",
|
||||
license: "Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/indexmap-rs/equivalent",
|
||||
copyright: ["Copyright (c) 2016--2023"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "event-listener-strategy",
|
||||
version: "0.5.4",
|
||||
license: "Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/smol-rs/event-listener-strategy",
|
||||
copyright: ["Copyright (c) John Nunley"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "event-listener",
|
||||
version: "5.4.1",
|
||||
license: "Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/smol-rs/event-listener",
|
||||
copyright: ["Copyright (c) Stjepan Glavina, John Nunley"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "fastrand",
|
||||
version: "2.4.1",
|
||||
license: "Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/smol-rs/fastrand",
|
||||
copyright: ["Copyright (c) Stjepan Glavina"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "fdeflate",
|
||||
version: "0.3.7",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/image-rs/fdeflate",
|
||||
copyright: ["Copyright (c) The image-rs Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "flate2",
|
||||
version: "1.1.9",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-lang/flate2-rs",
|
||||
copyright: ["Copyright (c) 2014-2026 Alex Crichton"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "foldhash",
|
||||
version: "0.2.0",
|
||||
license: "Zlib",
|
||||
repository: "https://github.com/orlp/foldhash",
|
||||
copyright: ["Copyright (c) 2024 Orson Peters"],
|
||||
texts: ["zlib"],
|
||||
},
|
||||
{
|
||||
name: "font-types",
|
||||
version: "0.12.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/googlefonts/fontations",
|
||||
copyright: ["Copyright (c) 2019 Fontations Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "futures-core",
|
||||
version: "0.3.32",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-lang/futures-rs",
|
||||
copyright: ["Copyright (c) 2016 Alex Crichton", "Copyright (c) 2017 The Tokio Authors"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "futures-task",
|
||||
version: "0.3.32",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-lang/futures-rs",
|
||||
copyright: ["Copyright (c) 2016 Alex Crichton", "Copyright (c) 2017 The Tokio Authors"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "futures-util",
|
||||
version: "0.3.32",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-lang/futures-rs",
|
||||
copyright: ["Copyright (c) 2016 Alex Crichton", "Copyright (c) 2017 The Tokio Authors"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "generic-array",
|
||||
version: "0.14.7",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/fizyk20/generic-array",
|
||||
copyright: ["Copyright (c) 2015 Bartłomiej Kamiński"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "getrandom",
|
||||
version: "0.3.4",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-random/getrandom",
|
||||
copyright: [
|
||||
"Copyright (c) 2018-2025 The rust-random Project Developers",
|
||||
"Copyright (c) 2014 The Rust Project Developers",
|
||||
],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "half",
|
||||
version: "2.7.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/VoidStarKat/half-rs",
|
||||
copyright: ["Copyright (c) Kathryn Long"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "hashbrown",
|
||||
version: "0.14.5",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-lang/hashbrown",
|
||||
copyright: ["Copyright (c) 2016 Amanieu d'Antras"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "hashbrown",
|
||||
version: "0.17.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-lang/hashbrown",
|
||||
copyright: ["Copyright (c) 2016 Amanieu d'Antras"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "indexmap",
|
||||
version: "2.14.0",
|
||||
license: "Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/indexmap-rs/indexmap",
|
||||
copyright: ["Copyright (c) 2016--2017"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "itoa",
|
||||
version: "1.0.18",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/dtolnay/itoa",
|
||||
copyright: ["Copyright (c) David Tolnay"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "js-sys",
|
||||
version: "0.3.103",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/js-sys",
|
||||
copyright: ["Copyright (c) 2014 Alex Crichton"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "libm",
|
||||
version: "0.2.16",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/rust-lang/compiler-builtins",
|
||||
copyright: [
|
||||
"Copyright (c) 2018 Jorge Aparicio",
|
||||
"Copyright © 2005-2020 Rich Felker, et al.",
|
||||
"Copyright © 1993,2004 Sun Microsystems",
|
||||
"Copyright © 2003-2011 David Schultz",
|
||||
"Copyright © 2003-2009 Steven G. Kargl",
|
||||
"Copyright © 2003-2009 Bruce D. Evans",
|
||||
"Copyright © 2008 Stephen L. Moshier",
|
||||
"Copyright © 2017-2018 Arm Limited",
|
||||
],
|
||||
texts: ["mit-3"],
|
||||
},
|
||||
{
|
||||
name: "lock_api",
|
||||
version: "0.4.14",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/Amanieu/parking_lot",
|
||||
copyright: ["Copyright (c) 2016 The Rust Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "log",
|
||||
version: "0.4.33",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/rust-lang/log",
|
||||
copyright: ["Copyright (c) 2014 The Rust Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "memchr",
|
||||
version: "2.8.3",
|
||||
license: "Unlicense OR MIT",
|
||||
repository: "https://github.com/BurntSushi/memchr",
|
||||
copyright: ["Copyright (c) 2015 Andrew Gallant"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "miniz_oxide",
|
||||
version: "0.8.9",
|
||||
license: "MIT OR Zlib OR Apache-2.0",
|
||||
repository: "https://github.com/Frommi/miniz_oxide/tree/master/miniz_oxide",
|
||||
copyright: [
|
||||
"Copyright 2013-2014 RAD Game Tools and Valve Software",
|
||||
"Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC",
|
||||
"Copyright (c) 2017 Frommi",
|
||||
"Copyright (c) 2017-2024 oyvindln",
|
||||
],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "once_cell",
|
||||
version: "1.21.4",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/matklad/once_cell",
|
||||
copyright: ["Copyright (c) Aleksey Kladov"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "parking_lot_core",
|
||||
version: "0.9.12",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/Amanieu/parking_lot",
|
||||
copyright: ["Copyright (c) 2016 The Rust Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "pin-project-lite",
|
||||
version: "0.2.17",
|
||||
license: "Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/taiki-e/pin-project-lite",
|
||||
copyright: ["Copyright (c) Taiki Endo"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "png",
|
||||
version: "0.18.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/image-rs/image-png",
|
||||
copyright: ["Copyright (c) 2015 nwin"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "quick-error",
|
||||
version: "2.0.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "http://github.com/tailhook/quick-error",
|
||||
copyright: ["Copyright (c) 2015 The quick-error Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "quick-xml",
|
||||
version: "0.41.0",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/tafia/quick-xml",
|
||||
copyright: ["Copyright (c) 2016 Johann Tuffe"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "read-fonts",
|
||||
version: "0.41.0",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/googlefonts/fontations",
|
||||
copyright: ["Copyright (c) 2019 Fontations Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "rustybuzz",
|
||||
version: "0.20.1",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/harfbuzz/rustybuzz",
|
||||
copyright: ["Copyright (c) HarfBuzz developers", "Copyright (c) 2020 Yevhenii Reizner"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "ryu-js",
|
||||
version: "1.0.3",
|
||||
license: "Apache-2.0 OR BSL-1.0",
|
||||
repository: "https://github.com/boa-dev/ryu-js",
|
||||
copyright: ["Copyright (c) David Tolnay, boa-dev"],
|
||||
texts: ["apache-2.0"],
|
||||
},
|
||||
{
|
||||
name: "scopeguard",
|
||||
version: "1.2.0",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/bluss/scopeguard",
|
||||
copyright: ['Copyright (c) 2016-2019 Ulrik Sverdrup "bluss" and scopeguard developers'],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "serde_core",
|
||||
version: "1.0.228",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/serde-rs/serde",
|
||||
copyright: ["Copyright (c) Erick Tryzelaar, David Tolnay"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "serde_json",
|
||||
version: "1.0.150",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/serde-rs/json",
|
||||
copyright: ["Copyright (c) Erick Tryzelaar, David Tolnay"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "serde",
|
||||
version: "1.0.228",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/serde-rs/serde",
|
||||
copyright: ["Copyright (c) Erick Tryzelaar, David Tolnay"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "sha2",
|
||||
version: "0.10.9",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/RustCrypto/hashes",
|
||||
copyright: [
|
||||
"Copyright (c) 2006-2009 Graydon Hoare",
|
||||
"Copyright (c) 2009-2013 Mozilla Foundation",
|
||||
"Copyright (c) 2016 Artyom Pavlov",
|
||||
],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "simd-adler32",
|
||||
version: "0.3.9",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/mcountryman/simd-adler32",
|
||||
copyright: ["Copyright (c) [2021] [Marvin Countryman]"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "skrifa",
|
||||
version: "0.44.0",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/googlefonts/fontations",
|
||||
copyright: ["Copyright (c) 2019 Fontations Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "slab",
|
||||
version: "0.4.12",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/tokio-rs/slab",
|
||||
copyright: ["Copyright (c) 2019 Carl Lerche"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "smallstr",
|
||||
version: "0.3.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/murarth/smallstr",
|
||||
copyright: ["Copyright (c) 2018-2020 Murarth"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "smallvec",
|
||||
version: "1.15.2",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/servo/rust-smallvec",
|
||||
copyright: ["Copyright (c) 2018 The Servo Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "strict-num",
|
||||
version: "0.1.1",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/RazrFalcon/strict-num",
|
||||
copyright: ["Copyright (c) 2022 Yevhenii Reizner"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "thiserror",
|
||||
version: "2.0.18",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/dtolnay/thiserror",
|
||||
copyright: ["Copyright (c) David Tolnay"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "tiff",
|
||||
version: "0.11.3",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/image-rs/image-tiff",
|
||||
copyright: ["Copyright (c) 2018 PistonDevelopers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "tiny-skia-path",
|
||||
version: "0.12.0",
|
||||
license: "BSD-3-Clause",
|
||||
repository: "https://github.com/linebender/tiny-skia/tree/master/path",
|
||||
copyright: [
|
||||
"Copyright (c) 2011 Google Inc. All rights reserved.",
|
||||
"Copyright (c) 2020 Yevhenii Reizner All rights reserved.",
|
||||
],
|
||||
texts: ["bsd-3-clause-2"],
|
||||
},
|
||||
{
|
||||
name: "tiny-skia",
|
||||
version: "0.12.0",
|
||||
license: "BSD-3-Clause",
|
||||
repository: "https://github.com/linebender/tiny-skia",
|
||||
copyright: [
|
||||
"Copyright (c) 2011 Google Inc. All rights reserved.",
|
||||
"Copyright (c) 2020 Yevhenii Reizner All rights reserved.",
|
||||
],
|
||||
texts: ["bsd-3-clause-2"],
|
||||
},
|
||||
{
|
||||
name: "ttf-parser",
|
||||
version: "0.25.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/harfbuzz/ttf-parser",
|
||||
copyright: ["Copyright (c) 2018 Yevhenii Reizner"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "typed-path",
|
||||
version: "0.12.3",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/chipsenkbeil/typed-path",
|
||||
copyright: ["Copyright (c) Chip Senkbeil"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "typenum",
|
||||
version: "1.20.1",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/paholg/typenum",
|
||||
copyright: ["Copyright (c) 2014 Paho Lurie-Gregg"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "unicode-bidi-mirroring",
|
||||
version: "0.4.0",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/RazrFalcon/unicode-bidi-mirroring",
|
||||
copyright: ["Copyright (c) 2020 Yevhenii Reizner"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "unicode-bidi",
|
||||
version: "0.3.18",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/servo/unicode-bidi",
|
||||
copyright: ["Copyright (c) 2015 The Rust Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "unicode-ccc",
|
||||
version: "0.4.0",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/RazrFalcon/unicode-ccc",
|
||||
copyright: ["Copyright (c) 2020 Yevhenii Reizner"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "unicode-ident",
|
||||
version: "1.0.24",
|
||||
license: "(MIT OR Apache-2.0) AND Unicode-3.0",
|
||||
repository: "https://github.com/dtolnay/unicode-ident",
|
||||
copyright: ["Copyright (c) David Tolnay", "Copyright © 1991-2023 Unicode, Inc."],
|
||||
texts: ["mit", "unicode-3.0"],
|
||||
},
|
||||
{
|
||||
name: "unicode-linebreak",
|
||||
version: "0.1.5",
|
||||
license: "Apache-2.0",
|
||||
repository: "https://github.com/axelf4/unicode-linebreak",
|
||||
copyright: ["Copyright (c) Axel Forsman"],
|
||||
texts: ["apache-2.0"],
|
||||
},
|
||||
{
|
||||
name: "unicode-properties",
|
||||
version: "0.1.4",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/unicode-rs/unicode-properties",
|
||||
copyright: ["Copyright (c) 2015 The Rust Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "unicode-script",
|
||||
version: "0.5.8",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/unicode-rs/unicode-script",
|
||||
copyright: ["Copyright (c) 2019 Manish Goregaokar"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "unicode-segmentation",
|
||||
version: "1.13.3",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/unicode-rs/unicode-segmentation",
|
||||
copyright: ["Copyright (c) 2015 The Rust Project Developers"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "wasm-bindgen-shared",
|
||||
version: "0.2.126",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/wasm-bindgen/wasm-bindgen/tree/master/crates/shared",
|
||||
copyright: ["Copyright (c) 2014 Alex Crichton"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "wasm-bindgen",
|
||||
version: "0.2.126",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/wasm-bindgen/wasm-bindgen",
|
||||
copyright: ["Copyright (c) 2014 Alex Crichton"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "weezl",
|
||||
version: "0.1.12",
|
||||
license: "MIT OR Apache-2.0",
|
||||
repository: "https://github.com/image-rs/weezl",
|
||||
copyright: ["Copyright (c) HeroicKatora 2020"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "yrs",
|
||||
version: "0.27.3",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/y-crdt/y-crdt/",
|
||||
copyright: ["Copyright (c) Kevin Jahns, Bartosz Sypytkowski"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "zerocopy",
|
||||
version: "0.8.52",
|
||||
license: "BSD-2-Clause OR Apache-2.0 OR MIT",
|
||||
repository: "https://github.com/google/zerocopy",
|
||||
copyright: ["Copyright 2023 The Fuchsia Authors"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "zip",
|
||||
version: "8.6.0",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/zip-rs/zip2",
|
||||
copyright: ["Copyright (c) 2014 Mathijs van de Nes"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
{
|
||||
name: "zlib-rs",
|
||||
version: "0.6.6",
|
||||
license: "Zlib",
|
||||
repository: "https://github.com/trifectatechfoundation/zlib-rs",
|
||||
copyright: ["(C) 2024 Trifecta Tech Foundation"],
|
||||
texts: ["zlib"],
|
||||
},
|
||||
{
|
||||
name: "zmij",
|
||||
version: "1.0.21",
|
||||
license: "MIT",
|
||||
repository: "https://github.com/dtolnay/zmij",
|
||||
copyright: ["Copyright (c) David Tolnay"],
|
||||
texts: ["mit"],
|
||||
},
|
||||
]
|
||||
|
||||
export const crateTexts = [
|
||||
{
|
||||
id: "mit",
|
||||
license: "MIT",
|
||||
title: "MIT License",
|
||||
text: 'Permission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the "Software"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.',
|
||||
},
|
||||
{
|
||||
id: "mit-2",
|
||||
license: "MIT",
|
||||
title: "MIT License",
|
||||
text: 'Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.',
|
||||
},
|
||||
{
|
||||
id: "mit-3",
|
||||
license: "MIT",
|
||||
title: "MIT License",
|
||||
text: 'rust-lang/libm as a whole is available for use under the MIT license:\n\n------------------------------------------------------------------------------\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n------------------------------------------------------------------------------\n\nAs a contributor, you agree that your code can be used under either the MIT\nlicense or the Apache-2.0 license:\n\n------------------------------------------------------------------------------\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n "License" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n "Licensor" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n "Legal Entity" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n "control" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n "You" (or "Your") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n "Source" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n "Object" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n "Work" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n "Derivative Works" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n "Contribution" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, "submitted"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as "Not a Contribution."\n\n "Contributor" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n
Line truncated
|
||||
},
|
||||
{
|
||||
id: "bsd-2-clause",
|
||||
license: "BSD-2-Clause",
|
||||
title: "BSD 2-Clause License",
|
||||
text: 'All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the\n distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.',
|
||||
},
|
||||
{
|
||||
id: "bsd-3-clause",
|
||||
license: "BSD-3-Clause",
|
||||
title: "BSD 3-Clause License",
|
||||
text: 'Redistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.',
|
||||
},
|
||||
{
|
||||
id: "bsd-3-clause-2",
|
||||
license: "BSD-3-Clause",
|
||||
title: "BSD 3-Clause License",
|
||||
text: 'Redistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n * Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived\n from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.',
|
||||
},
|
||||
{
|
||||
id: "zlib",
|
||||
license: "Zlib",
|
||||
title: "zlib License",
|
||||
text: "This software is provided 'as-is', without any express or implied warranty. In\nno event will the authors be held liable for any damages arising from the use of\nthis software.\n\nPermission is granted to anyone to use this software for any purpose, including\ncommercial applications, and to alter it and redistribute it freely, subject to\nthe following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not claim\n that you wrote the original software. If you use this software in a product,\n an acknowledgment in the product documentation would be appreciated but is\n not required.\n\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n\n3. This notice may not be removed or altered from any source distribution.",
|
||||
},
|
||||
{
|
||||
id: "unicode-3.0",
|
||||
license: "Unicode-3.0",
|
||||
title: "Unicode License v3",
|
||||
text: 'NOTICE TO USER: Carefully read the following legal agreement. BY\nDOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR\nSOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE\nTERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT\nDOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of data files and any associated documentation (the "Data Files") or\nsoftware and any associated documentation (the "Software") to deal in the\nData Files or Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, and/or sell\ncopies of the Data Files or Software, and to permit persons to whom the\nData Files or Software are furnished to do so, provided that either (a)\nthis copyright and permission notice appear with all copies of the Data\nFiles or Software, or (b) this copyright and permission notice appear in\nassociated Documentation.\n\nTHE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY\nKIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF\nTHIRD PARTY RIGHTS.\n\nIN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE\nBE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,\nOR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,\nWHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,\nARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA\nFILES OR SOFTWARE.\n\nExcept as contained in this notice, the name of a copyright holder shall\nnot be used in advertising or otherwise to promote the sale, use or other\ndealings in these Data Files or Software without prior written\nauthorization of the copyright holder.',
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,107 @@
|
||||
import { For, Show } from "solid-js"
|
||||
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup, type DialogProps } from "@opencode/ui/dialog"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
import { ExternalLink } from "@/runtime/platform/external-link"
|
||||
import { fonts } from "./fonts"
|
||||
import { crateNotices, notices } from "./notices"
|
||||
|
||||
const licenseOf = new Map(crateNotices.texts.map((text) => [text.id, text.license]))
|
||||
|
||||
const texts = crateNotices.texts.map((text) => ({
|
||||
...text,
|
||||
// Two versions of one crate (hashbrown) read as one name.
|
||||
crates: [...new Set(crateNotices.crates.filter((crate) => crate.texts.includes(text.id)).map((crate) => crate.name))],
|
||||
}))
|
||||
|
||||
export default function DialogThirdPartyNotices(props: { onCloseAutoFocus?: DialogProps["onCloseAutoFocus"] }) {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<Dialog size="large" onCloseAutoFocus={props.onCloseAutoFocus}>
|
||||
<DialogHeader>
|
||||
<DialogTitleGroup
|
||||
title={language.t("settings.about.notices.title")}
|
||||
description={language.t("settings.about.notices.description")}
|
||||
/>
|
||||
</DialogHeader>
|
||||
<DialogBody class="settings-notices">
|
||||
<For each={notices}>
|
||||
{(entry) => (
|
||||
<section class="settings-notice">
|
||||
<div class="settings-notice-heading">
|
||||
<ExternalLink href={entry.url}>{entry.name}</ExternalLink>
|
||||
<span>{entry.license}</span>
|
||||
</div>
|
||||
<p>{entry.detailKey ? language.t(entry.detailKey) : entry.detail}</p>
|
||||
<Show when={entry.notice}>{(notice) => <pre>{notice()}</pre>}</Show>
|
||||
<details>
|
||||
<summary>{language.t("settings.about.notices.license")}</summary>
|
||||
<pre>{entry.text}</pre>
|
||||
</details>
|
||||
</section>
|
||||
)}
|
||||
</For>
|
||||
<section class="settings-notice settings-crates">
|
||||
<h3 class="settings-notice-heading">{language.t("settings.about.notices.fonts.title")}</h3>
|
||||
<p>{language.t("settings.about.notices.fonts.description")}</p>
|
||||
<ul class="settings-crate-list">
|
||||
<For each={fonts.families}>
|
||||
{(family) => (
|
||||
<li class="settings-crate">
|
||||
<div class="settings-crate-heading">
|
||||
<ExternalLink href={family.url}>{family.name}</ExternalLink>
|
||||
</div>
|
||||
<p>{family.copyright.join("\n")}</p>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
<details>
|
||||
<summary>SIL Open Font License 1.1</summary>
|
||||
<pre>{fonts.license}</pre>
|
||||
</details>
|
||||
</section>
|
||||
<section class="settings-notice settings-crates">
|
||||
<h3 class="settings-notice-heading">{language.t("settings.about.notices.crates.title")}</h3>
|
||||
<p>{language.t("settings.about.notices.crates.description")}</p>
|
||||
<ul class="settings-crate-list">
|
||||
<For each={crateNotices.crates}>
|
||||
{(crate) => {
|
||||
const applied = new Set(crate.texts.map((id) => licenseOf.get(id)))
|
||||
|
||||
return (
|
||||
<li class="settings-crate">
|
||||
<div class="settings-crate-heading">
|
||||
<span>
|
||||
<ExternalLink href={crate.repository}>{crate.name}</ExternalLink> {crate.version}
|
||||
</span>
|
||||
<span>
|
||||
<For each={crate.license.split(/([\s()]+)/)}>
|
||||
{(part) => (applied.has(part) ? <strong>{part}</strong> : part)}
|
||||
</For>
|
||||
</span>
|
||||
</div>
|
||||
<p>{crate.copyright.join("\n")}</p>
|
||||
</li>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
<h4>{language.t("settings.about.notices.crates.licenses")}</h4>
|
||||
<For each={texts}>
|
||||
{(text) => (
|
||||
<details>
|
||||
<summary>
|
||||
{text.title}
|
||||
<span>{language.plural("settings.about.notices.crates.count", text.crates.length)}</span>
|
||||
</summary>
|
||||
<p>{language.rich("settings.about.notices.crates.appliesTo", { names: language.list(text.crates) })}</p>
|
||||
<pre>{text.text}</pre>
|
||||
</details>
|
||||
)}
|
||||
</For>
|
||||
</section>
|
||||
</DialogBody>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import license from "./fonts/OFL-1.1.txt?raw"
|
||||
|
||||
/**
|
||||
* The font families the Office previews ship, each with the copyright lines its own license file carries. Every
|
||||
* family is under the SIL Open Font License 1.1, so the license text is shown once.
|
||||
*/
|
||||
export const fonts = {
|
||||
license,
|
||||
families: [
|
||||
{
|
||||
name: "Carlito",
|
||||
url: "https://github.com/googlefonts/carlito",
|
||||
copyright: [
|
||||
'Copyright 2013 The Carlito Project Authors (https://github.com/googlefonts/carlito), with Reserved Font Name "Carlito"',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Caladea",
|
||||
url: "https://github.com/huertatipografica/Caladea",
|
||||
copyright: ["Copyright 2012 The Caladea Project Authors (https://github.com/huertatipografica/Caladea)"],
|
||||
},
|
||||
{
|
||||
name: "Liberation Sans, Liberation Serif, Liberation Mono",
|
||||
url: "https://github.com/liberationfonts/liberation-fonts",
|
||||
copyright: [
|
||||
"Digitized data copyright (c) 2010 Google Corporation",
|
||||
"with Reserved Font Arimo, Tinos and Cousine.",
|
||||
"Copyright (c) 2012 Red Hat, Inc.",
|
||||
"with Reserved Font Name Liberation.",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Gelasio",
|
||||
url: "https://github.com/SorkinType/Gelasio",
|
||||
copyright: ["Copyright 2022 The Gelasio Project Authors (https://github.com/SorkinType/Gelasio)"],
|
||||
},
|
||||
{
|
||||
name: "Comic Relief",
|
||||
url: "https://github.com/loudifier/Comic-Relief",
|
||||
copyright: ["Copyright 2013 The Comic Relief Project Authors (https://github.com/loudifier/Comic-Relief)"],
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
url: "https://github.com/rsms/inter",
|
||||
copyright: ["Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)"],
|
||||
},
|
||||
{
|
||||
name: "Roboto",
|
||||
url: "https://github.com/googlefonts/roboto-classic",
|
||||
copyright: ["Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)"],
|
||||
},
|
||||
{
|
||||
name: "Source Sans 3",
|
||||
url: "https://github.com/adobe-fonts/source-sans",
|
||||
copyright: [
|
||||
"Copyright 2010-2020 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "DM Sans",
|
||||
url: "https://github.com/googlefonts/dm-fonts",
|
||||
copyright: ["Copyright 2014 The DM Sans Project Authors (https://github.com/googlefonts/dm-fonts)"],
|
||||
},
|
||||
{
|
||||
name: "DM Serif Display",
|
||||
url: "https://github.com/googlefonts/dm-fonts",
|
||||
copyright: [
|
||||
"Copyright 2014-2018 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. Copyright 2019 Google LLC.",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Open Sans",
|
||||
url: "https://github.com/googlefonts/opensans",
|
||||
copyright: ["Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans)"],
|
||||
},
|
||||
{
|
||||
name: "Montserrat",
|
||||
url: "https://github.com/JulietaUla/Montserrat",
|
||||
copyright: ["Copyright 2024 The Montserrat.Git Project Authors (https://github.com/JulietaUla/Montserrat.git)"],
|
||||
},
|
||||
{
|
||||
name: "Poppins",
|
||||
url: "https://github.com/itfoundry/Poppins",
|
||||
copyright: ["Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)"],
|
||||
},
|
||||
{
|
||||
name: "Oswald",
|
||||
url: "https://github.com/googlefonts/OswaldFont",
|
||||
copyright: ["Copyright 2016 The Oswald Project Authors (https://github.com/googlefonts/OswaldFont)"],
|
||||
},
|
||||
{
|
||||
name: "Heebo",
|
||||
url: "https://github.com/OdedEzer/heebo",
|
||||
copyright: ["Copyright 2014 The Heebo Project Authors (https://github.com/OdedEzer/heebo)"],
|
||||
},
|
||||
{
|
||||
name: "Noto Sans Arabic, Noto Naskh Arabic",
|
||||
url: "https://github.com/notofonts/arabic",
|
||||
copyright: ["Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)"],
|
||||
},
|
||||
{
|
||||
name: "Noto Sans Hebrew",
|
||||
url: "https://github.com/notofonts/hebrew",
|
||||
copyright: ["Copyright 2022 The Noto Project Authors (https://github.com/notofonts/hebrew)"],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,37 @@
|
||||
import apache from "./apache-2.0.txt?raw"
|
||||
import betterOfficeNotice from "./betteroffice-notice.txt?raw"
|
||||
import { crates, crateTexts } from "./crates"
|
||||
|
||||
/**
|
||||
* Third-party software that ships inside the app, with the attribution its license asks redistributions to carry.
|
||||
* Notices and license texts are reproduced verbatim from each project, so they are not translated.
|
||||
*/
|
||||
export const notices = [
|
||||
{
|
||||
name: "BetterOffice",
|
||||
detail: "@betteroffice/docx, @betteroffice/xlsx, @betteroffice/pptx, @betteroffice/fonts",
|
||||
detailKey: undefined,
|
||||
url: "https://github.com/openooxml/betteroffice",
|
||||
license: "Apache License 2.0",
|
||||
notice: `${betterOfficeNotice.trim()}\nCopyright 2026 The OpenOOXML contributors`,
|
||||
text: apache,
|
||||
},
|
||||
{
|
||||
name: "eigenpal docx editor",
|
||||
detail: undefined,
|
||||
detailKey: "settings.about.notices.eigenpal" as const,
|
||||
url: "https://github.com/eigenpal/docx-editor",
|
||||
license: "Apache License 2.0",
|
||||
notice: "Copyright 2026 EigenPal Inc.",
|
||||
text: apache,
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* The Rust crates the BetterOffice wasm engines compile in, generated from the published Cargo.lock and crate
|
||||
* tarballs. Each crate keeps its own copyright lines; license texts are stored once and referenced by id.
|
||||
*/
|
||||
export const crateNotices = {
|
||||
crates,
|
||||
texts: [...crateTexts, { id: "apache-2.0", license: "Apache-2.0", title: "Apache License 2.0", text: apache }],
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Badge } from "@opencode/ui/badge"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { useDialog } from "@opencode/ui/context/dialog"
|
||||
import { createMemo, Show, type Component } from "solid-js"
|
||||
import { ServerRowMenu } from "@/servers/registry/row-menu"
|
||||
@@ -29,6 +30,10 @@ export const SettingsServerGeneral: Component<{
|
||||
const edit = (server: ServerConnection.Http) =>
|
||||
void dialog.push(() => <DialogServer mode="edit" server={server} onSave={props.onServerChange} />)
|
||||
|
||||
// An HTTP server that rejects its saved credentials signs in again through the edit dialog.
|
||||
const signedOut = (server: ServerConnection.Any) =>
|
||||
server.type === "http" && health()?.unauthorized ? server : undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="settings-tab-header">
|
||||
@@ -59,7 +64,7 @@ export const SettingsServerGeneral: Component<{
|
||||
{(server) => (
|
||||
<div class="settings-servers-row">
|
||||
<div class="settings-servers-lead">
|
||||
<ServerHealthIndicator health={health()} />
|
||||
<ServerHealthIndicator health={health()} authenticationRequired={!!health()?.unauthorized} />
|
||||
<div class="settings-servers-copy">
|
||||
<bdi class="settings-servers-name" dir="auto">
|
||||
{serverName(server()) || props.entry.key}
|
||||
@@ -70,6 +75,13 @@ export const SettingsServerGeneral: Component<{
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-servers-actions">
|
||||
<Show when={signedOut(server())}>
|
||||
{(http) => (
|
||||
<Button size="small" variant="neutral" onClick={() => edit(http())}>
|
||||
{language.t("server.action.authenticate")}
|
||||
</Button>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={controller.defaults.available() && controller.defaults.key() === props.entry.key}>
|
||||
<Badge>{language.t("dialog.server.status.default")}</Badge>
|
||||
</Show>
|
||||
|
||||
@@ -289,6 +289,152 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.settings-about-link {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
.settings-about-link:hover,
|
||||
.settings-about-link:focus-visible {
|
||||
color: var(--v2-text-text-base);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.settings-notices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0 20px 20px;
|
||||
color: var(--v2-text-text-muted);
|
||||
font-size: 13px;
|
||||
line-height: var(--line-height-base);
|
||||
}
|
||||
|
||||
.settings-notice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.settings-notice p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.settings-notice-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: var(--v2-text-text-base);
|
||||
font-weight: 540;
|
||||
}
|
||||
|
||||
.settings-notice-heading span {
|
||||
color: var(--v2-text-text-faint);
|
||||
font-weight: 440;
|
||||
}
|
||||
|
||||
.settings-notice-heading a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.settings-notice summary {
|
||||
cursor: pointer;
|
||||
color: var(--v2-text-text-faint);
|
||||
}
|
||||
|
||||
.settings-notice pre {
|
||||
margin: 0;
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 11px;
|
||||
line-height: var(--line-height-compact);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.settings-notice details pre {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--v2-background-bg-layer-02);
|
||||
}
|
||||
|
||||
.settings-crates {
|
||||
padding-top: 20px;
|
||||
border-top: 0.5px solid var(--v2-border-border-base);
|
||||
}
|
||||
|
||||
.settings-crates h3,
|
||||
.settings-crates h4 {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: var(--line-height-base);
|
||||
}
|
||||
|
||||
.settings-crates h4 {
|
||||
margin-top: 8px;
|
||||
color: var(--v2-text-text-base);
|
||||
font-weight: 540;
|
||||
}
|
||||
|
||||
.settings-crate-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 8px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.settings-crate {
|
||||
line-height: var(--line-height-compact);
|
||||
}
|
||||
|
||||
.settings-crate-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.settings-crate-heading > span:first-child {
|
||||
color: var(--v2-text-text-faint);
|
||||
}
|
||||
|
||||
.settings-crate-heading a {
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
|
||||
.settings-crate-heading > span:last-child {
|
||||
flex-shrink: 0;
|
||||
color: var(--v2-text-text-faint);
|
||||
}
|
||||
|
||||
.settings-crate-heading strong {
|
||||
color: var(--v2-text-text-base);
|
||||
font-weight: 540;
|
||||
}
|
||||
|
||||
.settings-crate p {
|
||||
color: var(--v2-text-text-faint);
|
||||
font-size: 12px;
|
||||
white-space: pre-line;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.settings-crates summary span {
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
|
||||
.settings-crates details > p {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.settings-back {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
@@ -1809,7 +1955,8 @@
|
||||
.settings-server-dialog-error {
|
||||
font-size: 11px;
|
||||
font-weight: 440;
|
||||
line-height: 1;
|
||||
/* Pairing errors are full sentences that wrap. */
|
||||
line-height: var(--line-height-compact);
|
||||
color: var(--v2-state-fg-danger);
|
||||
}
|
||||
|
||||
@@ -1819,6 +1966,25 @@
|
||||
color: var(--v2-text-text-muted);
|
||||
}
|
||||
|
||||
.settings-server-dialog-notice {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--v2-border-border-base);
|
||||
border-radius: 6px;
|
||||
background: var(--v2-background-bg-layer-01);
|
||||
font-size: 13px;
|
||||
line-height: var(--line-height-base);
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
|
||||
.settings-server-dialog-notice [data-slot="icon-svg"] {
|
||||
flex: none;
|
||||
margin-block: 2px;
|
||||
color: var(--v2-icon-icon-muted);
|
||||
}
|
||||
|
||||
.settings-extensions-tabs[data-component="tabs-v2"][data-variant="pill"] > [data-slot="tabs-v2-list"] {
|
||||
width: min(280px, 100%);
|
||||
}
|
||||
|
||||
Loaded 100 of 255 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user