From 4b0fcfaa7bb308788e562520d0075061598926a3 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Wed, 26 Aug 2026 23:10:37 +0300 Subject: [PATCH] fix(desktop): boot relay-paired default hosts without the recovery wall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The startup path probed a relay host's stored direct URL — often the pairing creator's own loopback — and any failure landed on the Remote Server Unreachable screen before the renderer's relay restore could run. A relay-capable default host now boots to main on the local substrate for any failed direct probe (unreachable, wrong-service, incompatible), skips the 10s second probe, and lets the renderer's existing restore pick direct-or-relay. --- CHANGELOG.md | 2 ++ packages/electron/main.mjs | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d930ec01..a743d8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ All notable changes to this project will be documented in this file. - Mobile: narrowing a browser window past phone size switches into the mobile layout (and back when widened); the old/new mobile layout setting is gone. - Browser: an agent opening a page with the browser tool no longer pops the browser panel open (or switches the surface you're on) — the page loads in the background and the rail is where you peek at it. - Usage: the Command Code tile is gone — their official API exposes no usage data, so the tile could only fail. +- Desktop: a relay-paired default host no longer greets every restart with the "Remote Server Unreachable" screen — the stored direct address (often the pairing machine's own loopback) failing its probe now boots the app normally and connects over the relay, picking the direct route back up automatically when it answers again. +- Mobile: on Android browsers the composer now stays above the keyboard in the chat too — the keyboard could cover it with no way to scroll it into view; the draft screen's viewport pinning now covers the chat screen on Android. - Auth: an expired OpenChamber login is announced within seconds by a banner with a Log in button, instead of being discovered through failing actions. Sending pauses until login, and a conversation that failed to load reloads itself afterwards. - Chat: a failed send returns your typed prompt to the input — whatever the reason — instead of losing it to an error toast; a mid-send session switch lands it in that session's draft. - Chat: opening a session or resizing panels could strand the view in a large empty space below the last message; the list now returns to the real end, and a width resize keeps a reader who was at the bottom at the bottom. diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 4c8dc66b..b2ede00a 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -1770,6 +1770,15 @@ const computeBootOutcome = ({ envTargetUrl, probe, config, localAvailable }) => : probe?.status === 'wrong-service' ? 'wrong-service' : 'ok'; + // A relay-capable host is not a recovery case just because its stored + // direct URL failed the http probe — that URL is often the pairing + // creator's own loopback (unreachable here, or worse, someone else's + // service). The relay leg is activated in the renderer's relay restore, + // which cannot run from a recovery screen: boot to main on the local + // substrate and let it pick direct-or-relay. + if (status !== 'ok' && sanitizeHostRelayForStorage(host.relay)) { + return { target: 'remote', status: 'ok', hostId: host.id, url: host.apiUrl || host.url, ...availability }; + } return { target: 'remote', status, hostId: host.id, url: host.apiUrl || host.url, ...availability }; }; @@ -3025,12 +3034,24 @@ const resolveInitialUrl = async () => { } } + const defaultHostRelayCapable = Boolean( + config.defaultHostId + && config.defaultHostId !== LOCAL_HOST_ID + && sanitizeHostRelayForStorage(config.hosts.find((entry) => entry.id === config.defaultHostId)?.relay), + ); if (apiBaseUrl && apiBaseUrl !== localUrl) { remoteProbe = await probeHostWithTimeout(apiBaseUrl, 2_000, clientToken, requestHeaders); - if (remoteProbe.status === 'unreachable') { + if (remoteProbe.status === 'unreachable' && !defaultHostRelayCapable) { remoteProbe = await probeHostWithTimeout(apiBaseUrl, 10_000, clientToken, requestHeaders); } - if (remoteProbe.status === 'unreachable') { + // The renderer's relay restore owns transport selection for relay-capable + // hosts; any failed direct probe falls back to the local substrate. + if (remoteProbe.status !== 'ok' && defaultHostRelayCapable) { + apiBaseUrl = localUrl || ''; + clientToken = localUrl ? readDesktopLocalClientToken() : ''; + requestHeaders = {}; + initialUrl = localUiUrl; + } else if (remoteProbe.status === 'unreachable') { state.unreachableHosts.add(apiBaseUrl); apiBaseUrl = localUrl || ''; clientToken = localUrl ? readDesktopLocalClientToken() : '';