diff --git a/packages/app/component-tests/extension-host.fixture.tsx b/packages/app/component-tests/extension-host.fixture.tsx
index 54cf343ce604..c20d81e3338e 100644
--- a/packages/app/component-tests/extension-host.fixture.tsx
+++ b/packages/app/component-tests/extension-host.fixture.tsx
@@ -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 {
keybind: () => [],
matches: () => false,
servers: () => ["local"],
+ server: () => undefined,
...overrides,
}
}
diff --git a/packages/app/e2e/regression/home.spec.ts b/packages/app/e2e/regression/home.spec.ts
index d80d5e5dd09d..87a8471e9153 100644
--- a/packages/app/e2e/regression/home.spec.ts
+++ b/packages/app/e2e/regression/home.spec.ts
@@ -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).groupBy
+ // SAFETY: as above, for the real global `Promise`.
delete (Promise as Partial).withResolvers
+ // SAFETY: as above, for the real global `Promise`.
delete (Promise as Partial).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 =
diff --git a/packages/app/e2e/regression/settings.spec.ts b/packages/app/e2e/regression/settings.spec.ts
index 6dd381744ad6..01ac1fc08f22 100644
--- a/packages/app/e2e/regression/settings.spec.ts
+++ b/packages/app/e2e/regression/settings.spec.ts
@@ -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(), refreshes: new Array() }
+
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"]')
diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts
index 31dc6c260775..4b408bb19885 100644
--- a/packages/app/e2e/utils/mock-server.ts
+++ b/packages/app/e2e/utils/mock-server.ts
@@ -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
+ // 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(
diff --git a/packages/app/src/home/projects/controller.tsx b/packages/app/src/home/projects/controller.tsx
index 8ff6df4eb4ff..174aa73aaba1 100644
--- a/packages/app/src/home/projects/controller.tsx
+++ b/packages/app/src/home/projects/controller.tsx
@@ -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(() => 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(() => )
- })
- },
+ 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)
diff --git a/packages/app/src/home/projects/view.tsx b/packages/app/src/home/projects/view.tsx
index f842e4c754a8..3bea74367b6b 100644
--- a/packages/app/src/home/projects/view.tsx
+++ b/packages/app/src/home/projects/view.tsx
@@ -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)
diff --git a/packages/app/src/runtime/extension/host-apis.tsx b/packages/app/src/runtime/extension/host-apis.tsx
index 0fc200672146..b127a228f8e3 100644
--- a/packages/app/src/runtime/extension/host-apis.tsx
+++ b/packages/app/src/runtime/extension/host-apis.tsx
@@ -69,6 +69,7 @@ type Attached = {
keybind: (command: string) => readonly string[]
matches: (command: string, event: KeyboardEvent) => boolean
servers: Accessor
+ 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 | undefined
+
+ const live = () => {
+ started ??= runWithOwner(owner, () =>
+ createMemo(
+ (previous) => global.serverCtx(key) ?? previous,
+ global.ensureServerCtx(connection(id) ?? conn),
+ ),
+ )!
+
+ return started()
+ }
- const live = runWithOwner(owner, () =>
- createMemo((previous) => global.serverCtx(key) ?? previous, global.ensureServerCtx(conn)),
- )!
+ // 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[0]),
layout: {
diff --git a/packages/app/src/runtime/i18n/am.ts b/packages/app/src/runtime/i18n/am.ts
index 6e54890621fa..66fe0e9ae3bf 100644
--- a/packages/app/src/runtime/i18n/am.ts
+++ b/packages/app/src/runtime/i18n/am.ts
@@ -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": "አስወግድ",
diff --git a/packages/app/src/runtime/i18n/ar.ts b/packages/app/src/runtime/i18n/ar.ts
index 2855f90dedfc..d57ccc4577c3 100644
--- a/packages/app/src/runtime/i18n/ar.ts
+++ b/packages/app/src/runtime/i18n/ar.ts
@@ -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": "إزالة",
diff --git a/packages/app/src/runtime/i18n/az.ts b/packages/app/src/runtime/i18n/az.ts
index 33415dfd1330..8e7f19b9c8f7 100644
--- a/packages/app/src/runtime/i18n/az.ts
+++ b/packages/app/src/runtime/i18n/az.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/bg.ts b/packages/app/src/runtime/i18n/bg.ts
index 8a3350f97785..544a30608729 100644
--- a/packages/app/src/runtime/i18n/bg.ts
+++ b/packages/app/src/runtime/i18n/bg.ts
@@ -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":
"Камерата не можа да бъде отворена. Разрешете достъпа до нея или въведете ръчно данните за връзка.",
diff --git a/packages/app/src/runtime/i18n/bn.ts b/packages/app/src/runtime/i18n/bn.ts
index f23305e8a40e..34acadc29e78 100644
--- a/packages/app/src/runtime/i18n/bn.ts
+++ b/packages/app/src/runtime/i18n/bn.ts
@@ -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":
"ক্যামেরা খুলতে পারিনি। ক্যামেরা অ্যাক্সেসের অনুমতি দিন বা ম্যানুয়ালি আপনার সংযোগের বিবরণ লিখুন।",
diff --git a/packages/app/src/runtime/i18n/br.ts b/packages/app/src/runtime/i18n/br.ts
index b96a614136e1..74adfa32342e 100644
--- a/packages/app/src/runtime/i18n/br.ts
+++ b/packages/app/src/runtime/i18n/br.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/bs.ts b/packages/app/src/runtime/i18n/bs.ts
index 5a683b9f9635..7d718ed904e0 100644
--- a/packages/app/src/runtime/i18n/bs.ts
+++ b/packages/app/src/runtime/i18n/bs.ts
@@ -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.",
diff --git a/packages/app/src/runtime/i18n/ca.ts b/packages/app/src/runtime/i18n/ca.ts
index 71da822eede9..a88502681aea 100644
--- a/packages/app/src/runtime/i18n/ca.ts
+++ b/packages/app/src/runtime/i18n/ca.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/cs.ts b/packages/app/src/runtime/i18n/cs.ts
index 60b096c4c60f..e073902c1200 100644
--- a/packages/app/src/runtime/i18n/cs.ts
+++ b/packages/app/src/runtime/i18n/cs.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/da.ts b/packages/app/src/runtime/i18n/da.ts
index 38823439434b..f6a717d27b27 100644
--- a/packages/app/src/runtime/i18n/da.ts
+++ b/packages/app/src/runtime/i18n/da.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/de.ts b/packages/app/src/runtime/i18n/de.ts
index feda2a36e69e..ec5725c67b37 100644
--- a/packages/app/src/runtime/i18n/de.ts
+++ b/packages/app/src/runtime/i18n/de.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/dv.ts b/packages/app/src/runtime/i18n/dv.ts
index 68c49efbadca..9d5151aa764c 100644
--- a/packages/app/src/runtime/i18n/dv.ts
+++ b/packages/app/src/runtime/i18n/dv.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/dz.ts b/packages/app/src/runtime/i18n/dz.ts
index 371cadb86dc1..7ea467a518d7 100644
--- a/packages/app/src/runtime/i18n/dz.ts
+++ b/packages/app/src/runtime/i18n/dz.ts
@@ -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":
"པར་ཆས་ཁ་ཕྱེ་མ་ཚུགས། པར་ཆས་འཛུལ་སྤྱོད་འབད་བཅུགཔ་ ཡང་ན་ ཁྱོད་རའི་མཐུད་ལམ་རྒྱས་བཤད་ཚུ་ལག་ཐོག་ལས་བཙུགས།",
diff --git a/packages/app/src/runtime/i18n/el.ts b/packages/app/src/runtime/i18n/el.ts
index e444f5654f68..4f3e66bb5710 100644
--- a/packages/app/src/runtime/i18n/el.ts
+++ b/packages/app/src/runtime/i18n/el.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts
index 5240800d4b28..3baa0793ac03 100644
--- a/packages/app/src/runtime/i18n/en.ts
+++ b/packages/app/src/runtime/i18n/en.ts
@@ -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",
diff --git a/packages/app/src/runtime/i18n/es.ts b/packages/app/src/runtime/i18n/es.ts
index 96cb0ddb4225..3a89547558a2 100644
--- a/packages/app/src/runtime/i18n/es.ts
+++ b/packages/app/src/runtime/i18n/es.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/et.ts b/packages/app/src/runtime/i18n/et.ts
index d1f90bd562ac..1ff9e422ca68 100644
--- a/packages/app/src/runtime/i18n/et.ts
+++ b/packages/app/src/runtime/i18n/et.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/fa.ts b/packages/app/src/runtime/i18n/fa.ts
index 4231c182f4f8..8c905676e3b8 100644
--- a/packages/app/src/runtime/i18n/fa.ts
+++ b/packages/app/src/runtime/i18n/fa.ts
@@ -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":
"باز کردن دوربین ممکن نشد. دسترسی دوربین را مجاز کنید یا جزئیات اتصال را دستی وارد کنید.",
diff --git a/packages/app/src/runtime/i18n/fi.ts b/packages/app/src/runtime/i18n/fi.ts
index 11c55c7396d7..1eac14803466 100644
--- a/packages/app/src/runtime/i18n/fi.ts
+++ b/packages/app/src/runtime/i18n/fi.ts
@@ -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",
diff --git a/packages/app/src/runtime/i18n/fo.ts b/packages/app/src/runtime/i18n/fo.ts
index 4184ead3bf12..b7f5bea545f7 100644
--- a/packages/app/src/runtime/i18n/fo.ts
+++ b/packages/app/src/runtime/i18n/fo.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/fr.ts b/packages/app/src/runtime/i18n/fr.ts
index be343f2d6178..e55c7e44d0c0 100644
--- a/packages/app/src/runtime/i18n/fr.ts
+++ b/packages/app/src/runtime/i18n/fr.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/he.ts b/packages/app/src/runtime/i18n/he.ts
index 3279d11b2674..0a51289d60e0 100644
--- a/packages/app/src/runtime/i18n/he.ts
+++ b/packages/app/src/runtime/i18n/he.ts
@@ -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": "הסרה",
diff --git a/packages/app/src/runtime/i18n/hi.ts b/packages/app/src/runtime/i18n/hi.ts
index 13bba1c0ba2e..b5b97397120c 100644
--- a/packages/app/src/runtime/i18n/hi.ts
+++ b/packages/app/src/runtime/i18n/hi.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/hr.ts b/packages/app/src/runtime/i18n/hr.ts
index e29b6903f7a0..ceffeff9d3d4 100644
--- a/packages/app/src/runtime/i18n/hr.ts
+++ b/packages/app/src/runtime/i18n/hr.ts
@@ -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.",
diff --git a/packages/app/src/runtime/i18n/hu.ts b/packages/app/src/runtime/i18n/hu.ts
index d0738d9ee7e2..bfd0b89ce95a 100644
--- a/packages/app/src/runtime/i18n/hu.ts
+++ b/packages/app/src/runtime/i18n/hu.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/hy.ts b/packages/app/src/runtime/i18n/hy.ts
index 56b00bcba73d..2777a8a5eb8c 100644
--- a/packages/app/src/runtime/i18n/hy.ts
+++ b/packages/app/src/runtime/i18n/hy.ts
@@ -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":
"Չկարողացա բացել տեսախցիկը։ Թույլատրել տեսախցիկի մուտքը կամ ձեռքով ներմուծել ձեր կապի տվյալները։",
diff --git a/packages/app/src/runtime/i18n/id.ts b/packages/app/src/runtime/i18n/id.ts
index 5ff3e70dc12e..fad4dc7d835d 100644
--- a/packages/app/src/runtime/i18n/id.ts
+++ b/packages/app/src/runtime/i18n/id.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/is.ts b/packages/app/src/runtime/i18n/is.ts
index fa022618a527..31f28f56cc7c 100644
--- a/packages/app/src/runtime/i18n/is.ts
+++ b/packages/app/src/runtime/i18n/is.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/it.ts b/packages/app/src/runtime/i18n/it.ts
index 584e7246a158..103707404f0c 100644
--- a/packages/app/src/runtime/i18n/it.ts
+++ b/packages/app/src/runtime/i18n/it.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/ja.ts b/packages/app/src/runtime/i18n/ja.ts
index a98c01ebe622..d53d9f6f7a69 100644
--- a/packages/app/src/runtime/i18n/ja.ts
+++ b/packages/app/src/runtime/i18n/ja.ts
@@ -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":
"カメラを開くことができません。カメラへのアクセスを許可するか、接続情報を手動で入力してください。",
diff --git a/packages/app/src/runtime/i18n/ka.ts b/packages/app/src/runtime/i18n/ka.ts
index aca86be053e6..124f5f6763cc 100644
--- a/packages/app/src/runtime/i18n/ka.ts
+++ b/packages/app/src/runtime/i18n/ka.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/km.ts b/packages/app/src/runtime/i18n/km.ts
index fb6e9c82bcc2..3209898dce75 100644
--- a/packages/app/src/runtime/i18n/km.ts
+++ b/packages/app/src/runtime/i18n/km.ts
@@ -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": "មិនអាចបើកកាមេរ៉ាបាន។ អនុញ្ញាតឱ្យចូលដំណើរការកាមេរ៉ា ឬបញ្ចូលព័ត៌មានការភ្ជាប់ដោយដៃ។",
diff --git a/packages/app/src/runtime/i18n/ko.ts b/packages/app/src/runtime/i18n/ko.ts
index 19e4e32949a0..9f52abdd9e77 100644
--- a/packages/app/src/runtime/i18n/ko.ts
+++ b/packages/app/src/runtime/i18n/ko.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/lo.ts b/packages/app/src/runtime/i18n/lo.ts
index 6d25b2cf56e9..105f90c60e2c 100644
--- a/packages/app/src/runtime/i18n/lo.ts
+++ b/packages/app/src/runtime/i18n/lo.ts
@@ -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": "ລຶບ",
diff --git a/packages/app/src/runtime/i18n/lt.ts b/packages/app/src/runtime/i18n/lt.ts
index 5743d2bf8225..2daa18deb2b8 100644
--- a/packages/app/src/runtime/i18n/lt.ts
+++ b/packages/app/src/runtime/i18n/lt.ts
@@ -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.",
diff --git a/packages/app/src/runtime/i18n/lv.ts b/packages/app/src/runtime/i18n/lv.ts
index bc218a856a5f..b9d3a5cdfb09 100644
--- a/packages/app/src/runtime/i18n/lv.ts
+++ b/packages/app/src/runtime/i18n/lv.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/mk.ts b/packages/app/src/runtime/i18n/mk.ts
index cd1444b4b1b7..f975f908519d 100644
--- a/packages/app/src/runtime/i18n/mk.ts
+++ b/packages/app/src/runtime/i18n/mk.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/mn.ts b/packages/app/src/runtime/i18n/mn.ts
index f1c69e656058..b2d5b24eacd5 100644
--- a/packages/app/src/runtime/i18n/mn.ts
+++ b/packages/app/src/runtime/i18n/mn.ts
@@ -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":
"Камерыг нээж чадсангүй. Камер ашиглахыг зөвшөөрөх эсвэл холболтын дэлгэрэнгүй мэдээллээ гараар оруулна уу.",
diff --git a/packages/app/src/runtime/i18n/ms.ts b/packages/app/src/runtime/i18n/ms.ts
index 737595fa7fda..43d6ef5731ce 100644
--- a/packages/app/src/runtime/i18n/ms.ts
+++ b/packages/app/src/runtime/i18n/ms.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/my.ts b/packages/app/src/runtime/i18n/my.ts
index 40dbd9da41a5..c20dc963822c 100644
--- a/packages/app/src/runtime/i18n/my.ts
+++ b/packages/app/src/runtime/i18n/my.ts
@@ -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":
"ကင်မရာဖွင့်၍ မရနိုင်ပါ။ ကင်မရာခွင့်ပြုချက်ပေးပါ သို့မဟုတ် ချိတ်ဆက်မှုအချက်အလက်များကို လက်ဖြင့်ထည့်ပါ။",
diff --git a/packages/app/src/runtime/i18n/ne.ts b/packages/app/src/runtime/i18n/ne.ts
index a4b60487ba84..5344d7f48f6d 100644
--- a/packages/app/src/runtime/i18n/ne.ts
+++ b/packages/app/src/runtime/i18n/ne.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/nl.ts b/packages/app/src/runtime/i18n/nl.ts
index e51495e12f18..c8171f7c065b 100644
--- a/packages/app/src/runtime/i18n/nl.ts
+++ b/packages/app/src/runtime/i18n/nl.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/no.ts b/packages/app/src/runtime/i18n/no.ts
index e97912845d2f..a838b12c7a23 100644
--- a/packages/app/src/runtime/i18n/no.ts
+++ b/packages/app/src/runtime/i18n/no.ts
@@ -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.",
diff --git a/packages/app/src/runtime/i18n/pa.ts b/packages/app/src/runtime/i18n/pa.ts
index 9c2c557fbfbe..2ad4d334ef5d 100644
--- a/packages/app/src/runtime/i18n/pa.ts
+++ b/packages/app/src/runtime/i18n/pa.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/pl.ts b/packages/app/src/runtime/i18n/pl.ts
index 700f0dc32908..6fcf1cf6fd30 100644
--- a/packages/app/src/runtime/i18n/pl.ts
+++ b/packages/app/src/runtime/i18n/pl.ts
@@ -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.",
diff --git a/packages/app/src/runtime/i18n/ro.ts b/packages/app/src/runtime/i18n/ro.ts
index 9145f31e760b..fe1f5b35430c 100644
--- a/packages/app/src/runtime/i18n/ro.ts
+++ b/packages/app/src/runtime/i18n/ro.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/ru.ts b/packages/app/src/runtime/i18n/ru.ts
index e3528cc51496..4934d721f12b 100644
--- a/packages/app/src/runtime/i18n/ru.ts
+++ b/packages/app/src/runtime/i18n/ru.ts
@@ -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":
"Не удалось открыть камеру. Разрешите доступ к камере или введите данные подключения вручную.",
diff --git a/packages/app/src/runtime/i18n/si.ts b/packages/app/src/runtime/i18n/si.ts
index d5131472a18c..f22a937131ea 100644
--- a/packages/app/src/runtime/i18n/si.ts
+++ b/packages/app/src/runtime/i18n/si.ts
@@ -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":
"කැමරාව විවෘත කිරීමට නොහැකි විය. කැමරා ප්රවේශයට ඉඩ දෙන්න හෝ ඔබගේ සම්බන්ධතා විස්තර හස්තීයව ඇතුලත් කරන්න.",
diff --git a/packages/app/src/runtime/i18n/sk.ts b/packages/app/src/runtime/i18n/sk.ts
index 36b223dd7bd0..0929a89d6332 100644
--- a/packages/app/src/runtime/i18n/sk.ts
+++ b/packages/app/src/runtime/i18n/sk.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/sl.ts b/packages/app/src/runtime/i18n/sl.ts
index 819bedd4073c..7bfbe8c2e268 100644
--- a/packages/app/src/runtime/i18n/sl.ts
+++ b/packages/app/src/runtime/i18n/sl.ts
@@ -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.",
diff --git a/packages/app/src/runtime/i18n/sq.ts b/packages/app/src/runtime/i18n/sq.ts
index 9526d8b9d10e..653cccd48908 100644
--- a/packages/app/src/runtime/i18n/sq.ts
+++ b/packages/app/src/runtime/i18n/sq.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/sr.ts b/packages/app/src/runtime/i18n/sr.ts
index a7780c477776..9cbb8da3d42c 100644
--- a/packages/app/src/runtime/i18n/sr.ts
+++ b/packages/app/src/runtime/i18n/sr.ts
@@ -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":
"Није могуће отворити камеру. Дозволите приступ камери или унесите податке за повезивање ручно.",
diff --git a/packages/app/src/runtime/i18n/sv.ts b/packages/app/src/runtime/i18n/sv.ts
index 748e3df67671..d3ffbd7eb8fb 100644
--- a/packages/app/src/runtime/i18n/sv.ts
+++ b/packages/app/src/runtime/i18n/sv.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/tg.ts b/packages/app/src/runtime/i18n/tg.ts
index 08332547ebe0..5a0b2b8d4b01 100644
--- a/packages/app/src/runtime/i18n/tg.ts
+++ b/packages/app/src/runtime/i18n/tg.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/th.ts b/packages/app/src/runtime/i18n/th.ts
index 5f2484a48e35..49d1e5944f54 100644
--- a/packages/app/src/runtime/i18n/th.ts
+++ b/packages/app/src/runtime/i18n/th.ts
@@ -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": "ลบ",
diff --git a/packages/app/src/runtime/i18n/tk.ts b/packages/app/src/runtime/i18n/tk.ts
index 0d0dd587b58f..38f22167ebf7 100644
--- a/packages/app/src/runtime/i18n/tk.ts
+++ b/packages/app/src/runtime/i18n/tk.ts
@@ -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ň.",
diff --git a/packages/app/src/runtime/i18n/tr.ts b/packages/app/src/runtime/i18n/tr.ts
index 9f9a0e5ca614..4dd3bf315303 100644
--- a/packages/app/src/runtime/i18n/tr.ts
+++ b/packages/app/src/runtime/i18n/tr.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/uk.ts b/packages/app/src/runtime/i18n/uk.ts
index 5bd7ac1a3323..990ec81efaad 100644
--- a/packages/app/src/runtime/i18n/uk.ts
+++ b/packages/app/src/runtime/i18n/uk.ts
@@ -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":
"Не вдалося відкрити камеру. Дозвольте доступ до камери або введіть дані підключення вручну.",
diff --git a/packages/app/src/runtime/i18n/ur.ts b/packages/app/src/runtime/i18n/ur.ts
index c4f3adf8821c..095c8a2e157d 100644
--- a/packages/app/src/runtime/i18n/ur.ts
+++ b/packages/app/src/runtime/i18n/ur.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/uz.ts b/packages/app/src/runtime/i18n/uz.ts
index 29702d28589a..78565c04deb1 100644
--- a/packages/app/src/runtime/i18n/uz.ts
+++ b/packages/app/src/runtime/i18n/uz.ts
@@ -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":
diff --git a/packages/app/src/runtime/i18n/vi.ts b/packages/app/src/runtime/i18n/vi.ts
index cc5a9dba4d79..db5ac5d4c519 100644
--- a/packages/app/src/runtime/i18n/vi.ts
+++ b/packages/app/src/runtime/i18n/vi.ts
@@ -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.",
diff --git a/packages/app/src/runtime/i18n/zh.ts b/packages/app/src/runtime/i18n/zh.ts
index c0adf59fc94a..0889beb675e0 100644
--- a/packages/app/src/runtime/i18n/zh.ts
+++ b/packages/app/src/runtime/i18n/zh.ts
@@ -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": "移除",
diff --git a/packages/app/src/runtime/i18n/zht.ts b/packages/app/src/runtime/i18n/zht.ts
index edf0c36c03f7..055e5b606d35 100644
--- a/packages/app/src/runtime/i18n/zht.ts
+++ b/packages/app/src/runtime/i18n/zht.ts
@@ -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": "移除",
diff --git a/packages/app/src/runtime/server/runtime.tsx b/packages/app/src/runtime/server/runtime.tsx
index 74697c040d28..13e686e8c334 100644
--- a/packages/app/src/runtime/server/runtime.tsx
+++ b/packages/app/src/runtime/server/runtime.tsx
@@ -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>()
const serverCtxDisposers = new Map 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()
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 ?? ""}` : ""
+}
diff --git a/packages/app/src/servers/connect/camera.ts b/packages/app/src/servers/connect/camera.ts
index bc122bb8a5d4..1f3adf17a190 100644
--- a/packages/app/src/servers/connect/camera.ts
+++ b/packages/app/src/servers/connect/camera.ts
@@ -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"
+}
diff --git a/packages/app/src/servers/connect/dialog.tsx b/packages/app/src/servers/connect/dialog.tsx
index f39f09ca469b..b4573f410b75 100644
--- a/packages/app/src/servers/connect/dialog.tsx
+++ b/packages/app/src/servers/connect/dialog.tsx
@@ -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 = () => (
+
+
+ {form.state.error()}
+
+
+ )
+
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}
/>
}
>
+ )
+}
diff --git a/packages/app/src/servers/connect/pairing.test.ts b/packages/app/src/servers/connect/pairing.test.ts
index 978af88ba535..f4318640120a 100644
--- a/packages/app/src/servers/connect/pairing.test.ts
+++ b/packages/app/src/servers/connect/pairing.test.ts
@@ -1,5 +1,5 @@
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", () => {
@@ -35,4 +35,23 @@ describe("pairing link", () => {
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)
+ })
})
diff --git a/packages/app/src/servers/connect/pairing.ts b/packages/app/src/servers/connect/pairing.ts
index f86a1dc7bfd1..6db2ec009106 100644
--- a/packages/app/src/servers/connect/pairing.ts
+++ b/packages/app/src/servers/connect/pairing.ts
@@ -1,5 +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) {
@@ -49,15 +50,83 @@ export function pairingLink(value: string) {
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#` or `/connect?data=`.
+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 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; code: string }) {
- return Promise.any(
- 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 })),
- ),
- ).catch(() => undefined)
+ 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(
+ (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,
+ pairing: Pairing,
+ answered: Promise | undefined,
+): Promise {
+ const first = urls[0]
+
+ if (!first || first === pairing.url || !answered) return pairing
+
+ const reachable = await Promise.race([
+ answered,
+ new Promise((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
}
diff --git a/packages/app/src/servers/connect/redeem.ts b/packages/app/src/servers/connect/redeem.ts
new file mode 100644
index 000000000000..c7170f9445e2
--- /dev/null
+++ b/packages/app/src/servers/connect/redeem.ts
@@ -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
+
+/** 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(", ") }) }
+ }
+}
diff --git a/packages/app/src/servers/connect/scanner.css b/packages/app/src/servers/connect/scanner.css
index eb9b9c899e4b..9f568c7eae7f 100644
--- a/packages/app/src/servers/connect/scanner.css
+++ b/packages/app/src/servers/connect/scanner.css
@@ -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);
+ }
}
diff --git a/packages/app/src/servers/connect/scanner.tsx b/packages/app/src/servers/connect/scanner.tsx
index b14dab33581e..bf228056481d 100644
--- a/packages/app/src/servers/connect/scanner.tsx
+++ b/packages/app/src/servers/connect/scanner.tsx
@@ -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()
+ 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
+
+ {(http) => (
+
+ )}
+ {language.t("dialog.server.status.default")}
diff --git a/packages/app/src/settings/settings.css b/packages/app/src/settings/settings.css
index 2f6553a14cba..73947e71be62 100644
--- a/packages/app/src/settings/settings.css
+++ b/packages/app/src/settings/settings.css
@@ -1955,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);
}
@@ -1965,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%);
}
diff --git a/packages/gui-extensions/AGENTS.md b/packages/gui-extensions/AGENTS.md
index 7e9d902ece1e..9bc7257b6603 100644
--- a/packages/gui-extensions/AGENTS.md
+++ b/packages/gui-extensions/AGENTS.md
@@ -32,7 +32,7 @@ Built-in features of the desktop and web app, each behind the SDK in `src/sdk/`.
An instance lives from `setup` until it is disabled, reloaded, removed, or its window closes. A reload can land at any `await`, so code that outlives a tick must prove it still belongs to the live instance.
- Everything registered through `ctx` (contributions, contracts, Ipc listeners, dialogs, embeds) is withdrawn with the current owner (a component, a `createKeyed` run), else with the instance. `ctx.dialogs.open` returns a handle whose `close()` closes that dialog alone. Anything else you start (timers, DOM listeners, subscriptions) needs teardown: in the window, Solid's `onCleanup` (setup runs under the extension's root owner); in main, `ctx.scope.addFinalizer`, which runs at once when the instance is already gone. `setup` returns nothing.
-- Window `setup` is synchronous: an async one fails to compile, and the host fails it at runtime. Put async work in `createKeyed` or `createLatest`, with `ctx.signal` or a signal derived from it. Only main `setup` may be async. After every `await`, return if the signal aborted (`ctx.signal` in window work, `ctx.scope.signal` in main) before touching state or contributing; there is no owner after an `await`, so window teardown that starts later listens to `ctx.signal`. Pass the signal to Ipc calls and long work. Methods without an input schema take options only, e.g. `pairing.info({ signal })`.
+- Window `setup` is synchronous: an async one fails to compile, and the host fails it at runtime. Put async work in `createKeyed` or `createLatest`, with `ctx.signal` or a signal derived from it. Only main `setup` may be async. After every `await`, return if the signal aborted (`ctx.signal` in window work, `ctx.scope.signal` in main) before touching state or contributing; there is no owner after an `await`, so window teardown that starts later listens to `ctx.signal`. Pass the signal to Ipc calls and long work. Methods without an input schema take options only, e.g. `pairing.screenActive({ signal })`.
- Never keep a value from a shorter lifetime in a longer one. Read a server's `client`, `data` and `url` from its live `ServerRef` each time: a restarted or re-authenticated server gets a new controller under the same id. Keep per-session state in a session-scoped store, or in a map keyed by `session.key` that you prune.
- Each routed session gets its own frozen `MountedSession`, and a new one when it moves to another directory (same `key` and `visit`, new `directory`). A render receives the next one through `props.session` (panels), `input.session` (slots) or `state.session` (tab labels) without remounting. Read those getters where you use them; never copy the object into a variable or key a cache by it, which would rebuild on every switch. A `MountedSession` has no actions that follow the route: the workspace files, comments and composer belong to the session screen, one object while it routes A, B and A again. Panel callbacks take one named input with a non-null, constant `screen`; every `session.*` slot gets it too, but `window.bottom` does not. Render `tab` and `session` fields are live getters; lifecycle callbacks keep their event's tab and call metadata. Key per-screen caches by the screen. Commands and link handlers read `ctx.screen.current()` where they act, never once in setup. It is defined exactly when `ctx.sessions.current()` is: from just after the screen's first render until the route leaves it, so never on Home or a draft. Renders and render effects use their inputs, not these accessors; pass the owning screen on, as `Changes.watch(screen, source)` takes it.
- Main: `ctx.lifecycle.restart(handoff, { keep: ctx.scope })` keeps the extension that owns `ctx.scope` (the caller by default) active until the handoff settles; when it rejects, return to a state the user can retry from.
diff --git a/packages/gui-extensions/README.md b/packages/gui-extensions/README.md
index e22d529ad3de..c138ae2e8162 100644
--- a/packages/gui-extensions/README.md
+++ b/packages/gui-extensions/README.md
@@ -186,14 +186,22 @@ const Page = lazy(() => import("./page"))
onCleanup(onIdle(() => void Page.preload()))
```
-Pairing's shipping page uses a query, not `createLatest`. It passes the query's abort signal directly to the no-input Ipc method; options are the only argument:
+Pairing's shipping page uses a query, not `createLatest`. It passes the query's abort signal directly to the no-input Ipc method; options are the only argument. The generation in the key asks again when main comes back:
-
+
```tsx
-const local = useQuery(() => ({
- queryKey: [ctx.id, "local"],
- queryFn: (input) => props.client.info({ signal: input.signal }),
+const screenActive = useQuery(() => ({
+ queryKey: [ctx.id, "screen-active", display()?.generation],
+ queryFn: (input) => {
+ const live = display()
+
+ if (!live) throw new Error("Pairing's main side is not active")
+
+ return live.value.screenActive({ signal: input.signal })
+ },
+ enabled: !!display(),
+ gcTime: 0,
}))
```
@@ -236,7 +244,7 @@ SDK icon fields use `IconName` from `@opencode/ui/icons/catalog`, a dependency-f
| `ctx.appearance` | `Appearance` | The user's mono font |
| `ctx.router` | `Router` | The route path, and whether it is changing |
| `ctx.keybinds` | `Keybinds` | Display and matching of command keybinds |
-| `ctx.servers` | `Servers` | Ids of the servers the app lists |
+| `ctx.servers` | `Servers` | The servers the app lists, and each one's live `ServerRef` |
| `ctx.workspaces` | `Workspaces` | `on("remove", …)` when a workspace is removed |
| `ctx.scope` (main) | `Scope` | The instance's lifetime |
| `ctx.storage` (main) | `Storage` | Synchronous storage, the same `Persisted` shape |
@@ -268,7 +276,7 @@ sequenceDiagram
M-->>W: emit("check", null, window): the app menu asks this window to check
```
-- Define it in `contract.ts`. Its id is your extension id, or `.`. A method without an input schema takes only optional call options: `pairing.info({ signal })`, never an `undefined` placeholder.
+- Define it in `contract.ts`. Its id is your extension id, or `.`. A method without an input schema takes only optional call options: `pairing.screenActive({ signal })`, never an `undefined` placeholder.
- Your own Ipc goes in `provides` only: the main entry provides it, and the window entry reads it as `ctx.uses.name`. Another extension's Ipc goes in `uses`.
- On the web there is no main process: the Ipc is always `inactive`.
- `IpcsProvided` fails to compile when a window provides, uses or requires an Ipc that no main entry provides. [`src/builtins.typecheck.ts`](src/builtins.typecheck.ts) checks the built-ins.
@@ -320,20 +328,16 @@ const act = (name: "check" | "install") => {
}
```
-The pairing settings page renders only while its main side is up, so it never offers a control that cannot answer:
+The pairing settings page waits for its main side only where it needs it. The link and QR code come from the local server's `ServerRef`, so they render without main; the display setting renders only while main is active, so it never offers a switch that cannot answer:
-
+
```tsx
-// The page shows nothing until the main side is up, and again while it is away.
-export default function PairingPage(props: { pairing: Accessor> }) {
- const client = () => {
- const live = props.pairing()
-
- return live.status === "active" ? live.value : undefined
- }
+// Only the display setting needs pairing's main side; the link and QR code come from the server, so they never wait.
+const display = () => {
+ const live = props.pairing()
- return {(client) => }
+ return live.status === "active" ? live : undefined
}
```
@@ -437,9 +441,9 @@ legacy: ["summary"],
## Build your first extension
-Pairing shows another device how to reach this machine's server, and keeps the display awake. It is small and uses both processes: a main entry that owns a power-save blocker and the server's credentials, and a window entry that adds a settings page and a command. Each block below is its shipping source.
+Pairing shows another device how to reach this machine's server, and keeps the display awake. It is small and uses both processes: a main entry that owns a power-save blocker, and a window entry that adds a settings page and a command. The window asks the local server for its addresses and pairing codes through `ctx.servers`: that server's `ServerRef` is already authenticated in the window, so its credentials never leave main. Each block below is its shipping source.
-1. **Define the contract.** `contract.ts` holds the tokens other code may import, here the Ipc between pairing's main and window entries. The schemas type both sides and encode every value.
+1. **Define the contract.** `contract.ts` holds the tokens other code may import, here the Ipc between pairing's main and window entries. It carries only what needs the main process, the display sleep blocker. The schemas type both sides and encode every value.
@@ -447,23 +451,22 @@ Pairing shows another device how to reach this machine's server, and keeps the d
import { Schema } from "effect"
import { Ipc } from "../sdk"
-export const PairingInfo = Schema.Struct({ urls: Schema.Array(Schema.String) })
-
-/** Pairs other devices with this machine's local server and keeps its display awake. */
+/**
+ * Keeps this machine's display awake while another device uses it. The window asks the local server for addresses and
+ * pairing codes through its own `ServerRef`; only the display sleep blocker needs the main process.
+ */
export const Pairing = Ipc.define({
id: "pairing",
methods: {
- /** The local server's advertised URLs. */
- info: { output: PairingInfo },
- /** A single-use code for an `/auth/connect/:code` link. */
- code: { output: Schema.String },
+ /** Whether main holds the display sleep blocker. */
screenActive: { output: Schema.Boolean },
+ /** Holds or releases the display sleep blocker, and remembers the choice. */
setScreenActive: { input: Schema.Boolean },
},
})
```
-2. **Write the definition.** Pairing provides its Ipc, so its window entry reads it as `ctx.uses.pairing` with no second declaration. Whether main keeps the display awake is a main store, imported once from the key the desktop kept it under before.
+2. **Write the definition.** Pairing provides its Ipc, so its window entry reads it as `ctx.uses.pairing` with no second declaration. Whether main keeps the display awake is a main store, imported once from the key the desktop kept it under before. The addresses pairing links use are a window store, which the settings page reads.
@@ -479,12 +482,14 @@ export default Extension.define({
stores: {
// Whether main keeps the display awake; stored before in the desktop's own settings namespace.
keepScreenActive: Store.main(Schema.Boolean, false, { state: ["opencode.settings", "keepScreenActive"] }),
+ // An address of this computer the server cannot see (a VPN, tunnel or proxy), and the address links use.
+ links: Store.global(Schema.Struct({ custom: Schema.String, selected: Schema.String }), { custom: "", selected: "" }),
},
i18n: { en },
})
```
-3. **Provide it from main.** `MainSetup` types `ctx.stores`, which main reads synchronously. A finalizer releases the blocker when the instance goes away, and the sidecar's credentials never leave main.
+3. **Provide it from main.** `MainSetup` types `ctx.stores`, which main reads synchronously. A finalizer releases the blocker when the instance goes away.
@@ -517,18 +522,7 @@ const setup: MainSetup = (ctx) => {
if (stored.value) keepScreenActive(true)
ctx.scope.addFinalizer(release)
- const client = async () => {
- const server = ctx.serverEndpoints.get("sidecar")
-
- if (!server) throw new Error("The local desktop server is not ready")
- const { OpenCode } = await import("@opencode/client/promise")
-
- return OpenCode.make({ baseUrl: server.url, headers: server.headers })
- }
-
ctx.provide(Pairing, {
- info: async () => ({ urls: (await (await client()).server.info()).urls }),
- code: async () => (await (await client()).server.pair()).code,
screenActive: () => blocker.id !== undefined && powerSaveBlocker.isStarted(blocker.id),
setScreenActive: (enabled) => keepScreenActive(enabled),
})
@@ -537,7 +531,7 @@ const setup: MainSetup = (ctx) => {
export default setup
```
-4. **Write the window entry.** Pairing exists only on desktop, so setup returns at once on the web. The settings page is heavy, so it loads behind `lazy()` and compiles while the app idles; its search entries are indexed without mounting it. The page receives `ctx.uses.pairing` and renders nothing until main answers (see `PairingPage` under [Live and failure handling](#live-and-failure-handling)).
+4. **Write the window entry.** Pairing exists only on desktop, so setup returns at once on the web. The settings page is heavy, so it loads behind `lazy()` and compiles while the app idles; its search entries are indexed without mounting it. The page receives the desktop's own server, found through `ctx.servers` by `builtin`, and `ctx.uses.pairing`. The search entry for the display setting, like the setting itself, exists only while main answers (see `display` under [Live and failure handling](#live-and-failure-handling)).
@@ -549,10 +543,19 @@ import type definition from "./index"
const setup: Setup = (ctx) => {
if (!ctx.desktop) return
const layout = ctx.layout
+ const servers = ctx.servers
+ const pairing = ctx.uses.pairing
const Page = lazy(() => import("./page"))
// Settings rows are small; load them while idle so settings opens without a blank row.
onCleanup(onIdle(() => void Page.preload()))
+ // The desktop's own server. Its ref is already authenticated in this window, so codes need no main process.
+ const local = () =>
+ servers
+ .list()
+ .map((id) => servers.get(id))
+ .find((server) => server?.builtin)
+
ctx.add(SettingsPage, {
id: "pairing",
icon: "server",
@@ -561,8 +564,13 @@ const setup: Setup = (ctx) => {
return ctx.t("title")
},
get entries() {
+ const pairingEntry = { id: "pairing", title: ctx.t("title"), keywords: "pair device qr local" }
+
+ // The display setting exists only while pairing's main side answers.
+ if (pairing().status !== "active") return [pairingEntry]
+
return [
- { id: "pairing", title: ctx.t("title"), keywords: "pair device qr local" },
+ pairingEntry,
{
id: "settings-keep-screen-active",
title: ctx.t("screenActive.title"),
@@ -573,7 +581,7 @@ const setup: Setup = (ctx) => {
},
render: () => (
-
+
),
})
diff --git a/packages/gui-extensions/src/browser/panel.fixture.tsx b/packages/gui-extensions/src/browser/panel.fixture.tsx
index 8e28f8afe299..ae94d5b4b563 100644
--- a/packages/gui-extensions/src/browser/panel.fixture.tsx
+++ b/packages/gui-extensions/src/browser/panel.fixture.tsx
@@ -767,7 +767,7 @@ export function mountBrowserRegion(input: RegionHost) {
const appearance: Appearance = { font: () => "monospace" }
const router: Router = { routing: () => false, path: () => "/" }
const keybinds: Keybinds = { keybind: () => [], keys: (bind) => bind.split("+"), matches: () => false }
- const servers: Servers = { list: () => [server.id] }
+ const servers: Servers = { list: () => [server.id], get: () => undefined }
const workspaces: Workspaces = { on: () => () => undefined }
const listeners = new Set<(value: { binding: string; event: PaneEvent }) => void>()
diff --git a/packages/gui-extensions/src/pairing/contract.ts b/packages/gui-extensions/src/pairing/contract.ts
index 7aca9a2ca258..1bb46cbfee72 100644
--- a/packages/gui-extensions/src/pairing/contract.ts
+++ b/packages/gui-extensions/src/pairing/contract.ts
@@ -1,17 +1,16 @@
import { Schema } from "effect"
import { Ipc } from "../sdk"
-export const PairingInfo = Schema.Struct({ urls: Schema.Array(Schema.String) })
-
-/** Pairs other devices with this machine's local server and keeps its display awake. */
+/**
+ * Keeps this machine's display awake while another device uses it. The window asks the local server for addresses and
+ * pairing codes through its own `ServerRef`; only the display sleep blocker needs the main process.
+ */
export const Pairing = Ipc.define({
id: "pairing",
methods: {
- /** The local server's advertised URLs. */
- info: { output: PairingInfo },
- /** A single-use code for an `/auth/connect/:code` link. */
- code: { output: Schema.String },
+ /** Whether main holds the display sleep blocker. */
screenActive: { output: Schema.Boolean },
+ /** Holds or releases the display sleep blocker, and remembers the choice. */
setScreenActive: { input: Schema.Boolean },
},
})
diff --git a/packages/gui-extensions/src/pairing/i18n/en.ts b/packages/gui-extensions/src/pairing/i18n/en.ts
index 465ae5770b1c..b0d11ddaf9f8 100644
--- a/packages/gui-extensions/src/pairing/i18n/en.ts
+++ b/packages/gui-extensions/src/pairing/i18n/en.ts
@@ -9,7 +9,27 @@ export default {
"screenActive.error": "Could not update the screen activity setting. Try again.",
description: "Connect another device to this machine's OpenCode server.",
qr: "Pairing QR code",
- copy: "Copy details",
+ copyLink: "Copy link",
"copy.error": "Could not copy pairing details. Try again.",
error: "Could not update pairing details. Try again.",
+ routes: "Connect with",
+ "route.local": "Local network",
+ "route.local.description": "Devices on the same network",
+ "route.vpn": "VPN",
+ "route.vpn.description": "Devices on the same VPN",
+ "route.custom": "Custom address",
+ "route.custom.description": "Devices that can open this address",
+ "route.other": "Network address",
+ "route.other.description": "Devices that can reach this address",
+ expires: "Expires in {{time}}",
+ refresh: "New code",
+ "custom.title": "Custom address",
+ "custom.description":
+ "An address that reaches this computer from other devices, such as a VPN, tunnel, or reverse proxy URL.",
+ "custom.placeholder": "https://opencode.example.com",
+ "custom.invalid": "Enter an HTTP or HTTPS address without a path, such as https://opencode.example.com.",
+ "unreachable.title": "Only this computer can reach the OpenCode server",
+ "unreachable.description":
+ "To pair a device on your network, run this command in a terminal, then reopen Pairing. You can also add a custom address below.",
+ "unreachable.copy": "Copy command",
}
diff --git a/packages/gui-extensions/src/pairing/index.ts b/packages/gui-extensions/src/pairing/index.ts
index f930c985d795..90715e954ffb 100644
--- a/packages/gui-extensions/src/pairing/index.ts
+++ b/packages/gui-extensions/src/pairing/index.ts
@@ -9,6 +9,8 @@ export default Extension.define({
stores: {
// Whether main keeps the display awake; stored before in the desktop's own settings namespace.
keepScreenActive: Store.main(Schema.Boolean, false, { state: ["opencode.settings", "keepScreenActive"] }),
+ // An address of this computer the server cannot see (a VPN, tunnel or proxy), and the address links use.
+ links: Store.global(Schema.Struct({ custom: Schema.String, selected: Schema.String }), { custom: "", selected: "" }),
},
i18n: { en },
})
diff --git a/packages/gui-extensions/src/pairing/main.ts b/packages/gui-extensions/src/pairing/main.ts
index 19846197325b..aab89479d1fc 100644
--- a/packages/gui-extensions/src/pairing/main.ts
+++ b/packages/gui-extensions/src/pairing/main.ts
@@ -26,18 +26,7 @@ const setup: MainSetup = (ctx) => {
if (stored.value) keepScreenActive(true)
ctx.scope.addFinalizer(release)
- const client = async () => {
- const server = ctx.serverEndpoints.get("sidecar")
-
- if (!server) throw new Error("The local desktop server is not ready")
- const { OpenCode } = await import("@opencode/client/promise")
-
- return OpenCode.make({ baseUrl: server.url, headers: server.headers })
- }
-
ctx.provide(Pairing, {
- info: async () => ({ urls: (await (await client()).server.info()).urls }),
- code: async () => (await (await client()).server.pair()).code,
screenActive: () => blocker.id !== undefined && powerSaveBlocker.isStarted(blocker.id),
setScreenActive: (enabled) => keepScreenActive(enabled),
})
diff --git a/packages/gui-extensions/src/pairing/page.stories.tsx b/packages/gui-extensions/src/pairing/page.stories.tsx
new file mode 100644
index 000000000000..fc9ef6b270b8
--- /dev/null
+++ b/packages/gui-extensions/src/pairing/page.stories.tsx
@@ -0,0 +1,50 @@
+import { PairingHost, pairingClient, pairingServer } from "../../../storybook/.storybook/mocks/gui-extensions/pairing"
+import PairingPage from "./page"
+
+const story = (urls: readonly string[], links?: { custom?: string; selected?: string }, main = true) => ({
+ render: () => {
+ const server = pairingServer(urls)
+ const client = pairingClient()
+
+ return (
+
+ server}
+ pairing={() =>
+ main ? { status: "active", value: client, generation: 1 } : { status: "inactive", reason: "disabled" }
+ }
+ />
+
+ )
+ },
+})
+
+export default {
+ title: "Extensions/Pairing",
+ id: "extensions-pairing",
+ parameters: {
+ docs: {
+ description: {
+ component: "Settings → Pairing on desktop: the addresses a pairing link can use, and its QR code.",
+ },
+ },
+ },
+}
+
+/** The server listens on the local network and on a VPN address. */
+export const NetworkAddresses = story([
+ "http://127.0.0.1:49374",
+ "http://192.168.10.248:49374",
+ "http://100.87.251.43:49374",
+])
+
+/** A saved custom address, such as a VPN, tunnel or reverse proxy URL, is offered first. */
+export const CustomAddress = story(["http://127.0.0.1:49374", "http://192.168.10.248:49374"], {
+ custom: "https://opencode.example.com",
+})
+
+/** The default service only listens on this computer, so the page explains how to let other devices in. */
+export const OnlyThisComputer = story(["http://127.0.0.1:49374"])
+
+/** Pairing's main side is not running: the link still works from the server, and only the display setting is gone. */
+export const WithoutMainProcess = story(["http://127.0.0.1:49374", "http://192.168.10.248:49374"], undefined, false)
diff --git a/packages/gui-extensions/src/pairing/page.tsx b/packages/gui-extensions/src/pairing/page.tsx
index 3da07a10d5f1..f0af374c28fe 100644
--- a/packages/gui-extensions/src/pairing/page.tsx
+++ b/packages/gui-extensions/src/pairing/page.tsx
@@ -1,67 +1,142 @@
import { Button } from "@opencode/ui/button"
import { Dialog, DialogBody, DialogHeader, DialogTitleGroup } from "@opencode/ui/dialog"
import { Icon } from "@opencode/ui/icon"
+import { RadioGroup, RadioItem } from "@opencode/ui/radio"
import { Switch } from "@opencode/ui/switch"
+import { TextInput } from "@opencode/ui/text-input"
import { Tooltip } from "@opencode/ui/tooltip"
import { useMutation, useQuery, useQueryClient } from "@tanstack/solid-query"
-import { createMemo, onCleanup, Show, type Accessor, type JSX } from "solid-js"
+import { createMemo, createSignal, For, onCleanup, Show, type Accessor, type JSX } from "solid-js"
+import { createStore } from "solid-js/store"
import { renderSVG } from "uqr"
import { useExtension, type Live, type IpcClient } from "../sdk"
import type { Pairing } from "./contract"
+import type definition from "./index"
+import { customAddress, pairingRoutes, type PairingRoute, type RouteKind } from "./routes"
type Client = IpcClient
-// The page shows nothing until the main side is up, and again while it is away.
-export default function PairingPage(props: { pairing: Accessor> }) {
- const client = () => {
- const live = props.pairing()
-
- return live.status === "active" ? live.value : undefined
+/**
+ * The parts of the local server's `ServerRef` the page uses. The renderer passes the live ref, so `client` is read
+ * on every call and follows a restarted server.
+ */
+export type PairingServer = {
+ readonly client: {
+ readonly server: {
+ info(options: { signal: AbortSignal }): Promise<{ readonly urls: readonly string[] }>
+ pair(options: { signal: AbortSignal }): Promise<{ readonly code: string; readonly expires_in: number }>
+ }
}
-
- return {(client) => }
}
-function SettingsPairing(props: { client: Client }) {
- const ctx = useExtension()
+/** The command that lets the local server accept connections from other devices. */
+const LISTEN_COMMAND = "opencode service set hostname 0.0.0.0"
+
+export default function PairingPage(props: {
+ server: Accessor
+ pairing: Accessor>
+}) {
+ const ctx = useExtension()
// The extension's dialogs close with it, so disabling pairing also stops the dialog's code polling.
const dialogs = ctx.dialogs
+ const system = ctx.system
+ const links = ctx.stores.links
const queryClient = useQueryClient()
+ const [custom, setCustom] = createStore({ draft: links.value.custom, invalid: false })
const local = useQuery(() => ({
queryKey: [ctx.id, "local"],
- queryFn: (input) => props.client.info({ signal: input.signal }),
+ queryFn: (input) => {
+ const server = props.server()
+
+ if (!server) throw new Error("The local server is not listed")
+
+ return server.client.server.info({ signal: input.signal })
+ },
+ enabled: !!props.server(),
+ // Asks again each time the page opens: the guidance below tells the user to change the hostname and reopen Pairing.
+ gcTime: 0,
}))
// Reading pending query data would suspend the entire settings surface.
const localInfo = () => (local.isSuccess ? local.data : undefined)
- // Loopback URLs are useless to the scanning device, so the QR code only carries reachable addresses.
- const localHosts = createMemo(() =>
- (localInfo()?.urls ?? []).filter((value) => {
- const host = new URL(value).hostname
-
- return (
- host !== "localhost" &&
- !host.endsWith(".localhost") &&
- !host.startsWith("127.") &&
- host !== "[::1]" &&
- host !== "0.0.0.0" &&
- host !== "[::]"
- )
- }),
- )
+ // The custom address field starts with the saved address, so a valid address being typed counts at once and a cleared
+ // or invalid one does not; it is saved on Enter, when the field loses focus, and when the dialog opens.
+ const routes = createMemo(() => pairingRoutes(localInfo()?.urls ?? [], customAddress(custom.draft.trim()) ?? ""))
+
+ // Whether the server itself listens only on this computer. Not derived from the routes above, so typing a custom
+ // address does not remove the notice above the field while the user types.
+ const listensLocally = () => local.isSuccess && pairingRoutes(local.data.urls, "").length === 0
+
+ const code = async (signal: AbortSignal) => {
+ const server = props.server()
+
+ if (!server) throw new Error("The local server is not listed")
+ const pairing = await server.client.server.pair({ signal })
+
+ return { code: pairing.code, expiresIn: pairing.expires_in }
+ }
+
+ // Only the display setting needs pairing's main side; the link and QR code come from the server, so they never wait.
+ const display = () => {
+ const live = props.pairing()
+
+ return live.status === "active" ? live : undefined
+ }
const screenActive = useQuery(() => ({
- queryKey: [ctx.id, "screen-active"],
- queryFn: () => props.client.screenActive(),
+ queryKey: [ctx.id, "screen-active", display()?.generation],
+ queryFn: (input) => {
+ const live = display()
+
+ if (!live) throw new Error("Pairing's main side is not active")
+
+ return live.value.screenActive({ signal: input.signal })
+ },
+ enabled: !!display(),
+ gcTime: 0,
}))
const screenActivity = useMutation(() => ({
- mutationFn: (enabled: boolean) => props.client.setScreenActive(enabled),
- onSuccess: (_, enabled) => queryClient.setQueryData([ctx.id, "screen-active"], enabled),
+ mutationFn: (enabled: boolean) => {
+ const live = display()
+
+ if (!live) throw new Error("Pairing's main side is not active")
+
+ return live.value.setScreenActive(enabled)
+ },
+ onSuccess: (_, enabled) => queryClient.setQueryData([ctx.id, "screen-active", display()?.generation], enabled),
+ }))
+
+ // "Copied" shows for two seconds after each copy.
+ const commandCopied: CopiedTimer = {}
+ onCleanup(() => clearTimeout(commandCopied.timeout))
+
+ const copyCommand = useMutation(() => ({
+ mutationFn: () => system.copy(LISTEN_COMMAND),
+ onMutate: () => clearTimeout(commandCopied.timeout),
+ onSuccess: () => {
+ commandCopied.timeout = setTimeout(() => copyCommand.reset(), 2000)
+ },
}))
+ const saveCustom = () => {
+ const draft = custom.draft.trim()
+ const address = draft ? customAddress(draft) : ""
+
+ if (address === undefined) return void setCustom("invalid", true)
+ setCustom({ draft: address, invalid: false })
+
+ if (address === links.value.custom) return
+ links.update((value) => {
+ value.custom = address
+
+ // A new address is what the user wants to share next.
+ if (address) value.selected = address
+ })
+ }
+
return (
<>