diff --git a/docs/changelog.mdx b/docs/changelog.mdx index e800cfe1..86ad9a57 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -11,6 +11,13 @@ again. ## Unreleased +- **The phone app pairs from a pasted link.** Its Pair and Boxes screens have + *Or paste a pairing link*, for a phone app added to the home screen, which + keeps its own storage apart from the browser. A good pairing link no longer + says it is damaged, and Back always returns to the list instead of leaving + the app. Needs the box upgraded. + [Phone](/guides/phone) + - **Labs: OpenCode, Gemini CLI and Cursor Agent run structured too.** With *Structured agents* on, they start through the Agent Client Protocol: their replies stream into the chat, their permission requests come to it (or, diff --git a/docs/guides/phone.mdx b/docs/guides/phone.mdx index 9e11b26b..b7c49581 100644 --- a/docs/guides/phone.mdx +++ b/docs/guides/phone.mdx @@ -15,6 +15,11 @@ answering them, from your phone. that has phone access on (or use **Copy link** to open it on the phone). Add the page to your home screen (Share → Add to Home Screen) so it opens like an app. + A home screen app keeps its own storage, apart from the browser, so a box + paired in the browser is not paired there. Open the home screen app and + paste the link from **Copy link** into **Or paste a pairing link** on its + Pair screen (or on Boxes, to add one later). + For notifications, install [ntfy](https://ntfy.sh) on the phone, make a topic in Settings → Phone (**New topic**, then **Turn on notifications**), and subscribe to the same topic in diff --git a/internal/box/phoneui/app.js b/internal/box/phoneui/app.js index f0686d7c..45c43ea6 100644 --- a/internal/box/phoneui/app.js +++ b/internal/box/phoneui/app.js @@ -24,30 +24,41 @@ function saveBoxes(boxes) { } catch {} } let boxes = MOCK ? mockBoxes() : loadBoxes(); +let toastTimer = 0; // A pairing link carries boxes in the fragment, which never reaches a // server: #pair=. Keep them, then drop the -// fragment so the token is not left in history. -(function pairFromLink() { - const m = location.hash.match(/^#pair=([A-Za-z0-9_-]+)/); - if (!m) return; +// fragment so the token is not left in history. The same link can be pasted +// on the Pair screen: a Home Screen app keeps its own storage, apart from the +// browser that opened the link, and has no address bar to open it in. +function pairFrom(text) { + const m = String(text).match(/#pair=([A-Za-z0-9_-]+)/); + if (!m) return false; try { const json = atob(m[1].replace(/-/g, "+").replace(/_/g, "/")); let list = JSON.parse(json); if (!Array.isArray(list)) list = [list]; + let added = 0; for (const b of list) { if (!b || !b.url || !b.token) continue; const url = new URL(b.url).origin; boxes = boxes.filter((x) => x.url !== url); boxes.push({ name: String(b.name || new URL(url).hostname), url, token: String(b.token) }); + added++; } + if (!added) throw new Error("no boxes"); saveBoxes(boxes); - toast(list.length === 1 ? "Paired with " + list[0].name : "Paired with " + list.length + " boxes"); + toast(added === 1 ? "Paired with " + list[0].name : "Paired with " + added + " boxes"); + return true; } catch { toast("That pairing link is damaged. Scan the code again."); + return false; } +} +if (/^#pair=/.test(location.hash)) { + pairFrom(location.hash); history.replaceState(null, "", location.pathname + location.search); -})(); +} // --- talking to boxes ------------------------------------------------------- @@ -395,6 +406,7 @@ function showPairing() {
  • Turn on phone access for your boxes.
  • Scan the code with this phone's camera.
  • +

    This phone needs to be on the same tailnet as your boxes (the Tailscale app).

    `; } @@ -405,7 +417,8 @@ function showBoxes() { main.innerHTML = `
    ${boxes .map((b, i) => `
    ${esc(b.name)}
    ${esc(b.url)}
    `) .join("")}
    -

    To add a box, scan its code from Shipyard → Settings → Phone. Forgetting a box only removes it from this phone; turn phone access off in Shipyard to stop it answering.

    `; +
    +

    To add a box, scan its code from Shipyard → Settings → Phone, or paste its link. Forgetting a box only removes it from this phone; turn phone access off in Shipyard to stop it answering.

    `; } // --- routing and chrome --------------------------------------------------------- @@ -453,7 +466,6 @@ function setBar(title, sub, state) { } } -let toastTimer = 0; function toast(msg) { let t = document.querySelector(".toast"); if (!t) { @@ -468,8 +480,13 @@ function toast(msg) { } $("back").addEventListener("click", () => { - if (history.length > 1) history.back(); - else location.hash = ""; + location.hash = ""; +}); +main.addEventListener("click", (e) => { + if (e.target.id !== "pair") return; + const field = $("pairlink"); + if (pairFrom(field.value)) render(); + else if (!/#pair=/.test(field.value)) toast("Paste the whole link from Shipyard → Settings → Phone."); }); main.addEventListener("click", (e) => { const p = e.target.closest("[data-pick]");