Skip to content

fix(app): handle one-time pairing links across the GUI - #53257

Merged
Hona merged 4 commits into
anomalyco:v2from
Hona:pairing-links
Oct 7, 2026
Merged

Hona merged 4 commits into
anomalyco:v2from
Hona:pairing-links

Conversation

@Hona

@Hona Hona commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

#50970 and #50972 replaced password pairing with one-time /auth/connect/<code> links. The connect screen handled the new links, but the rest of the GUI did not: the Add server dialog treated a link as a server address, desktop had no way to redeem one, and a server whose 30-day session expired showed a red dot and no action. This PR fixes the app and GUI side only. It needs no server or protocol changes.

flowchart LR
  Link["Pasted or scanned link<br/>http(s)://host/auth/connect/code"] --> Redeem["useRedeemPairing()"]
  Redeem -->|200 token| Save["Save url + token as password"]
  Redeem -->|401| Expired["Link expired or already used"]
  Redeem -->|network error| Mixed{"https page,<br/>http server?"}
  Mixed -->|yes| MixedMsg["Use an HTTPS address"]
  Mixed -->|no| Unreachable["Could not reach host"]
  Redeem -->|"/connect#… (old server)"| Legacy["Update OpenCode on the server"]
Loading
  • The pairing link comes first, the password second. The connect screen and the Add/Edit server dialog start with a single Pairing link field. A quiet No pairing link? Use password switch, like the Console's "Service account? Use API key", opens the server address and password form. That form says where to find the password (opencode service get password). If you paste a bare address into the link field, the form switches to the password view with the address filled in.
  • One redeem path (servers/connect/redeem.ts). The connect screen, the dialog and the QR scanner all use it, so they all show the same specific errors. Before, every failure said "expired", even when the server was unreachable and the code still worked. A redeemed token is kept, so if the connection check fails, a retry does not spend the link again.
  • The pairing link field only switches to the password form for a real server address (a host with no path). A bare code or a cut-off link shows "Paste the full pairing link…" instead.
  • Signing in again keeps the server's own address. A token works on every address of its server, so when you re-pair localhost:4096 with a link that names 127.0.0.1:4096 (as opencode pair prints), the app updates the existing entry instead of adding a duplicate.
  • Every redeem path reuses a token it already got — pasted link, link pasted into the address field, and QR scan (the scanner redeems with the form's own redeem and fills the link in). A retry after a failed connection check never spends the code again.
  • A signed-out server's action continues after you save (e.g. opening a project), as it does for SSH servers.
  • Add/Edit server dialog redeems links. This is now the way to add a remote server on desktop, which has no scanner.
  • Re-pair a signed-out server. An HTTP server that answers 401 shows the lock icon and Authenticate, as SSH servers already do. Authenticate opens the edit dialog on the pairing link field, with a notice.
  • Scanner: skips a code that already failed instead of redeeming it again on every frame, keeps scanning after an error, and shows "Checking…" while it redeems.
  • On plain http (a phone that opens the server over the LAN), the disabled Scan QR code button is replaced by guidance to use the device's camera app.
  • Desktop Settings → Pairing:
    • Lists every reachable address with a label (Local network / VPN / Custom address). Before, it silently used the first non-loopback address. The choice is remembered.
    • Shows the whole link, code included, not just the host, so you can read it out or type it on a device without a camera.
    • Custom address field for addresses the server cannot see: a VPN, a tunnel, or a reverse proxy.
    • When the server listens only on 127.0.0.1, the page explains why and shows opencode service set hostname 0.0.0.0 with a copy button, instead of a disabled button.
    • Expiry countdown from expires_in, and a New code action, which also appears next to the error when getting a code fails. The link now refreshes when the code expires, not every 60 s.
  • New copy is English only. Two keys that no screen uses any more are removed from every locale.

Connect screen

Before

After: pairing link first

After: Use password

Connect screen: pasted link to a server that cannot be reached

Before

After

Connect screen over plain http

Before

After

Add server dialog

Before: a pasted pairing link is treated as an address

After: pairing link first

After: added

After: Use password

Signed-out server (expired session or changed password)

Before

After

After: Authenticate opens the edit dialog

After: paste a new link and save

Desktop Settings → Pairing: server listens only on this computer

Before

After

Desktop Settings → Pairing: details dialog

Before

After

After: custom address

After: invalid custom address

Scanner failure

QrScanner sends decoder failures (anything but "no QR code in this frame") only to console.log, so a blocked decoder worker (the CSP bug fixed in #53278) left a live preview that never scanned. scanner.tsx now shows an error for them. Checked in Chromium with a fake camera: with worker-src removed the message appears; with it allowed, 15 s of frames without a code show nothing.

Pairing page without the main process

Pairing's main entry existed to read the sidecar's credentials for server.info() and server.pair(). The window's ServerRef is already authenticated for the sidecar (wireRendererHeaders adds the credentials in main), so:

flowchart LR
  Page["pairing/page.tsx"] -->|"ctx.servers.get(id).client<br/>server.info() / server.pair()"| Server["local server"]
  Page -->|"Pairing Ipc<br/>screenActive / setScreenActive"| Main["pairing/main.ts<br/>powerSaveBlocker"]
Loading
  • New ctx.servers.get(id) returns a listed server's live ServerRef (sdk/host-apis.ts, wired to the host's existing server(id) in runtime/extension/host-apis.tsx).
  • The renderer passes the desktop's own server (found by builtin). The link and QR code render from it and no longer wait for main.
  • The Pairing Ipc keeps only the display sleep blocker. The keep-screen-active row, and its settings search entry, show only while main is active.
  • Inbound pairing (servers/connect) stays in the app: ConnectServerScreen renders before extensions mount when signed out on the web. DialogServer already redeems pasted links on submit through the same useRedeemPairing.
  • Checked: the extension component tests (host, graph matrix) pass, and a new WithoutMainProcess story issues a code with main inactive.

Pairing dialog with main inactive

Rebased on #53588 (every pairing address in QR codes)

  • pairingLink reads both links and the {"code","urls"} QR payload. redeemPairingLink tries every address in parallel, as feat(cli): include every pairing address in QR codes #53588 does, and still tells the outcomes apart: "expired" only when a server answered 401, otherwise "unreachable". The mixed-content message shows only when every address is plain HTTP from an HTTPS page.
  • The desktop Pairing dialog's QR code carries every listed address as that JSON, the chosen one first. The copyable link stays the chosen address.
  • A phone's camera app cannot open a JSON QR code, so the plain-HTTP hint now says to paste the link instead of suggesting the camera app.

@Hona
Hona requested a review from Brendonovich as a code owner October 5, 2026 02:18
Copilot AI balanced review requested due to automatic review settings October 5, 2026 02:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Pairing link first with a password fallback in the connect screen and the Add/Edit server dialog, one redeem path with specific errors, re-pairing signed-out servers from Home and Settings, and a desktop Pairing page that lists every reachable address and gets codes from the window's server client.
Hona added 3 commits October 7, 2026 11:32
…and keep the chosen pairing address

A server's controller kept the password it started with, so changing a working server's password left it on the old one. Controllers now rebuild when the listed connection's address or password changes, and callers holding an older connection get the current one. Pairing keeps the first address when its token works there, a scanned QR code fills in a readable link, ServerRef no longer starts a controller just to read builtin, and the docs no longer name the removed pairing.info.
… pair button for a typed custom address

A retried link with the same code keeps an address the user edited. The server dialog closes before its continuation opens the next dialog. The pairing page counts a valid custom address while it is typed.
…ettle custom address edits

The chosen pairing address is preferred only when its own redeem attempt answered, so an unreachable local address no longer delays pairing by five seconds. An unchanged save on a signed-out server checks its credentials instead of closing. The custom address comes from the field alone, and typing it no longer moves the field. SDK examples name the remaining Pairing methods.
@Hona
Hona merged commit eebe9d3 into anomalyco:v2 Oct 7, 2026
9 checks passed
@lr0pb

lr0pb commented Oct 7, 2026

Copy link
Copy Markdown

Hey, doing pairing on windows with only Desktop app installed makes using opencode command in cli not really possibe, I had to run

$oc = "C:\Users\<user>\AppData\Local\Programs\@opencode-aidesktop\resources\opencode-cli.exe"

and then

& $oc service set hostname 0.0.0.0

Probably this should be mentioned in the pairing section or just add opencode bin to PATH on Desktop install

@Hona

Hona commented Oct 8, 2026 •

Copy link
Copy Markdown
Member Author

Thanks for the comment. A few things. I want to make it a button to do this nicely for you, and also for the built-in terminal, just like add the built-in CLI to path. So, yeah, a couple things to improve, but yeah, just gonna make it better overall

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants